Skip to content

Commit

Permalink
Support reformatting in base QgsCodeEditorPython class
Browse files Browse the repository at this point in the history
  • Loading branch information
nyalldawson committed Mar 20, 2023
1 parent ce9e57c commit ac5f8da
Show file tree
Hide file tree
Showing 6 changed files with 238 additions and 17 deletions.
11 changes: 10 additions & 1 deletion python/gui/auto_generated/codeeditors/qgscodeeditor.sip.in
Expand Up @@ -519,12 +519,21 @@ actions to the context menu.
.. versionadded:: 3.30
%End

virtual QString reformatCodeString( const QString &string, bool &ok /Out/, QString &error /Out/ ) const;
virtual QString reformatCodeString( const QString &string );
%Docstring
Applies code reformatting to a ``string`` and returns the result.

This is only supported for editors which return the Qgis.ScriptLanguageCapability.Reformat capability from :py:func:`~QgsCodeEditor.languageCapabilities`.

.. versionadded:: 3.32
%End

virtual void showMessage( const QString &title, const QString &message, Qgis::MessageLevel level );
%Docstring
Shows a user facing message (eg a warning message).

The default implementation uses QMessageBox.

.. versionadded:: 3.32
%End

Expand Down
12 changes: 11 additions & 1 deletion python/gui/auto_generated/codeeditors/qgscodeeditorpython.sip.in
Expand Up @@ -42,6 +42,8 @@ Construct a new Python editor.

virtual Qgis::ScriptLanguage language() const;

virtual Qgis::ScriptLanguageCapabilities languageCapabilities() const;


void loadAPIs( const QList<QString> &filenames );
%Docstring
Expand Down Expand Up @@ -74,6 +76,13 @@ Returns the character before the cursor, or an empty string if cursor is set at
Returns the character after the cursor, or an empty string if the cursot is set at end

.. versionadded:: 3.30
%End

void updateCapabilities();
%Docstring
Updates the editor capabilities.

.. versionadded:: 3.32
%End

public slots:
Expand All @@ -96,8 +105,9 @@ Toggle comment for the selected text.

virtual void initializeLexer();


virtual void keyPressEvent( QKeyEvent *event );
virtual QString reformatCodeString( const QString &string );


protected slots:

Expand Down
35 changes: 23 additions & 12 deletions src/gui/codeeditors/qgscodeeditor.cpp
Expand Up @@ -33,6 +33,7 @@
#include <QMenu>
#include <QClipboard>
#include <QScrollBar>
#include <QMessageBox>

QMap< QgsCodeEditorColorScheme::ColorRole, QString > QgsCodeEditor::sColorRoleToSettingsKey
{
Expand Down Expand Up @@ -575,12 +576,31 @@ void QgsCodeEditor::populateContextMenu( QMenu * )

}

QString QgsCodeEditor::reformatCodeString( const QString &string, bool &ok, QString & ) const
QString QgsCodeEditor::reformatCodeString( const QString &string )
{
ok = false;
return string;
}

void QgsCodeEditor::showMessage( const QString &title, const QString &message, Qgis::MessageLevel level )
{
switch ( level )
{
case Qgis::Info:
case Qgis::Success:
case Qgis::NoLevel:
QMessageBox::information( this, title, message );
break;

case Qgis::Warning:
QMessageBox::warning( this, title, message );
break;

case Qgis::Critical:
QMessageBox::critical( this, title, message );
break;
}
}

void QgsCodeEditor::updatePrompt()
{
if ( mInterpreter )
Expand Down Expand Up @@ -670,16 +690,7 @@ void QgsCodeEditor::reformatCode()

const QString originalText = text();

bool ok = false;
QString error;
const QString newText = reformatCodeString( originalText, ok, error );
if ( !ok )
{
if ( !error.isEmpty() )
{
// TODO raise
}
}
const QString newText = reformatCodeString( originalText );

if ( originalText == newText )
return;
Expand Down
11 changes: 10 additions & 1 deletion src/gui/codeeditors/qgscodeeditor.h
Expand Up @@ -537,7 +537,16 @@ class GUI_EXPORT QgsCodeEditor : public QsciScintilla
*
* \since QGIS 3.32
*/
virtual QString reformatCodeString( const QString &string, bool &ok SIP_OUT, QString &error SIP_OUT ) const;
virtual QString reformatCodeString( const QString &string );

/**
* Shows a user facing message (eg a warning message).
*
* The default implementation uses QMessageBox.
*
* \since QGIS 3.32
*/
virtual void showMessage( const QString &title, const QString &message, Qgis::MessageLevel level );

private:

Expand Down
174 changes: 173 additions & 1 deletion src/gui/codeeditors/qgscodeeditorpython.cpp
Expand Up @@ -19,7 +19,8 @@
#include "qgssymbollayerutils.h"
#include "qgssettings.h"
#include "qgis.h"

#include "qgspythonrunner.h"
#include "qgsprocessingutils.h"
#include <QWidget>
#include <QString>
#include <QFont>
Expand Down Expand Up @@ -57,13 +58,20 @@ QgsCodeEditorPython::QgsCodeEditorPython( QWidget *parent, const QList<QString>
setCaretWidth( 2 );

QgsCodeEditorPython::initializeLexer();

updateCapabilities();
}

Qgis::ScriptLanguage QgsCodeEditorPython::language() const
{
return Qgis::ScriptLanguage::Python;
}

Qgis::ScriptLanguageCapabilities QgsCodeEditorPython::languageCapabilities() const
{
return mCapabilities;
}

void QgsCodeEditorPython::initializeLexer()
{
const QgsSettings settings;
Expand Down Expand Up @@ -348,6 +356,157 @@ void QgsCodeEditorPython::keyPressEvent( QKeyEvent *event )
return QgsCodeEditor::keyPressEvent( event );
}

QString QgsCodeEditorPython::reformatCodeString( const QString &string )
{
if ( !QgsPythonRunner::isValid() )
{
return string;
}

QgsSettings settings;
const QString formatter = settings.value( QStringLiteral( "pythonConsole/formatter" ), QStringLiteral( "autopep8" ) ).toString();
const int maxLineLength = settings.value( QStringLiteral( "pythonConsole/maxLineLength" ), 80 ).toInt();

QString newText = string;

QStringList missingModules;

if ( settings.value( "pythonConsole/sortImports", true ).toBool() )
{
const QString defineSortImports = QStringLiteral(
"def __qgis_sort_imports(str):\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" )
.arg( maxLineLength )
.arg( formatter == QLatin1String( "black" ) ? QStringLiteral( "black" ) : QString() );

if ( !QgsPythonRunner::run( defineSortImports ) )
{
QgsDebugMsg( QStringLiteral( "Error running script: %1" ).arg( defineSortImports ) );
return string;
}

const QString script = QStringLiteral( "__qgis_sort_imports(%1)" ).arg( QgsProcessingUtils::stringToPythonLiteral( newText ) );
QString result;
if ( QgsPythonRunner::eval( script, result ) )
{
if ( result == QLatin1String( "_ImportError" ) )
{
missingModules << QStringLiteral( "isort" );
}
else
{
newText = result;
}
}
else
{
QgsDebugMsg( QStringLiteral( "Error running script: %1" ).arg( script ) );
return newText;
}
}

if ( formatter == QLatin1String( "autopep8" ) )
{
const int level = settings.value( QStringLiteral( "pythonConsole/autopep8Level" ), 1 ).toInt();

const QString defineReformat = QStringLiteral(
"def __qgis_reformat(str):\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" )
.arg( level )
.arg( maxLineLength );

if ( !QgsPythonRunner::run( defineReformat ) )
{
QgsDebugMsg( QStringLiteral( "Error running script: %1" ).arg( defineReformat ) );
return newText;
}

const QString script = QStringLiteral( "__qgis_reformat(%1)" ).arg( QgsProcessingUtils::stringToPythonLiteral( newText ) );
QString result;
if ( QgsPythonRunner::eval( script, result ) )
{
if ( result == QLatin1String( "_ImportError" ) )
{
missingModules << QStringLiteral( "autopep8" );
}
else
{
newText = result;
}
}
else
{
QgsDebugMsg( QStringLiteral( "Error running script: %1" ).arg( script ) );
return newText;
}
}
else if ( formatter == QLatin1String( "black" ) )
{
const bool normalize = settings.value( QStringLiteral( "pythonConsole/blackNormalizeQuotes" ), true ).toBool();

const QString defineReformat = QStringLiteral(
"def __qgis_reformat(str):\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" )
.arg( QgsProcessingUtils::variantToPythonLiteral( normalize ) )
.arg( maxLineLength );

if ( !QgsPythonRunner::run( defineReformat ) )
{
QgsDebugMsg( QStringLiteral( "Error running script: %1" ).arg( defineReformat ) );
return string;
}

const QString script = QStringLiteral( "__qgis_reformat(%1)" ).arg( QgsProcessingUtils::stringToPythonLiteral( newText ) );
QString result;
if ( QgsPythonRunner::eval( script, result ) )
{
if ( result == QLatin1String( "_ImportError" ) )
{
missingModules << QStringLiteral( "black" );
}
else
{
newText = result;
}
}
else
{
QgsDebugMsg( QStringLiteral( "Error running script: %1" ).arg( script ) );
return newText;
}
}

if ( !missingModules.empty() )
{
if ( missingModules.size() == 1 )
{
showMessage( tr( "Reformat Code" ), tr( "The Python module %1 is missing" ).arg( missingModules.at( 0 ) ), Qgis::MessageLevel::Warning );
}
else
{
const QString modules = missingModules.join( QStringLiteral( ", " ) );
showMessage( tr( "Reformat Code" ), tr( "The Python modules %1 are missing" ).arg( modules ), Qgis::MessageLevel::Warning );
}
}

return newText;
}

void QgsCodeEditorPython::autoComplete()
{
switch ( autoCompletionSource() )
Expand Down Expand Up @@ -455,6 +614,19 @@ QString QgsCodeEditorPython::characterAfterCursor() const
return text( position, position + 1 );
}

void QgsCodeEditorPython::updateCapabilities()
{
mCapabilities = Qgis::ScriptLanguageCapabilities();

if ( !QgsPythonRunner::isValid() )
return;

// 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;
}

void QgsCodeEditorPython::searchSelectedTextInPyQGISDocs()
{
if ( !hasSelectedText() )
Expand Down
12 changes: 11 additions & 1 deletion src/gui/codeeditors/qgscodeeditorpython.h
Expand Up @@ -63,6 +63,7 @@ class GUI_EXPORT QgsCodeEditorPython : public QgsCodeEditor
QgsCodeEditor::Mode mode = QgsCodeEditor::Mode::ScriptEditor );

Qgis::ScriptLanguage language() const override;
Qgis::ScriptLanguageCapabilities languageCapabilities() const override;

/**
* Load APIs from one or more files
Expand Down Expand Up @@ -96,6 +97,13 @@ class GUI_EXPORT QgsCodeEditorPython : public QgsCodeEditor
*/
QString characterAfterCursor() const;

/**
* Updates the editor capabilities.
*
* \since QGIS 3.32
*/
void updateCapabilities();

public slots:

/**
Expand All @@ -115,8 +123,8 @@ class GUI_EXPORT QgsCodeEditorPython : public QgsCodeEditor
protected:

void initializeLexer() override;

virtual void keyPressEvent( QKeyEvent *event ) override;
QString reformatCodeString( const QString &string ) override;

protected slots:

Expand All @@ -132,6 +140,8 @@ class GUI_EXPORT QgsCodeEditorPython : public QgsCodeEditor
QList<QString> mAPISFilesList;
QString mPapFile;

Qgis::ScriptLanguageCapabilities mCapabilities;

static const QMap<QString, QString> sCompletionPairs;

// Only used for selected text
Expand Down

0 comments on commit ac5f8da

Please sign in to comment.