Add simple tab-completion in interactive python shell

This commit is contained in:
baldurk
2017-12-25 13:21:05 +00:00
parent 16f64a5ace
commit a7398525b2
4 changed files with 286 additions and 1 deletions
@@ -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;
+154 -1
View File
@@ -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);
}
+3
View File
@@ -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);