diff --git a/qrenderdoc/Code/pyrenderdoc/PythonContext.cpp b/qrenderdoc/Code/pyrenderdoc/PythonContext.cpp index b71fd9b70..3f73beb50 100644 --- a/qrenderdoc/Code/pyrenderdoc/PythonContext.cpp +++ b/qrenderdoc/Code/pyrenderdoc/PythonContext.cpp @@ -2275,6 +2275,64 @@ void PythonContext::LaunchDebugger(QWidget *window, PersistantConfig &config, QS }); } +PyParseError PythonContext::CheckPyParse(const QByteArray &script, const rdcstr &scriptNameForErrors) +{ + PyParseError parseError; + + PyGILState_STATE gil = PyGILState_Ensure(); + + PyObject *ast = PyImport_ImportModule("ast"); + + PyObject *scriptText = PyUnicode_FromStringAndSize(script.data(), script.size()); + PyObject *scriptName = PyUnicode_FromString(scriptNameForErrors.c_str()); + + PyObject *parseResult = PyObject_CallMethod(ast, "parse", "OO", scriptText, scriptName); + + if(!parseResult) + { + PyObject *exObj = NULL, *valueObj = NULL, *tracebackObj = NULL; + + PyErr_Fetch(&exObj, &valueObj, &tracebackObj); + PyErr_NormalizeException(&exObj, &valueObj, &tracebackObj); + + { + PyObject *a; + + PyObject_GetOptionalAttrString(valueObj, "lineno", &a); + parseError.lineno = PyLong_AsInt(a); + Py_XDECREF(a); + + PyObject_GetOptionalAttrString(valueObj, "offset", &a); + parseError.offset = PyLong_AsInt(a); + Py_XDECREF(a); + + PyObject *repr = PyObject_Str(valueObj); + PyObject *utf8 = PyUnicode_AsUTF8String(repr); + parseError.errStr = PyBytes_AsString(utf8); + for(int i = 1; i < parseError.offset - 1; i++) + parseError.errStr.insert(0, ' '); + parseError.errStr.insert(parseError.offset - 2, "^\n"); + + Py_XDECREF(repr); + Py_XDECREF(utf8); + } + + Py_XDECREF(exObj); + Py_XDECREF(valueObj); + Py_XDECREF(tracebackObj); + } + + Py_XDECREF(scriptText); + Py_XDECREF(scriptName); + Py_XDECREF(parseResult); + + Py_XDECREF(ast); + + PyGILState_Release(gil); + + return parseError; +} + extern "C" PyThreadState *GetExecutingThreadState(PyObject *global_handle) { OutputRedirector *redirector = (OutputRedirector *)global_handle; diff --git a/qrenderdoc/Code/pyrenderdoc/PythonContext.h b/qrenderdoc/Code/pyrenderdoc/PythonContext.h index d1878fd6e..deb0e06da 100644 --- a/qrenderdoc/Code/pyrenderdoc/PythonContext.h +++ b/qrenderdoc/Code/pyrenderdoc/PythonContext.h @@ -37,6 +37,12 @@ typedef struct _object PyObject; typedef struct _frame PyFrameObject; typedef struct _ts PyThreadState; +struct PyParseError +{ + int lineno = -1, offset = -1; + rdcstr errStr; +}; + class PythonContext : public QObject { private: @@ -77,6 +83,8 @@ public: static bool WaitForDebugger(); static void LaunchDebugger(QWidget *window, PersistantConfig &config, QString context_location); + PyParseError CheckPyParse(const QByteArray &script, const rdcstr &scriptNameForErrors); + bool CheckInterfaces(rdcstr &log); QString versionString(); diff --git a/qrenderdoc/Windows/PythonShell.cpp b/qrenderdoc/Windows/PythonShell.cpp index c3d8ba0cb..25d5074a7 100644 --- a/qrenderdoc/Windows/PythonShell.cpp +++ b/qrenderdoc/Windows/PythonShell.cpp @@ -952,7 +952,42 @@ PythonShell::PythonShell(ICaptureContext &ctx, QWidget *parent) scriptEditor = new ScintillaEdit(this); + // don't repeatedly re-parse for errors. Have a reasonable timeout + QTimer *checkTimer = new QTimer(this); + checkTimer->setSingleShot(true); + checkTimer->setInterval(1200); + + QObject::connect(checkTimer, &QTimer::timeout, [this]() { + PythonContext *context = new PythonContext(); + + setGlobals(context); + + QByteArray script = scriptEditor->getText(scriptEditor->textLength() + 1); + PyParseError parseError = context->CheckPyParse(script, "script.py"); + + if(parseError.lineno >= 0) + { + sptr_t end = scriptEditor->lineLength(parseError.lineno - 1); + sptr_t linePos = scriptEditor->positionFromLine(parseError.lineno - 1); + while(QChar(QLatin1Char(script[int(linePos + end - 1)])).isSpace()) + end--; + scriptEditor->setIndicatorCurrent(0); + scriptEditor->indicatorFillRange(linePos + parseError.offset - 1, end + 1 - parseError.offset); + + scriptEditor->annotationSetText(parseError.lineno - 1, parseError.errStr.c_str()); + scriptEditor->annotationSetVisible(ANNOTATION_BOXED); + scriptEditor->annotationSetStyle(parseError.lineno - 1, 100); + } + + context->Finish(); + }); + + scriptEditor->indicSetFore(0, 0x0000ff); + scriptEditor->styleSetFont(STYLE_DEFAULT, Formatter::FixedFont().family().toUtf8().data()); + scriptEditor->styleSetFont(100, Formatter::FixedFont().family().toUtf8().data()); + scriptEditor->styleSetBack( + 100, IsDarkTheme() ? SCINTILLA_COLOUR(175, 70, 70) : SCINTILLA_COLOUR(255, 150, 150)); scriptEditor->setMarginLeft(4.0); scriptEditor->setMarginWidthN(0, 32.0); @@ -992,12 +1027,21 @@ PythonShell::PythonShell(ICaptureContext &ctx, QWidget *parent) }); QObject::connect(scriptEditor, &ScintillaEdit::modified, - [this](int type, int, int, int, const QByteArray &, int, int, int) { + [this, checkTimer](int type, int, int, int, const QByteArray &, int, int, int) { if(type & (SC_MOD_INSERTTEXT | SC_MOD_DELETETEXT | SC_MOD_BEFOREINSERT | SC_MOD_BEFOREDELETE)) { scriptEditor->markerDeleteAll(CURRENT_MARKER); scriptEditor->markerDeleteAll(CURRENT_MARKER + 1); + + // always remove errors immediately + scriptEditor->setIndicatorCurrent(0); + scriptEditor->indicatorClearRange(0, scriptEditor->textLength() + 1); + scriptEditor->annotationClearAll(); + + // we'll reparse when this timer finishes (it will be re-started on every + // change, so only N ms after the last change + checkTimer->start(); } });