mirror of
https://github.com/baldurk/renderdoc.git
synced 2026-05-29 21:30:53 +00:00
Add simple tab-completion in interactive python shell
This commit is contained in:
@@ -242,6 +242,18 @@ void PythonContext::GlobalInit()
|
||||
// import sys
|
||||
PyDict_SetItemString(main_dict, "sys", PyImport_ImportModule("sys"));
|
||||
|
||||
PyObject *rlcompleter = PyImport_ImportModule("rlcompleter");
|
||||
|
||||
if(rlcompleter)
|
||||
{
|
||||
PyDict_SetItemString(main_dict, "rlcompleter", rlcompleter);
|
||||
}
|
||||
else
|
||||
{
|
||||
// ignore a failed import
|
||||
PyErr_Clear();
|
||||
}
|
||||
|
||||
// sysobj = sys
|
||||
PyObject *sysobj = PyDict_GetItemString(main_dict, "sys");
|
||||
|
||||
@@ -329,6 +341,8 @@ PythonContext::PythonContext(QObject *parent) : QObject(parent)
|
||||
// clone our own local context
|
||||
context_namespace = PyDict_Copy(main_dict);
|
||||
|
||||
PyObject *rlcompleter = PyDict_GetItemString(main_dict, "rlcompleter");
|
||||
|
||||
// for compatibility with earlier versions of python that took a char * instead of const char *
|
||||
char noparams[1] = "";
|
||||
|
||||
@@ -344,6 +358,40 @@ PythonContext::PythonContext(QObject *parent) : QObject(parent)
|
||||
Py_DECREF(redirector);
|
||||
}
|
||||
|
||||
if(rlcompleter)
|
||||
{
|
||||
PyObject *Completer = PyObject_GetAttrString(rlcompleter, "Completer");
|
||||
|
||||
if(Completer)
|
||||
{
|
||||
// create a completer for our context's namespace
|
||||
m_Completer = PyObject_CallFunction(Completer, "O", context_namespace);
|
||||
|
||||
if(m_Completer)
|
||||
{
|
||||
PyDict_SetItemString(context_namespace, "_renderdoc_completer", m_Completer);
|
||||
}
|
||||
else
|
||||
{
|
||||
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);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_Completer = NULL;
|
||||
}
|
||||
|
||||
// release the GIL again
|
||||
PyGILState_Release(gil);
|
||||
|
||||
@@ -358,6 +406,11 @@ PythonContext::PythonContext(QObject *parent) : QObject(parent)
|
||||
|
||||
PythonContext::~PythonContext()
|
||||
{
|
||||
PyGILState_STATE gil = PyGILState_Ensure();
|
||||
if(m_Completer)
|
||||
Py_DecRef(m_Completer);
|
||||
PyGILState_Release(gil);
|
||||
|
||||
// do a final tick to gather any remaining output
|
||||
outputTick();
|
||||
}
|
||||
@@ -577,6 +630,77 @@ QWidget *PythonContext::QWidgetFromPy(PyObject *widget)
|
||||
#endif
|
||||
}
|
||||
|
||||
QStringList PythonContext::completionOptions(QString base)
|
||||
{
|
||||
QStringList ret;
|
||||
|
||||
if(!m_Completer)
|
||||
return ret;
|
||||
|
||||
QByteArray bytes = base.toUtf8();
|
||||
const char *input = (const char *)bytes.data();
|
||||
|
||||
PyGILState_STATE gil = PyGILState_Ensure();
|
||||
|
||||
PyObject *completeFunction = PyObject_GetAttrString(m_Completer, "complete");
|
||||
|
||||
int idx = 0;
|
||||
PyObject *opt = NULL;
|
||||
do
|
||||
{
|
||||
opt = PyObject_CallFunction(completeFunction, "si", input, idx);
|
||||
|
||||
if(opt && opt != Py_None)
|
||||
{
|
||||
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 && opt != Py_None);
|
||||
|
||||
// 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)
|
||||
{
|
||||
for(int i = 0; i < ret.count();)
|
||||
{
|
||||
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++;
|
||||
}
|
||||
}
|
||||
|
||||
Py_DecRef(completeFunction);
|
||||
|
||||
PyGILState_Release(gil);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
PyObject *PythonContext::QtObjectToPython(const char *typeName, QObject *object)
|
||||
{
|
||||
#if PYSIDE2_ENABLED
|
||||
|
||||
@@ -87,6 +87,8 @@ public:
|
||||
static PyObject *QWidgetToPy(QWidget *widget) { return QtObjectToPython("QWidget", widget); }
|
||||
static QWidget *QWidgetFromPy(PyObject *widget);
|
||||
|
||||
QStringList completionOptions(QString base);
|
||||
|
||||
void setThreadBlocking(bool block) { m_Block = block; }
|
||||
bool threadBlocking() { return m_Block; }
|
||||
void abort() { m_Abort = true; }
|
||||
@@ -116,6 +118,9 @@ private:
|
||||
// globals are set into and any scripts execute in
|
||||
PyObject *context_namespace = NULL;
|
||||
|
||||
// a rlcompleter.Completer object used for tab-completion
|
||||
PyObject *m_Completer = NULL;
|
||||
|
||||
// this is set during an execute, so we can identify when a callback happens within our execute or
|
||||
// not
|
||||
PyThreadState *m_State = NULL;
|
||||
|
||||
@@ -458,6 +458,8 @@ PythonShell::PythonShell(ICaptureContext &ctx, QWidget *parent)
|
||||
scriptEditor->markerDefine(CURRENT_MARKER, SC_MARK_SHORTARROW);
|
||||
scriptEditor->markerDefine(CURRENT_MARKER + 1, SC_MARK_BACKGROUND);
|
||||
|
||||
scriptEditor->autoCSetMaxHeight(10);
|
||||
|
||||
ConfigureSyntax(scriptEditor, SCLEX_PYTHON);
|
||||
|
||||
scriptEditor->setTabWidth(4);
|
||||
@@ -476,6 +478,20 @@ PythonShell::PythonShell(ICaptureContext &ctx, QWidget *parent)
|
||||
}
|
||||
});
|
||||
|
||||
QObject::connect(scriptEditor, &ScintillaEdit::charAdded, [this](int ch) {
|
||||
if(ch == '.')
|
||||
{
|
||||
startAutocomplete();
|
||||
}
|
||||
});
|
||||
|
||||
QObject::connect(scriptEditor, &ScintillaEdit::keyPressed, [this](QKeyEvent *ev) {
|
||||
if(ev->key() == Qt::Key_Space && ev->modifiers() && Qt::ControlModifier)
|
||||
{
|
||||
startAutocomplete();
|
||||
}
|
||||
});
|
||||
|
||||
ui->scriptSplitter->insertWidget(0, scriptEditor);
|
||||
int w = ui->scriptSplitter->rect().width();
|
||||
ui->scriptSplitter->setSizes({w * 2 / 3, w / 3});
|
||||
@@ -693,6 +709,79 @@ void PythonShell::textOutput(bool isStdError, const QString &output)
|
||||
|
||||
void PythonShell::interactive_keypress(QKeyEvent *event)
|
||||
{
|
||||
if(event->key() == Qt::Key_Tab)
|
||||
{
|
||||
QString base = ui->lineInput->text();
|
||||
if(!base.isEmpty() && !base.rbegin()->isSpace())
|
||||
{
|
||||
// search backwards from the end for the first non dotted identifier first, and extract that
|
||||
// substring. This is just ASCII, not unicode
|
||||
for(int i = base.count() - 1; i >= 0; i--)
|
||||
{
|
||||
if(!base[i].isLetterOrNumber() && base[i] != QLatin1Char('.') && base[i] != QLatin1Char('_'))
|
||||
{
|
||||
base = base.right(base.count() - 1 - i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// skip any initial digits that got included in the coarse search above
|
||||
while(!base.isEmpty() && base[0].isDigit())
|
||||
base.remove(0, 1);
|
||||
|
||||
QStringList options = interactiveContext->completionOptions(base);
|
||||
|
||||
QString line = ui->lineInput->text();
|
||||
|
||||
if(!options.isEmpty())
|
||||
{
|
||||
QString commonSubstring = options[0];
|
||||
|
||||
for(int i = 1; i < options.count(); i++)
|
||||
{
|
||||
const QString &opt = options[i];
|
||||
if(opt.count() < commonSubstring.count())
|
||||
commonSubstring.truncate(opt.count());
|
||||
|
||||
for(int j = 0; j < commonSubstring.count(); j++)
|
||||
{
|
||||
if(commonSubstring[j] != opt[j])
|
||||
{
|
||||
commonSubstring.truncate(j);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(commonSubstring.length() > base.length())
|
||||
{
|
||||
line.chop(base.length());
|
||||
line += commonSubstring;
|
||||
ui->lineInput->setText(line);
|
||||
}
|
||||
|
||||
if(options.count() > 1)
|
||||
{
|
||||
QString text;
|
||||
text += line;
|
||||
text += lit("\n");
|
||||
for(const QString &opt : options)
|
||||
{
|
||||
text += opt;
|
||||
text += lit("\n");
|
||||
}
|
||||
text += m_storedLines.isEmpty() ? lit(">> ") : lit(".. ");
|
||||
appendText(ui->interactiveOutput, text);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
ui->lineInput->insert(lit("\t"));
|
||||
return;
|
||||
}
|
||||
|
||||
if(event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter)
|
||||
on_execute_clicked();
|
||||
|
||||
@@ -756,6 +845,65 @@ void PythonShell::enableButtons(bool enable)
|
||||
ui->abortRun->setEnabled(!enable);
|
||||
}
|
||||
|
||||
void PythonShell::startAutocomplete()
|
||||
{
|
||||
sptr_t pos = scriptEditor->currentPos();
|
||||
sptr_t line = scriptEditor->lineFromPosition(pos);
|
||||
sptr_t lineStart = scriptEditor->positionFromLine(line);
|
||||
QByteArray lineText = scriptEditor->getLine(line);
|
||||
|
||||
sptr_t end = pos - lineStart - 1;
|
||||
sptr_t start;
|
||||
for(start = end; start >= 0; start--)
|
||||
{
|
||||
if(QChar::fromLatin1(lineText[start]).isLetterOrNumber() || lineText[start] == '.' ||
|
||||
lineText[start] == '_')
|
||||
continue;
|
||||
|
||||
start++;
|
||||
break;
|
||||
}
|
||||
|
||||
QString comp = QString::fromUtf8(lineText.mid(start, end - start + 1));
|
||||
|
||||
PythonContext *context = new PythonContext();
|
||||
|
||||
setGlobals(context);
|
||||
|
||||
// super hack. Try to import any modules to get completion suggestions from them.
|
||||
// we only process imports with no indentation since they should be unconditional. We ignore
|
||||
// imports that fail.
|
||||
QByteArray text = scriptEditor->getText(pos + 1);
|
||||
|
||||
for(int offs = 0; offs < text.length();)
|
||||
{
|
||||
// find the next newline (may be NULL if we're at the end)
|
||||
int newline = text.indexOf('\n', offs);
|
||||
|
||||
// execute the import if there is one
|
||||
const char *c = text.data() + offs;
|
||||
if(!strncmp(c, "import ", 7))
|
||||
{
|
||||
context->executeString(newline >= 0 ? QString::fromUtf8(c, newline - offs + 1)
|
||||
: QString::fromUtf8(c));
|
||||
}
|
||||
|
||||
if(newline < 0)
|
||||
break;
|
||||
|
||||
// move to the next line
|
||||
offs = newline + 1;
|
||||
}
|
||||
|
||||
QStringList completions = context->completionOptions(comp);
|
||||
|
||||
context->Finish();
|
||||
|
||||
qDebug() << "auto complete from" << comp;
|
||||
|
||||
scriptEditor->autoCShow(comp.count(), completions.join(QLatin1Char(' ')).toUtf8().data());
|
||||
}
|
||||
|
||||
PythonContext *PythonShell::newContext()
|
||||
{
|
||||
PythonContext *ret = new PythonContext();
|
||||
@@ -764,7 +912,12 @@ PythonContext *PythonShell::newContext()
|
||||
QObject::connect(ret, &PythonContext::exception, this, &PythonShell::exception);
|
||||
QObject::connect(ret, &PythonContext::textOutput, this, &PythonShell::textOutput);
|
||||
|
||||
ret->setGlobal("pyrenderdoc", (ICaptureContext *)m_ThreadCtx);
|
||||
setGlobals(ret);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
void PythonShell::setGlobals(PythonContext *ret)
|
||||
{
|
||||
ret->setGlobal("pyrenderdoc", (ICaptureContext *)m_ThreadCtx);
|
||||
}
|
||||
|
||||
@@ -82,6 +82,9 @@ private:
|
||||
QString m_storedLines;
|
||||
|
||||
PythonContext *newContext();
|
||||
void setGlobals(PythonContext *ret);
|
||||
|
||||
void startAutocomplete();
|
||||
|
||||
QString scriptHeader();
|
||||
void appendText(QTextEdit *output, const QString &text);
|
||||
|
||||
Reference in New Issue
Block a user