diff --git a/python/core/auto_additions/qgis.py b/python/core/auto_additions/qgis.py index 157dedd84f0..d448ce12080 100644 --- a/python/core/auto_additions/qgis.py +++ b/python/core/auto_additions/qgis.py @@ -2590,7 +2590,8 @@ Qgis.ScriptLanguage.__doc__ = 'Scripting languages.\n\n.. versionadded:: 3.30\n\ Qgis.ScriptLanguage.baseClass = Qgis # monkey patching scoped based enum Qgis.ScriptLanguageCapability.Reformat.__doc__ = "Language supports automatic code reformatting" -Qgis.ScriptLanguageCapability.__doc__ = 'Script language capabilities.\n\nThe flags reflect the support capabilities of a scripting language.\n\n.. versionadded:: 3.32\n\n' + '* ``Reformat``: ' + Qgis.ScriptLanguageCapability.Reformat.__doc__ +Qgis.ScriptLanguageCapability.CheckSyntax.__doc__ = "Language supports syntax checking" +Qgis.ScriptLanguageCapability.__doc__ = 'Script language capabilities.\n\nThe flags reflect the support capabilities of a scripting language.\n\n.. versionadded:: 3.32\n\n' + '* ``Reformat``: ' + Qgis.ScriptLanguageCapability.Reformat.__doc__ + '\n' + '* ``CheckSyntax``: ' + Qgis.ScriptLanguageCapability.CheckSyntax.__doc__ # -- Qgis.ScriptLanguageCapability.baseClass = Qgis Qgis.ScriptLanguageCapabilities.baseClass = Qgis diff --git a/python/core/auto_generated/qgis.sip.in b/python/core/auto_generated/qgis.sip.in index 1db6d260617..45faf08d76b 100644 --- a/python/core/auto_generated/qgis.sip.in +++ b/python/core/auto_generated/qgis.sip.in @@ -1592,6 +1592,7 @@ The development version enum class ScriptLanguageCapability { Reformat, + CheckSyntax, }; typedef QFlags ScriptLanguageCapabilities; diff --git a/python/gui/auto_generated/codeeditors/qgscodeeditor.sip.in b/python/gui/auto_generated/codeeditors/qgscodeeditor.sip.in index ea2f39e083a..11f98dc7c74 100644 --- a/python/gui/auto_generated/codeeditors/qgscodeeditor.sip.in +++ b/python/gui/auto_generated/codeeditors/qgscodeeditor.sip.in @@ -427,6 +427,15 @@ Applies code reformatting to the editor. This is only supported for editors which return the Qgis.ScriptLanguageCapability.Reformat capability from :py:func:`~QgsCodeEditor.languageCapabilities`. +.. versionadded:: 3.32 +%End + + virtual bool checkSyntax(); +%Docstring +Applies syntax checking to the editor. + +This is only supported for editors which return the Qgis.ScriptLanguageCapability.CheckSyntax capability from :py:func:`~QgsCodeEditor.languageCapabilities`. + .. versionadded:: 3.32 %End diff --git a/python/gui/auto_generated/codeeditors/qgscodeeditorpython.sip.in b/python/gui/auto_generated/codeeditors/qgscodeeditorpython.sip.in index 4e374582087..b46c156018a 100644 --- a/python/gui/auto_generated/codeeditors/qgscodeeditorpython.sip.in +++ b/python/gui/auto_generated/codeeditors/qgscodeeditorpython.sip.in @@ -85,6 +85,9 @@ Updates the editor capabilities. .. versionadded:: 3.32 %End + virtual bool checkSyntax(); + + public slots: void searchSelectedTextInPyQGISDocs(); diff --git a/src/core/qgis.h b/src/core/qgis.h index 33a47535427..e7819af07fc 100644 --- a/src/core/qgis.h +++ b/src/core/qgis.h @@ -2735,6 +2735,7 @@ class CORE_EXPORT Qgis enum class ScriptLanguageCapability : int { Reformat = 1 << 0, //!< Language supports automatic code reformatting + CheckSyntax = 1 << 1, //!< Language supports syntax checking }; Q_ENUM( ScriptLanguageCapability ) diff --git a/src/gui/codeeditors/qgscodeeditor.cpp b/src/gui/codeeditors/qgscodeeditor.cpp index 5b1d3be211b..73aca486308 100644 --- a/src/gui/codeeditors/qgscodeeditor.cpp +++ b/src/gui/codeeditors/qgscodeeditor.cpp @@ -224,16 +224,29 @@ void QgsCodeEditor::contextMenuEvent( QContextMenuEvent *event ) QMenu *menu = createStandardContextMenu(); menu->setAttribute( Qt::WA_DeleteOnClose ); - if ( languageCapabilities() & Qgis::ScriptLanguageCapability::Reformat ) + if ( ( languageCapabilities() & Qgis::ScriptLanguageCapability::Reformat ) || + ( languageCapabilities() & Qgis::ScriptLanguageCapability::CheckSyntax ) ) { menu->addSeparator(); + } + if ( languageCapabilities() & Qgis::ScriptLanguageCapability::Reformat ) + { QAction *reformatAction = new QAction( tr( "Reformat Code" ), menu ); + reformatAction->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "console/iconFormatCode.svg" ) ) ); reformatAction->setEnabled( !isReadOnly() ); connect( reformatAction, &QAction::triggered, this, &QgsCodeEditor::reformatCode ); menu->addAction( reformatAction ); } + if ( languageCapabilities() & Qgis::ScriptLanguageCapability::Reformat ) + { + QAction *syntaxCheckAction = new QAction( tr( "Check Syntax" ), menu ); + syntaxCheckAction->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "console/iconSyntaxErrorConsole.svg" ) ) ); + connect( syntaxCheckAction, &QAction::triggered, this, &QgsCodeEditor::checkSyntax ); + menu->addAction( syntaxCheckAction ); + } + populateContextMenu( menu ); menu->exec( mapToGlobal( event->pos() ) ); @@ -709,6 +722,11 @@ void QgsCodeEditor::reformatCode() endUndoAction(); } +bool QgsCodeEditor::checkSyntax() +{ + return true; +} + QStringList QgsCodeEditor::history() const { return mHistory; diff --git a/src/gui/codeeditors/qgscodeeditor.h b/src/gui/codeeditors/qgscodeeditor.h index 2b314b49082..27a0d06c7f3 100644 --- a/src/gui/codeeditors/qgscodeeditor.h +++ b/src/gui/codeeditors/qgscodeeditor.h @@ -446,6 +446,15 @@ class GUI_EXPORT QgsCodeEditor : public QsciScintilla */ void reformatCode(); + /** + * Applies syntax checking to the editor. + * + * This is only supported for editors which return the Qgis::ScriptLanguageCapability::CheckSyntax capability from languageCapabilities(). + * + * \since QGIS 3.32 + */ + virtual bool checkSyntax(); + signals: /** diff --git a/src/gui/codeeditors/qgscodeeditorpython.cpp b/src/gui/codeeditors/qgscodeeditorpython.cpp index 8fb40162a54..a56c3dd7204 100644 --- a/src/gui/codeeditors/qgscodeeditorpython.cpp +++ b/src/gui/codeeditors/qgscodeeditorpython.cpp @@ -374,13 +374,13 @@ QString QgsCodeEditorPython::reformatCodeString( const QString &string ) if ( settings.value( "pythonConsole/sortImports", true ).toBool() ) { const QString defineSortImports = QStringLiteral( - "def __qgis_sort_imports(str):\n" + "def __qgis_sort_imports(script):\n" " try:\n" " import isort\n" " except ImportError:\n" " return '_ImportError'\n" " options={'line_length': %1, 'profile': '%2', 'known_first_party': ['qgis', 'console', 'processing', 'plugins']}\n" - " return isort.code(str, **options)\n" ) + " return isort.code(script, **options)\n" ) .arg( maxLineLength ) .arg( formatter == QLatin1String( "black" ) ? QStringLiteral( "black" ) : QString() ); @@ -415,13 +415,13 @@ QString QgsCodeEditorPython::reformatCodeString( const QString &string ) const int level = settings.value( QStringLiteral( "pythonConsole/autopep8Level" ), 1 ).toInt(); const QString defineReformat = QStringLiteral( - "def __qgis_reformat(str):\n" + "def __qgis_reformat(script):\n" " try:\n" " import autopep8\n" " except ImportError:\n" " return '_ImportError'\n" " options={'aggressive': %1, 'max_line_length': %2}\n" - " return autopep8.fix_code(str, options=options)\n" ) + " return autopep8.fix_code(script, options=options)\n" ) .arg( level ) .arg( maxLineLength ); @@ -455,13 +455,13 @@ QString QgsCodeEditorPython::reformatCodeString( const QString &string ) const bool normalize = settings.value( QStringLiteral( "pythonConsole/blackNormalizeQuotes" ), true ).toBool(); const QString defineReformat = QStringLiteral( - "def __qgis_reformat(str):\n" + "def __qgis_reformat(script):\n" " try:\n" " import black\n" " except ImportError:\n" " return '_ImportError'\n" " options={'string_normalization': %1, 'line_length': %2}\n" - " return black.format_str(str, mode=black.Mode(**options))\n" ) + " return black.format_str(script, mode=black.Mode(**options))\n" ) .arg( QgsProcessingUtils::variantToPythonLiteral( normalize ) ) .arg( maxLineLength ); @@ -621,12 +621,72 @@ void QgsCodeEditorPython::updateCapabilities() if ( !QgsPythonRunner::isValid() ) return; + mCapabilities |= Qgis::ScriptLanguageCapability::CheckSyntax; + // we could potentially check for autopep8/black import here and reflect the capabilty accordingly. // (current approach is to to always indicate this capability and raise a user-friendly warning // when attempting to reformat if the libraries can't be imported) mCapabilities |= Qgis::ScriptLanguageCapability::Reformat; } +bool QgsCodeEditorPython::checkSyntax() +{ + clearWarnings(); + + if ( !QgsPythonRunner::isValid() ) + { + return true; + } + + const QString originalText = text(); + + const QString defineCheckSyntax = QStringLiteral( + "def __check_syntax(script):\n" + " try:\n" + " compile(script.encode('utf-8'), '', 'exec')\n" + " except SyntaxError as detail:\n" + " eline = detail.lineno or 1\n" + " eline -= 1\n" + " ecolumn = detail.offset or 1\n" + " edescr = detail.msg\n" + " return '!!!!'.join([str(eline), str(ecolumn), edescr])\n" + " return ''" ); + + if ( !QgsPythonRunner::run( defineCheckSyntax ) ) + { + QgsDebugMsg( QStringLiteral( "Error running script: %1" ).arg( defineCheckSyntax ) ); + return true; + } + + const QString script = QStringLiteral( "__check_syntax(%1)" ).arg( QgsProcessingUtils::stringToPythonLiteral( originalText ) ); + QString result; + if ( QgsPythonRunner::eval( script, result ) ) + { + if ( result.size() == 0 ) + { + return true; + } + else + { + const QStringList parts = result.split( QStringLiteral( "!!!!" ) ); + if ( parts.size() == 3 ) + { + const int line = parts.at( 0 ).toInt(); + const int column = parts.at( 1 ).toInt(); + addWarning( line, parts.at( 2 ) ); + setCursorPosition( line, column - 1 ); + ensureLineVisible( line ); + } + return false; + } + } + else + { + QgsDebugMsg( QStringLiteral( "Error running script: %1" ).arg( script ) ); + return true; + } +} + void QgsCodeEditorPython::searchSelectedTextInPyQGISDocs() { if ( !hasSelectedText() ) diff --git a/src/gui/codeeditors/qgscodeeditorpython.h b/src/gui/codeeditors/qgscodeeditorpython.h index e931fdbcef1..45dc1bcce43 100644 --- a/src/gui/codeeditors/qgscodeeditorpython.h +++ b/src/gui/codeeditors/qgscodeeditorpython.h @@ -104,6 +104,8 @@ class GUI_EXPORT QgsCodeEditorPython : public QgsCodeEditor */ void updateCapabilities(); + bool checkSyntax() override; + public slots: /**