mirror of
https://github.com/baldurk/renderdoc.git
synced 2026-08-23 23:16:31 +00:00
Use reflection script for autocomplete and help in python shell
* Also add syntax checking via parse attempts.
This commit is contained in:
@@ -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<rdcstr, PyObject *> 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<QString> 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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<const QModelIndex &>::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<ScintillaEdit *>(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();
|
||||
|
||||
@@ -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<QString> 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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user