From b100e05053e30ddd653ba54a416c1280ba1e0a94 Mon Sep 17 00:00:00 2001 From: baldurk Date: Thu, 27 Aug 2026 13:43:14 +0100 Subject: [PATCH] Add autocomplete for python help entry --- qrenderdoc/Code/pyrenderdoc/PythonContext.cpp | 61 ++++++++ qrenderdoc/Code/pyrenderdoc/PythonContext.h | 1 + .../Code/pyrenderdoc/parse_reflection.py | 2 + qrenderdoc/Windows/PythonShell.cpp | 148 ++++++++++-------- qrenderdoc/Windows/PythonShell.h | 5 +- 5 files changed, 147 insertions(+), 70 deletions(-) diff --git a/qrenderdoc/Code/pyrenderdoc/PythonContext.cpp b/qrenderdoc/Code/pyrenderdoc/PythonContext.cpp index 6c5def58b..7685e2d84 100644 --- a/qrenderdoc/Code/pyrenderdoc/PythonContext.cpp +++ b/qrenderdoc/Code/pyrenderdoc/PythonContext.cpp @@ -1783,6 +1783,67 @@ void PythonContext::reflectSource(QString src) PyGILState_Release(gil); } +void PythonContext::makeHelpContext() +{ + // expand out members of renderdoc and qrenderdoc using the stubs + if(!m_Reflector) + { + for(int i = 0; i < 50 && m_DeferredInit == 0; i++) + QThread::msleep(20); + m_DeferredInit = 1; + + if(!m_Reflector) + return; + } + + PyGILState_STATE gil = PyGILState_Ensure(); + + PyObject *alias_modules = PyObject_SafeGetAttrString(m_Reflector, "alias_modules"); + PyObject *stub_rd = PyDict_GetItemString(alias_modules, "renderdoc"); + PyObject *stub_qrd = PyDict_GetItemString(alias_modules, "qrenderdoc"); + + PyDict_SetItemString(context_namespace, "stub_rd", stub_rd); + PyDict_SetItemString(context_namespace, "stub_qrd", stub_qrd); + + Py_XDECREF(stub_rd); + Py_XDECREF(stub_qrd); + Py_XDECREF(alias_modules); + + PyGILState_Release(gil); + + executeString(lit(R"( +def publicise_module(mod): + for name in dir(mod): + if name[0] == '_' or '_circular' in name: + continue + obj = getattr(mod, name) + if hasattr(obj, '__module__') and mod.__name__ not in getattr(obj, '__module__'): + print(f"{name} is {obj.__module__} vs {mod.__name__}") + continue + globals()[name] = obj + +publicise_module(stub_rd) +publicise_module(stub_qrd) + +del publicise_module + +)")); + + gil = PyGILState_Ensure(); + + PyDict_DelItemString(context_namespace, "stub_rd"); + PyDict_DelItemString(context_namespace, "stub_qrd"); + + if(PyDict_ContainsString(context_namespace, "pyrenderdoc")) + PyDict_DelItemString(context_namespace, "pyrenderdoc"); + if(PyDict_ContainsString(context_namespace, "sys")) + PyDict_DelItemString(context_namespace, "sys"); + + PyGILState_Release(gil); + + reflectSource(QString()); +} + QString PythonContext::tooltipForLoc(int line, int col) { PyGILState_STATE gil = PyGILState_Ensure(); diff --git a/qrenderdoc/Code/pyrenderdoc/PythonContext.h b/qrenderdoc/Code/pyrenderdoc/PythonContext.h index fbaa327e0..c12bf4abc 100644 --- a/qrenderdoc/Code/pyrenderdoc/PythonContext.h +++ b/qrenderdoc/Code/pyrenderdoc/PythonContext.h @@ -118,6 +118,7 @@ public: static QWidget *QWidgetFromPy(PyObject *widget); void reflectSource(QString src); + void makeHelpContext(); QString tooltipForLoc(int line, int col); QList> completionOptions(int line, QString expr, int &prefix_len); QString tryFunctionCompletion(int line, QString expr); diff --git a/qrenderdoc/Code/pyrenderdoc/parse_reflection.py b/qrenderdoc/Code/pyrenderdoc/parse_reflection.py index 558823852..ed8d19d5c 100644 --- a/qrenderdoc/Code/pyrenderdoc/parse_reflection.py +++ b/qrenderdoc/Code/pyrenderdoc/parse_reflection.py @@ -2143,6 +2143,8 @@ class PyReflector: prefix_filter = node.id while curscope is not None: for k, v in curscope.identifiers.items(): + if k.startswith('_renderdoc'): + continue if any([x.line <= line for x in v]): ret.append(k) curscope = curscope.parent diff --git a/qrenderdoc/Windows/PythonShell.cpp b/qrenderdoc/Windows/PythonShell.cpp index 7e466ee9f..876cf420f 100644 --- a/qrenderdoc/Windows/PythonShell.cpp +++ b/qrenderdoc/Windows/PythonShell.cpp @@ -187,9 +187,10 @@ PythonShell::PythonShell(ICaptureContext &ctx, QWidget *parent) ui->setupUi(this); QObject::connect(ui->lineInput, &RDLineEdit::keyPress, this, &PythonShell::interactive_keypress); - QObject::connect(ui->helpSearch, &RDLineEdit::keyPress, this, &PythonShell::helpSearch_keypress); + QObject::connect(ui->helpSearch, &RDLineEdit::keyPress, this, &PythonShell::interactive_keypress); QObject::connect(ui->lineInput, &RDLineEdit::leave, [this]() { hideFunccompleteTooltip(); }); + QObject::connect(ui->helpSearch, &RDLineEdit::leave, [this]() { hideFunccompleteTooltip(); }); // we create this up front so its state stays persistent as much as possible. m_FindReplace = new FindReplace(m_Scintillas, this); @@ -217,6 +218,7 @@ PythonShell::PythonShell(ICaptureContext &ctx, QWidget *parent) m_FindReplace->setFindAllResultsDisplay(m_FindResults); ui->lineInput->setFont(Formatter::FixedFont()); + ui->helpSearch->setFont(Formatter::FixedFont()); ui->interactiveOutput->setFont(Formatter::FixedFont()); ui->scriptOutput->setFont(Formatter::FixedFont()); ui->helpText->setFont(Formatter::FixedFont()); @@ -226,6 +228,7 @@ PythonShell::PythonShell(ICaptureContext &ctx, QWidget *parent) ui->replGroup->setWindowTitle(tr("Interactive REPL")); ui->lineInput->setAcceptTabCharacters(true); + ui->helpSearch->setAcceptTabCharacters(true); // don't repeatedly re-parse for errors. Have a reasonable timeout m_SyntaxCheckTimer = new QTimer(this); @@ -282,13 +285,6 @@ PythonShell::PythonShell(ICaptureContext &ctx, QWidget *parent) completionContext = new PythonContext(); - // if we're help printing in the completion context, append it to the help text - QObject::connect(completionContext, &PythonContext::textOutput, - [this](const QString &, bool isStdError, const QString &output) { - if(m_HelpPrinting) - appendText(ui->helpText, output); - }); - QObject::connect(m_SyntaxCheckTimer, &QTimer::timeout, this, &PythonShell::doSyntaxCheck); QObject::connect(ui->interactiveOutput, &RDTextEdit::keyPress, [this](QKeyEvent *e) { @@ -313,7 +309,6 @@ PythonShell::PythonShell(ICaptureContext &ctx, QWidget *parent) m_InteractiveCompleter->popup()->setFont(Formatter::FixedFont()); m_InteractiveCompleter->popup()->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Preferred); m_InteractiveCompleter->popup()->setTextElideMode(Qt::ElideNone); - m_InteractiveCompleter->setWidget(ui->lineInput); m_InteractiveCompleter->setCompletionMode(QCompleter::UnfilteredPopupCompletion); m_InteractiveCompleter->setWrapAround(false); m_InteractiveCompletionModel = new QStringListModel(this); @@ -338,15 +333,18 @@ PythonShell::PythonShell(ICaptureContext &ctx, QWidget *parent) QObject::connect(m_InteractiveCompleter, OverloadedSlot::of(&QCompleter::activated), [this](const QModelIndex &idx) { + RDLineEdit *edit = qobject_cast(m_InteractiveCompleter->widget()); + if(!edit) + return; int i = idx.row(); if(i >= 0 && i < m_InteractiveCompletionModel->rowCount()) { - QString curText = ui->lineInput->text(); + QString curText = edit->text(); curText.resize(curText.size() - m_InteractiveCompletionPrefix); curText += m_InteractiveCompletionModel->stringList()[i]; - ui->lineInput->setText(curText); + edit->setText(curText); - ui->lineInput->setCursorPosition(curText.size()); + edit->setCursorPosition(curText.size()); } }); @@ -533,6 +531,15 @@ PythonShell::PythonShell(ICaptureContext &ctx, QWidget *parent) QObject::connect(PythonContext::GetExtensionContext(), &PythonContext::extensionLoaded, this, &PythonShell::extensionLoaded); + helpContext = newContext(); + + helpContext->makeHelpContext(); + + QObject::connect(helpContext, &PythonContext::textOutput, + [this](const QString &, bool isStdError, const QString &output) { + appendText(ui->helpText, output); + }); + m_Ctx.GetMainWindow()->RegisterShortcut("CTRL+S", this, [this](QWidget *) { this->on_saveScript_clicked(); }); @@ -566,6 +573,7 @@ PythonShell::~PythonShell() completionContext->Finish(); interactiveContext->Finish(); + helpContext->Finish(); delete ui; } @@ -1777,7 +1785,8 @@ bool PythonShell::saveEditor(EditorWrapper *editor, QString filename) void PythonShell::removeEditor(EditorWrapper *editor) { hideFunccompleteTooltip(); - m_Watcher->removePath(editor->filename()); + if(!editor->filename().isEmpty()) + m_Watcher->removePath(editor->filename()); m_Editors.removeOne(editor); m_Scintillas.removeOne(editor->scintilla()); updateEditorCloseButton(); @@ -2194,9 +2203,7 @@ void PythonShell::refreshCurrentHelp() ui->helpText->clear(); - m_HelpPrinting = true; - - completionContext->executeString(lit(R"( + helpContext->executeString(lit(R"( try: import keyword if keyword.iskeyword("%1"): @@ -2206,15 +2213,17 @@ try: except ImportError: help(%1) )") - .arg(ui->helpSearch->text())); + .arg(ui->helpSearch->text())); ui->helpText->verticalScrollBar()->setValue(0); - - m_HelpPrinting = false; } void PythonShell::interactive_keypress(QKeyEvent *event) { + RDLineEdit *edit = qobject_cast(QObject::sender()); + if(!edit) + return; + bool triggerCompletion = false; if(m_InteractiveCompleter->popup()->isVisible()) @@ -2224,12 +2233,13 @@ void PythonShell::interactive_keypress(QKeyEvent *event) // manually trigger a completion with tab case Qt::Key_Tab: // allow prefixed tabs to be inserted - if(ui->lineInput->text().trimmed().isEmpty()) + if(edit->text().trimmed().isEmpty()) { - ui->lineInput->insert(lit("\t")); + edit->insert(lit("\t")); } else { + m_InteractiveCompleter->setWidget(edit); m_InteractiveCompleter->activated( m_InteractiveCompleter->popup()->selectionModel()->currentIndex()); m_InteractiveCompleter->popup()->hide(); @@ -2259,7 +2269,7 @@ void PythonShell::interactive_keypress(QKeyEvent *event) if(triggerCompletion) { - QString base = ui->lineInput->text(); + QString base = edit->text(); QStringList completions; int oldCount = m_CompletionTipList.count(); @@ -2267,8 +2277,10 @@ void PythonShell::interactive_keypress(QKeyEvent *event) if(base.trimmed() != QString()) { - m_CompletionTipList = - interactiveContext->completionOptions(0, base, m_InteractiveCompletionPrefix); + PythonContext *ctx = interactiveContext; + if(edit == ui->helpSearch) + ctx = helpContext; + m_CompletionTipList = ctx->completionOptions(0, base, m_InteractiveCompletionPrefix); for(const QPair &item : m_CompletionTipList) completions << item.first; @@ -2280,7 +2292,7 @@ void PythonShell::interactive_keypress(QKeyEvent *event) if(completions.isEmpty()) { if(event->key() == Qt::Key_Tab) - ui->lineInput->insert(lit("\t")); + edit->insert(lit("\t")); m_InteractiveCompleter->popup()->hide(); QString prompt = interactiveContext->tryFunctionCompletion(0, base); @@ -2289,13 +2301,13 @@ void PythonShell::interactive_keypress(QKeyEvent *event) { m_ToolTip->configureTip(this, prompt); - QPoint p = ui->lineInput->fontMetrics().boundingRect(base).bottomRight(); - p.setY(ui->lineInput->geometry().height()); - p = ui->lineInput->mapToGlobal(p); + QPoint p = edit->fontMetrics().boundingRect(base).bottomRight(); + p.setY(edit->geometry().height()); + p = edit->mapToGlobal(p); if(!m_ToolTip->isVisible()) m_ToolTip->showTipAtPos(p); m_FuncTip = true; - m_FuncTipWidget = ui->lineInput; + m_FuncTipWidget = edit; } else { @@ -2309,8 +2321,8 @@ void PythonShell::interactive_keypress(QKeyEvent *event) m_InteractiveCompletionModel->setStringList(completions); - QRect r = ui->lineInput->rect(); - QFontMetrics fm = ui->lineInput->fontMetrics(); + QRect r = edit->rect(); + QFontMetrics fm = edit->fontMetrics(); #if(QT_VERSION < QT_VERSION_CHECK(5, 11, 0)) #define horizontalAdvance width @@ -2325,9 +2337,10 @@ void PythonShell::interactive_keypress(QKeyEvent *event) base.resize(base.size() - m_InteractiveCompletionPrefix); r.setLeft(r.left() + fm.horizontalAdvance(base)); - r.setWidth(longestWidth + ui->lineInput->style()->pixelMetric(QStyle::PM_ScrollBarExtent) + - ui->lineInput->style()->pixelMetric(QStyle::PM_ButtonMargin)); + r.setWidth(longestWidth + edit->style()->pixelMetric(QStyle::PM_ScrollBarExtent) + + edit->style()->pixelMetric(QStyle::PM_ButtonMargin)); + m_InteractiveCompleter->setWidget(edit); m_InteractiveCompleter->complete(r); m_InteractiveCompleter->popup()->selectionModel()->setCurrentIndex( m_InteractiveCompletionModel->index(0), QItemSelectionModel::ClearAndSelect); @@ -2337,45 +2350,46 @@ void PythonShell::interactive_keypress(QKeyEvent *event) if(event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) { - on_execute_clicked(); + if(edit == ui->lineInput) + on_execute_clicked(); + else if(edit == ui->helpSearch) + refreshCurrentHelp(); } - bool moved = false; - - if(event->key() == Qt::Key_Down && historyidx > -1) + // only line input has history + if(edit == ui->lineInput) { - historyidx--; + bool moved = false; - moved = true; + if(event->key() == Qt::Key_Down && historyidx > -1) + { + historyidx--; + + moved = true; + } + + QString workingtext; + + if(event->key() == Qt::Key_Up && historyidx + 1 < history.count()) + { + if(historyidx == -1) + workingtext = ui->lineInput->text(); + + historyidx++; + + moved = true; + } + + if(moved) + { + if(historyidx == -1) + ui->lineInput->setText(workingtext); + else + ui->lineInput->setText(history[historyidx]); + + ui->lineInput->deselect(); + } } - - QString workingtext; - - if(event->key() == Qt::Key_Up && historyidx + 1 < history.count()) - { - if(historyidx == -1) - workingtext = ui->lineInput->text(); - - historyidx++; - - moved = true; - } - - if(moved) - { - if(historyidx == -1) - ui->lineInput->setText(workingtext); - else - ui->lineInput->setText(history[historyidx]); - - ui->lineInput->deselect(); - } -} - -void PythonShell::helpSearch_keypress(QKeyEvent *e) -{ - if(e->key() == Qt::Key_Return || e->key() == Qt::Key_Enter) - refreshCurrentHelp(); } QString PythonShell::scriptHeader() diff --git a/qrenderdoc/Windows/PythonShell.h b/qrenderdoc/Windows/PythonShell.h index 3239e96bf..95893bbab 100644 --- a/qrenderdoc/Windows/PythonShell.h +++ b/qrenderdoc/Windows/PythonShell.h @@ -149,7 +149,6 @@ private slots: // manual slots void interactive_keypress(QKeyEvent *e); - void helpSearch_keypress(QKeyEvent *e); void traceLine(const QString &file, int line); void exception(const QString &extension, const QString &type, const QString &value, int finalLine, QList frames); @@ -176,7 +175,6 @@ private: QWidget *m_FuncTipWidget = NULL; intptr_t m_FuncTipLine = 0; bool m_ContextMenuVisible = false; - bool m_HelpPrinting = false; bool m_IgnoreRecovered = false; @@ -205,7 +203,8 @@ private: int m_CurLine = 0; QTimer *m_CurLineTimer = NULL; - PythonContext *interactiveContext = NULL, *scriptContext = NULL, *completionContext = NULL; + PythonContext *interactiveContext = NULL, *scriptContext = NULL, *completionContext = NULL, + *helpContext = NULL; QList history; int historyidx = -1;