Skip to content

Commit

Permalink
Move syntax checking capability to QgsCodeEditorPython
Browse files Browse the repository at this point in the history
  • Loading branch information
nyalldawson committed Mar 20, 2023
1 parent ac5f8da commit 3312bf1
Show file tree
Hide file tree
Showing 9 changed files with 112 additions and 8 deletions.
3 changes: 2 additions & 1 deletion python/core/auto_additions/qgis.py
Expand Up @@ -2590,7 +2590,8 @@
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
Expand Down
1 change: 1 addition & 0 deletions python/core/auto_generated/qgis.sip.in
Expand Up @@ -1592,6 +1592,7 @@ The development version
enum class ScriptLanguageCapability
{
Reformat,
CheckSyntax,
};

typedef QFlags<Qgis::ScriptLanguageCapability> ScriptLanguageCapabilities;
Expand Down
9 changes: 9 additions & 0 deletions python/gui/auto_generated/codeeditors/qgscodeeditor.sip.in
Expand Up @@ -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

Expand Down
Expand Up @@ -85,6 +85,9 @@ Updates the editor capabilities.
.. versionadded:: 3.32
%End

virtual bool checkSyntax();


public slots:

void searchSelectedTextInPyQGISDocs();
Expand Down
1 change: 1 addition & 0 deletions src/core/qgis.h
Expand Up @@ -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 )

Expand Down
20 changes: 19 additions & 1 deletion src/gui/codeeditors/qgscodeeditor.cpp
Expand Up @@ -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() ) );
Expand Down Expand Up @@ -709,6 +722,11 @@ void QgsCodeEditor::reformatCode()
endUndoAction();
}

bool QgsCodeEditor::checkSyntax()
{
return true;
}

QStringList QgsCodeEditor::history() const
{
return mHistory;
Expand Down
9 changes: 9 additions & 0 deletions src/gui/codeeditors/qgscodeeditor.h
Expand Up @@ -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:

/**
Expand Down
72 changes: 66 additions & 6 deletions src/gui/codeeditors/qgscodeeditorpython.cpp
Expand Up @@ -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() );

Expand Down Expand Up @@ -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 );

Expand Down Expand Up @@ -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 );

Expand Down Expand Up @@ -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() )
Expand Down
2 changes: 2 additions & 0 deletions src/gui/codeeditors/qgscodeeditorpython.h
Expand Up @@ -104,6 +104,8 @@ class GUI_EXPORT QgsCodeEditorPython : public QgsCodeEditor
*/
void updateCapabilities();

bool checkSyntax() override;

public slots:

/**
Expand Down

0 comments on commit 3312bf1

Please sign in to comment.