From 88782bea9724ed7b161005f16b8813fde3c2a4aa Mon Sep 17 00:00:00 2001 From: baldurk Date: Wed, 5 Aug 2026 11:16:52 +0100 Subject: [PATCH] Use reflection script for autocomplete and help in python shell * Also add syntax checking via parse attempts. --- qrenderdoc/Code/pyrenderdoc/PythonContext.cpp | 284 +++++++----- qrenderdoc/Code/pyrenderdoc/PythonContext.h | 7 +- qrenderdoc/Windows/PythonShell.cpp | 413 ++++++++++++++++++ qrenderdoc/Windows/PythonShell.h | 24 +- 4 files changed, 622 insertions(+), 106 deletions(-) diff --git a/qrenderdoc/Code/pyrenderdoc/PythonContext.cpp b/qrenderdoc/Code/pyrenderdoc/PythonContext.cpp index c46f9edff..dc83f1971 100644 --- a/qrenderdoc/Code/pyrenderdoc/PythonContext.cpp +++ b/qrenderdoc/Code/pyrenderdoc/PythonContext.cpp @@ -148,6 +148,7 @@ PyObject *PythonContext::main_dict = NULL; PyObject *PythonContext::m_DebugPy = NULL; PyObject *PythonContext::m_CallWrapper = NULL; PyObject *PythonContext::m_Reflector = NULL; +QAtomicInt PythonContext::m_DeferredInit = 0; PyObject *PythonContext::m_CallWrapperGlobals = NULL; PythonContext *PythonContext::m_ExtensionContext = NULL; QMap PythonContext::extensions; @@ -516,19 +517,8 @@ void PythonContext::GlobalInit(PersistantConfig &config) // which then indicates they need to forward to a global object // import sys - PyDict_SetItemString(main_dict, "sys", PyImport_ImportModule("sys")); - - PyObject *rlcompleter = PyImport_ImportModule("rlcompleter"); - - if(rlcompleter) - { - // leak a reference so it stays loaded - } - else - { - // ignore a failed import - PyErr_Clear(); - } + PyObject *sysobj = PyImport_ImportModule("sys"); + PyDict_SetItemString(main_dict, "sys", sysobj); // try to import threading library to make debuggers happier if(!PyImport_ImportModule("threading")) @@ -537,9 +527,6 @@ void PythonContext::GlobalInit(PersistantConfig &config) PyErr_Clear(); } - // sysobj = sys - PyObject *sysobj = PyDict_GetItemString(main_dict, "sys"); - // sysobj.stdout = renderdoc_output_redirector() // sysobj.stderr = renderdoc_output_redirector() if(PyType_Ready(&OutputRedirectorType) >= 0) @@ -1003,6 +990,8 @@ except: qCritical() << "Couldn't find valid stubs path"; } + m_DeferredInit = 1; + PyGILState_Release(gil); }); @@ -1047,40 +1036,6 @@ PythonContext::PythonContext(bool extensionContext, QObject *parent) : QObject(p Py_DECREF(redirector); } - PyObject *rlcompleter = PyImport_ImportModule("rlcompleter"); - - if(rlcompleter) - { - PyObject *Completer = PyObject_SafeGetAttrString(rlcompleter, "Completer"); - - if(Completer) - { - // create a completer for our context's namespace - m_Completer = PyObject_CallFunction(Completer, "O", context_namespace); - - if(!m_Completer) - { - QString typeStr; - QString valueStr; - int finalLine = -1; - QList frames; - FetchException(typeStr, valueStr, finalLine, frames); - - // failure is not fatal - qWarning() << "Couldn't create completion object. " << typeStr << ": " << valueStr; - PyErr_Clear(); - } - - Py_DecRef(Completer); - } - - Py_DecRef(rlcompleter); - } - else - { - m_Completer = NULL; - } - // release the GIL again PyGILState_Release(gil); @@ -1766,74 +1721,195 @@ QWidget *PythonContext::QWidgetFromPy(PyObject *widget) #endif } -QStringList PythonContext::completionOptions(QString base) +void PythonContext::reflectSource(QString src) { - QStringList ret; + if(!m_Reflector) + { + for(int i = 0; i < 50 && m_DeferredInit == 0; i++) + QThread::msleep(20); + m_DeferredInit = 1; - if(!m_Completer) - return ret; - - QByteArray bytes = base.toUtf8(); - const char *input = (const char *)bytes.data(); + if(!m_Reflector) + return; + } PyGILState_STATE gil = PyGILState_Ensure(); - PyObject *completeFunction = PyObject_SafeGetAttrString(m_Completer, "complete"); + PyObject *refl = PyObject_CallFunction(m_Reflector, "sOO", (const char *)src.toUtf8().data(), + context_namespace, Py_False); - if(!completeFunction) + if(refl) + { + PyDict_SetItemString(context_namespace, "_renderdoc_refl", refl); + + Py_XDECREF(refl); + } + else + { + HandleException(NULL); + } + + PyGILState_Release(gil); +} + +QString PythonContext::tooltipForLoc(int line, int col) +{ + PyGILState_STATE gil = PyGILState_Ensure(); + + PyObject *refl = PyDict_GetItemString(context_namespace, "_renderdoc_refl"); + + if(!refl) + { + PyGILState_Release(gil); + return QString(); + } + + PyObject *tooltip = PyObject_CallMethod(refl, "get_location_tooltip", "ii", line, col); + + QString ret; + if(tooltip) + { + ret = ToQStr(tooltip); + + Py_XDECREF(tooltip); + } + else + { + HandleException(NULL); + } + + PyGILState_Release(gil); + + return ret; +} + +QString PythonContext::typenameForLoc(int line, int col) +{ + PyGILState_STATE gil = PyGILState_Ensure(); + + PyObject *refl = PyDict_GetItemString(context_namespace, "_renderdoc_refl"); + + if(!refl) + { + PyGILState_Release(gil); + return QString(); + } + + PyObject *typeObj = PyObject_CallMethod(refl, "get_location_type", "ii", line, col); + + if(typeObj) + { + PyObject *typing = PyImport_ImportModule("typing"); + PyObject *Any = PyObject_SafeGetAttrString(typing, "Any"); + + if(typeObj == Any) + { + Py_XDECREF(typeObj); + typeObj = NULL; + } + + Py_XDECREF(Any); + Py_XDECREF(typing); + } + else + { + HandleException(NULL); + } + + QString ret; + + if(typeObj) + { + PyObject *name = PyObject_CallMethod(refl, "get_name", "O", typeObj); + + if(name) + { + ret = ToQStr(name); + + Py_XDECREF(typeObj); + Py_XDECREF(name); + } + else + { + HandleException(NULL); + } + } + + PyGILState_Release(gil); + + return ret; +} + +QStringList PythonContext::completionOptions(int line, QString expr, int &prefix_len) +{ + QStringList ret; + + PyGILState_STATE gil = PyGILState_Ensure(); + + PyObject *refl = PyDict_GetItemString(context_namespace, "_renderdoc_refl"); + + if(!refl) + { + PyGILState_Release(gil); return ret; - - int idx = 0; - PyObject *opt = NULL; - do - { - opt = PyObject_CallFunction(completeFunction, "si", input, idx); - - if(opt && !Py_IsNone(opt)) - { - QString optstr = ToQStr(opt); - - bool add = true; - - // little hack, remove some of the ugly swig template instantiations that we can't avoid. - if(optstr.contains(lit("renderdoc.rdcarray")) || optstr.contains(lit("renderdoc.rdcstr")) || - optstr.contains(lit("renderdoc.bytebuf"))) - add = false; - - if(add) - ret << optstr; - } - - idx++; - } while(opt && !Py_IsNone(opt)); - - // extra hack, remove the swig object functions/data but ONLY if we find a sure-fire identifier - // (thisown) since otherwise we could remove append from a list object - bool containsSwigInternals = false; - for(const QString &optstr : ret) - { - if(optstr.contains(lit(".thisown"))) - { - containsSwigInternals = true; - break; - } } - if(containsSwigInternals) + PyObject *completions = PyObject_CallMethod(refl, "get_autocompletion", "is", line, + (const char *)expr.toUtf8().data()); + + prefix_len = 0; + + if(completions) { - for(int i = 0; i < ret.count();) + PyObject *comp_list = PyTuple_GetItem(completions, 0); + prefix_len = PyLong_AsLong(PyTuple_GetItem(completions, 1)); + + if(comp_list) { - if(ret[i].endsWith(lit(".acquire(")) || ret[i].endsWith(lit(".append(")) || - ret[i].endsWith(lit(".disown(")) || ret[i].endsWith(lit(".next(")) || - ret[i].endsWith(lit(".own(")) || ret[i].endsWith(lit(".this")) || - ret[i].endsWith(lit(".thisown"))) - ret.removeAt(i); - else - i++; + for(Py_ssize_t i = 0, len = PyList_Size(comp_list); i < len; i++) + { + ret << ToQStr(PyList_GetItem(comp_list, i)); + } } } + else + { + HandleException(NULL); + } - Py_DecRef(completeFunction); + Py_XDECREF(completions); + + PyGILState_Release(gil); + + return ret; +} + +QString PythonContext::tryFunctionCompletion(int line, QString expr) +{ + PyGILState_STATE gil = PyGILState_Ensure(); + + PyObject *refl = PyDict_GetItemString(context_namespace, "_renderdoc_refl"); + + if(!refl) + { + PyGILState_Release(gil); + return QString(); + } + + PyObject *funcComp = PyObject_CallMethod(refl, "get_funccompletion", "is", line, + (const char *)expr.toUtf8().data()); + + QString ret; + if(funcComp) + { + ret = ToQStr(PyTuple_GetItem(funcComp, 2)); + + Py_XDECREF(funcComp); + } + else + { + HandleException(NULL); + } PyGILState_Release(gil); diff --git a/qrenderdoc/Code/pyrenderdoc/PythonContext.h b/qrenderdoc/Code/pyrenderdoc/PythonContext.h index a735789ed..fdb1f3950 100644 --- a/qrenderdoc/Code/pyrenderdoc/PythonContext.h +++ b/qrenderdoc/Code/pyrenderdoc/PythonContext.h @@ -116,7 +116,11 @@ public: static PyObject *QWidgetToPy(QWidget *widget) { return QtObjectToPython("QWidget", widget); } static QWidget *QWidgetFromPy(PyObject *widget); - QStringList completionOptions(QString base); + void reflectSource(QString src); + QString tooltipForLoc(int line, int col); + QStringList completionOptions(int line, QString expr, int &prefix_len); + QString tryFunctionCompletion(int line, QString expr); + QString typenameForLoc(int line, int col); void FlushOutput() { outputTick(); } @@ -159,6 +163,7 @@ private: // the PyReflector from parse_reflection static PyObject *m_Reflector; + static QAtomicInt m_DeferredInit; // a statically created PythonContext for extension events/output. // each extension has its own dictionary but this is used so that users can connect to it and receieve events diff --git a/qrenderdoc/Windows/PythonShell.cpp b/qrenderdoc/Windows/PythonShell.cpp index a8f2d688f..4bae5bb86 100644 --- a/qrenderdoc/Windows/PythonShell.cpp +++ b/qrenderdoc/Windows/PythonShell.cpp @@ -1050,6 +1050,8 @@ PythonShell::PythonShell(ICaptureContext &ctx, QWidget *parent) QObject::connect(ui->lineInput, &RDLineEdit::keyPress, this, &PythonShell::interactive_keypress); QObject::connect(ui->helpSearch, &RDLineEdit::keyPress, this, &PythonShell::helpSearch_keypress); + QObject::connect(ui->lineInput, &RDLineEdit::leave, [this]() { hideFunccompleteTooltip(); }); + ui->lineInput->setFont(Formatter::FixedFont()); ui->interactiveOutput->setFont(Formatter::FixedFont()); ui->scriptOutput->setFont(Formatter::FixedFont()); @@ -1061,6 +1063,23 @@ PythonShell::PythonShell(ICaptureContext &ctx, QWidget *parent) ui->lineInput->setAcceptTabCharacters(true); + // don't repeatedly re-parse for errors. Have a reasonable timeout + m_SyntaxCheckTimer = new QTimer(this); + m_SyntaxCheckTimer->setSingleShot(true); + m_SyntaxCheckTimer->setInterval(1200); + + completionContext = new PythonContext(); + setGlobals(completionContext); + + // 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) { // ignore keypresses that aren't typing, but for up/down redirect that to the line input to get history if((e->text().isEmpty() || !e->text()[0].isPrint()) && e->key() != Qt::Key_Up && @@ -1070,6 +1089,36 @@ PythonShell::PythonShell(ICaptureContext &ctx, QWidget *parent) QApplication::postEvent(ui->lineInput, new QKeyEvent(*e)); }); + m_ToolTip = new RDToolTip(this); + + m_ToolTip->setFont(Formatter::FixedFont()); + + m_InteractiveCompleter = new QCompleter(this); + 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); + m_InteractiveCompleter->setModel(m_InteractiveCompletionModel); + m_InteractiveCompleter->setCompletionRole(Qt::DisplayRole); + + QObject::connect(m_InteractiveCompleter, + OverloadedSlot::of(&QCompleter::activated), + [this](const QModelIndex &idx) { + int i = idx.row(); + if(i >= 0 && i < m_InteractiveCompletionModel->rowCount()) + { + QString curText = ui->lineInput->text(); + curText.resize(curText.size() - m_InteractiveCompletionPrefix); + curText += m_InteractiveCompletionModel->stringList()[i]; + ui->lineInput->setText(curText); + + ui->lineInput->setCursorPosition(curText.size()); + } + }); + // reset output to default on_clear_clicked(); on_newScript_clicked(); @@ -1153,6 +1202,9 @@ PythonShell::~PythonShell() for(ScintillaEdit *edit : m_Scintillas) delete edit; + delete m_ToolTip; + + completionContext->Finish(); interactiveContext->Finish(); delete m_ThreadCtx; @@ -1160,6 +1212,31 @@ PythonShell::~PythonShell() delete ui; } +void PythonShell::doSyntaxCheck() +{ + ScintillaEdit *editor = curEditor(); + + if(!editor) + return; + + QByteArray script = editor->getText(editor->textLength() + 1); + PyParseError parseError = completionContext->CheckPyParse(script, "script.py"); + + if(parseError.lineno >= 0) + { + sptr_t end = editor->lineLength(parseError.lineno - 1); + sptr_t linePos = editor->positionFromLine(parseError.lineno - 1); + while(QChar(QLatin1Char(script[int(linePos + end - 1)])).isSpace()) + end--; + editor->setIndicatorCurrent(0); + editor->indicatorFillRange(linePos + parseError.offset - 1, end + 1 - parseError.offset); + + editor->annotationSetText(parseError.lineno - 1, parseError.errStr.c_str()); + editor->annotationSetVisible(ANNOTATION_BOXED); + editor->annotationSetStyle(parseError.lineno - 1, 100); + } +} + void PythonShell::editorTab_Changed(int index) { ScintillaEdit *editor = curEditor(); @@ -1228,6 +1305,11 @@ ScintillaEdit *PythonShell::makeEditor() updateEditorCloseButton(); }); + QObject::connect(editor, &ScintillaEdit::autoCompleteCancelled, + [this]() { m_SyntaxCheckTimer->start(); }); + QObject::connect(editor, &ScintillaEdit::autoCompleteSelection, + [this]() { m_SyntaxCheckTimer->start(); }); + QObject::connect(editor, &ScintillaEdit::modified, [this, editor](int type, int, int, int, const QByteArray &text, int, int, int) { if(type & (SC_MOD_INSERTTEXT | SC_MOD_DELETETEXT | SC_MOD_BEFOREINSERT | @@ -1243,9 +1325,113 @@ ScintillaEdit *PythonShell::makeEditor() editor->setIndicatorCurrent(0); editor->indicatorClearRange(0, editor->textLength()); editor->annotationClearAll(); + + // we'll reparse when this timer finishes (it will be re-started on every + // change, so only N ms after the last change + if(!editor->autoCActive() && (!m_FuncTip || !m_ToolTip->isVisible())) + m_SyntaxCheckTimer->start(); + else + m_SyntaxCheckTimer->stop(); + } + + if(type & (SC_MOD_INSERTTEXT | SC_MOD_DELETETEXT)) + { + if(!editor->autoCActive() || text.contains('\r') || text.contains('\n')) + { + completionContext->reflectSource( + QString::fromUtf8(editor->getText(editor->textLength() + 1))); + } + else if(editor->autoCActive()) + { + // delay updating the autocomplete so the current cursor position is updated + GUIInvoke::defer(this, [this, editor]() { + doAutocomplete(editor); + m_SyntaxCheckTimer->stop(); + }); + } } }); + QObject::connect(editor, &ScintillaEdit::dwellStart, [this, editor](int x, int y) { + if(editor->autoCActive()) + return; + + if(m_ToolTip->isVisible() && m_FuncTip) + return; + + if(!editor->geometry().contains(editor->mapFromGlobal(QCursor::pos()))) + return; + + sptr_t pos = editor->positionFromPointClose(x, y); + + if(pos == -1) + return; + + sptr_t line = editor->lineFromPosition(pos); + sptr_t col = pos - editor->positionFromLine(line); + + QString tooltip = completionContext->tooltipForLoc(line + 1, col); + + if(!tooltip.isEmpty()) + { + hideFunccompleteTooltip(); + + m_ToolTip->configureTip(this, tooltip); + m_ToolTip->showTipAtPos(QCursor::pos() + QPoint(5, 5)); + } + }); + + QObject::connect(editor, &ScintillaEdit::dwellEnd, [this, editor](int, int) { + if(editor->autoCActive()) + return; + + if(!m_FuncTip) + m_ToolTip->hideTip(); + }); + + QObject::connect(editor, &ScintillaEdit::charAdded, [this, editor](int ch) { + doAutocomplete(editor); + m_SyntaxCheckTimer->stop(); + }); + + QObject::connect(editor, &ScintillaEdit::buttonPressed, + [this, editor](QMouseEvent *ev) { hideFunccompleteTooltip(); }); + + QObject::connect(editor, &ScintillaEdit::keyPressed, [this, editor](QKeyEvent *ev) { + if(ev->key() == Qt::Key_Space && (ev->modifiers() & Qt::ControlModifier)) + { + doAutocomplete(editor); + m_SyntaxCheckTimer->stop(); + } + + if(m_ToolTip->isVisible() && m_FuncTip) + { + if(editor->lineFromPosition(editor->currentPos()) == m_FuncTipLine) + { + doFunccomplete(editor); + return; + } + + hideFunccompleteTooltip(); + } + + if(ev->key() == Qt::Key_F1) + { + sptr_t pos = editor->currentPos(); + + if(pos >= 0) + { + sptr_t line = editor->lineFromPosition(pos); + sptr_t col = pos - editor->positionFromLine(line); + + QString typeName = completionContext->typenameForLoc(line + 1, col); + + if(!typeName.isEmpty()) + selectedHelp(typeName); + } + } + }); + if(m_Scintillas.empty()) { ui->docking->addToolWindow(editor, ToolWindowManager::EmptySpace); @@ -1279,6 +1465,36 @@ void PythonShell::updateEditorCloseButton() } } +bool PythonShell::eventFilter(QObject *watched, QEvent *event) +{ + if(qobject_cast(watched)) + { + if(event->type() == QEvent::Leave) + { + if(!m_FuncTip) + { + m_ToolTip->hideTip(); + } + else if(m_FuncTip) + { + QRect geom = m_ToolTip->geometry(); + QPoint pos = QCursor::pos(); + QPoint pos2 = m_ToolTip->mapFromGlobal(QCursor::pos()); + if(!geom.contains(pos)) + { + hideFunccompleteTooltip(); + } + } + } + else if(event->type() == QEvent::KeyPress && ((QKeyEvent *)event)->key() == Qt::Key_Escape) + { + hideFunccompleteTooltip(); + } + } + + return QObject::eventFilter(watched, event); +} + QVariant PythonShell::persistData() { QVariantMap state = ui->docking->saveState(); @@ -1467,6 +1683,7 @@ void PythonShell::on_execute_clicked() if(command.trimmed().length() > 0) { interactiveContext->executeString(command); + interactiveContext->reflectSource(QString()); } appendText(ui->interactiveOutput, lit(">> ")); @@ -1484,6 +1701,7 @@ void PythonShell::on_clear_clicked() interactiveContext->Finish(); interactiveContext = newContext(); + interactiveContext->reflectSource(QString()); } void PythonShell::on_newScript_clicked() @@ -1714,10 +1932,23 @@ void PythonShell::editor_contextMenu(const QPoint &pos) if(!editor) return; + hideFunccompleteTooltip(); + + m_ContextMenuVisible = true; + QMenu contextMenu(this); QString typeName; + sptr_t scintillaPos = editor->positionFromPoint(pos.x(), pos.y()); + if(scintillaPos >= 0) + { + sptr_t line = editor->lineFromPosition(scintillaPos); + sptr_t col = scintillaPos - editor->positionFromLine(line); + + typeName = completionContext->typenameForLoc(line + 1, col); + } + bool valid = !typeName.isEmpty(); QAction help(valid ? tr("Help for '%1'").arg(typeName) : tr("Help"), this); @@ -1779,18 +2010,143 @@ void PythonShell::editor_contextMenu(const QPoint &pos) contextMenu.addAction(&selectAll); RDDialog::show(&contextMenu, editor->viewport()->mapToGlobal(pos)); + + m_ContextMenuVisible = false; } void PythonShell::selectedHelp(QString word) { + ui->helpSearch->setText(word); + + refreshCurrentHelp(); } void PythonShell::refreshCurrentHelp() { + ToolWindowManager::raiseToolWindow(ui->helpGroup); + + ui->helpText->clear(); + + m_HelpPrinting = true; + + completionContext->executeString(lit(R"( +try: + import keyword + if keyword.iskeyword("%1"): + help("%1") + else: + help(%1) +except ImportError: + help(%1) +)") + .arg(ui->helpSearch->text())); + + ui->helpText->verticalScrollBar()->setValue(0); + + m_HelpPrinting = false; } void PythonShell::interactive_keypress(QKeyEvent *event) { + bool triggerCompletion = false; + + if(m_InteractiveCompleter->popup()->isVisible()) + { + switch(event->key()) + { + // manually trigger a completion with tab + case Qt::Key_Tab: + m_InteractiveCompleter->activated( + m_InteractiveCompleter->popup()->selectionModel()->currentIndex()); + m_InteractiveCompleter->popup()->hide(); + return; + // if a completion is in progress ignore any events the completer will process + case Qt::Key_Return: + case Qt::Key_Enter: return; + // allow key scrolling + case Qt::Key_Up: + case Qt::Key_Down: + case Qt::Key_PageUp: + case Qt::Key_PageDown: + break; + // all other keys close the popup + default: triggerCompletion = true; + } + } + else + { + if(event->text() != QString() && event->text()[0].isPrint() && event->key() != Qt::Key_Return && + event->key() != Qt::Key_Enter) + triggerCompletion = true; + + if(event->key() == Qt::Key_Escape && m_FuncTip && m_ToolTip->isVisible()) + hideFunccompleteTooltip(); + } + + if(triggerCompletion) + { + QString base = ui->lineInput->text(); + + QStringList completions; + + if(base.trimmed() != QString()) + completions = interactiveContext->completionOptions(0, base, m_InteractiveCompletionPrefix); + + if(completions.isEmpty()) + { + if(event->key() == Qt::Key_Tab) + ui->lineInput->insert(lit("\t")); + m_InteractiveCompleter->popup()->hide(); + + QString prompt = interactiveContext->tryFunctionCompletion(0, base); + + if(!prompt.isEmpty()) + { + m_ToolTip->configureTip(this, prompt); + + QPoint p = ui->lineInput->fontMetrics().boundingRect(base).bottomRight(); + p.setY(ui->lineInput->geometry().height()); + p = ui->lineInput->mapToGlobal(p); + if(!m_ToolTip->isVisible()) + m_ToolTip->showTipAtPos(p); + m_FuncTip = true; + } + else + { + hideFunccompleteTooltip(); + } + + return; + } + + hideFunccompleteTooltip(); + + m_InteractiveCompletionModel->setStringList(completions); + + QRect r = ui->lineInput->rect(); + QFontMetrics fm = ui->lineInput->fontMetrics(); + +#if(QT_VERSION < QT_VERSION_CHECK(5, 11, 0)) +#define horizontalAdvance width +#endif + + int longestWidth = 0; + for(QString &c : completions) + { + longestWidth = qMax(longestWidth, fm.horizontalAdvance(c)); + } + + 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)); + + m_InteractiveCompleter->complete(r); + + return; + } + if(event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) { on_execute_clicked(); @@ -1868,6 +2224,63 @@ void PythonShell::enableButtons(bool enable) } } +void PythonShell::doAutocomplete(ScintillaEdit *editor) +{ + sptr_t pos = editor->currentPos(); + sptr_t line = editor->lineFromPosition(pos); + sptr_t lineStart = editor->positionFromLine(line); + QByteArray lineText = editor->getLine(line); + lineText.resize(pos - lineStart); + + int prefix_len = 0; + QStringList completions = + completionContext->completionOptions(line, QString::fromUtf8(lineText), prefix_len); + + if(completions.empty()) + return doFunccomplete(editor); + + hideFunccompleteTooltip(); + editor->autoCShow(prefix_len, completions.join(QLatin1Char(' ')).toUtf8().data()); +} + +void PythonShell::doFunccomplete(ScintillaEdit *editor) +{ + sptr_t pos = editor->currentPos(); + sptr_t line = editor->lineFromPosition(pos); + sptr_t lineStart = editor->positionFromLine(line); + QByteArray lineText = editor->getLine(line); + lineText.resize(pos - lineStart); + + QString prompt = completionContext->tryFunctionCompletion(line, QString::fromUtf8(lineText)); + + if(!prompt.isEmpty()) + { + m_ToolTip->configureTip(this, prompt); + + sptr_t tooltipPos = editor->positionFromLine(line + 1); + + QPoint p(editor->pointXFromPosition(tooltipPos), + editor->pointYFromPosition(lineStart + lineText.size()) + editor->textHeight(line)); + p = editor->mapToGlobal(p); + if(!m_ToolTip->isVisible()) + m_ToolTip->showTipAtPos(p); + m_FuncTip = true; + m_FuncTipLine = line; + } + else + { + hideFunccompleteTooltip(); + } +} + +void PythonShell::hideFunccompleteTooltip() +{ + m_ToolTip->hideTip(); + m_FuncTip = false; + // start the syntax check timer in case this naturally disappeared + m_SyntaxCheckTimer->start(); +} + PythonContext *PythonShell::newContext() { PythonContext *ret = new PythonContext(); diff --git a/qrenderdoc/Windows/PythonShell.h b/qrenderdoc/Windows/PythonShell.h index b6f970d42..149ca5765 100644 --- a/qrenderdoc/Windows/PythonShell.h +++ b/qrenderdoc/Windows/PythonShell.h @@ -32,6 +32,8 @@ class PythonContext; class QTextEdit; class RDToolTip; class QTimer; +class QCompleter; +class QStringListModel; namespace Ui { @@ -117,6 +119,7 @@ private slots: void extensionLoaded(const QString &extension); void editor_contextMenu(const QPoint &pos); void editorTab_Changed(int index); + void doSyntaxCheck(); private: Ui::PythonShell *ui; @@ -125,9 +128,21 @@ private: ScintillaEdit *runningScriptEditor = NULL; + RDToolTip *m_ToolTip; + bool m_FuncTip = false; + intptr_t m_FuncTipLine = 0; + bool m_ContextMenuVisible = false; + bool m_HelpPrinting = false; + + QTimer *m_SyntaxCheckTimer; + + QCompleter *m_InteractiveCompleter; + QStringListModel *m_InteractiveCompletionModel; + int m_InteractiveCompletionPrefix = 0; + static const int CURRENT_MARKER = 0; - PythonContext *interactiveContext = NULL, *scriptContext = NULL; + PythonContext *interactiveContext = NULL, *scriptContext = NULL, *completionContext = NULL; QList history; int historyidx = -1; @@ -150,6 +165,8 @@ private: ScintillaEdit *makeEditor(); void updateEditorCloseButton(); + bool eventFilter(QObject *watched, QEvent *event) override; + void updateScriptOutput(bool fullRefresh); PythonContext *newContext(); @@ -157,6 +174,11 @@ private: void runScript(bool debugging); + void doAutocomplete(ScintillaEdit *editor); + void doFunccomplete(ScintillaEdit *editor); + + void hideFunccompleteTooltip(); + void selectedHelp(QString word); void refreshCurrentHelp();