32#include <Qsci/qscilexerpython.h>
33#include <QDesktopServices>
38const QMap<QString, QString> QgsCodeEditorPython::sCompletionPairs
46const QStringList QgsCodeEditorPython::sCompletionSingleCharacters{
"`",
"*"};
48const QgsSettingsEntryString *QgsCodeEditorPython::settingCodeFormatter =
new QgsSettingsEntryString( QStringLiteral(
"formatter" ), sTreePythonCodeEditor, QStringLiteral(
"autopep8" ), QStringLiteral(
"Python code autoformatter" ) );
50const QgsSettingsEntryBool *QgsCodeEditorPython::settingSortImports =
new QgsSettingsEntryBool( QStringLiteral(
"sort-imports" ), sTreePythonCodeEditor,
true, QStringLiteral(
"Whether imports should be sorted when auto-formatting code" ) );
52const QgsSettingsEntryBool *QgsCodeEditorPython::settingBlackNormalizeQuotes =
new QgsSettingsEntryBool( QStringLiteral(
"black-normalize-quotes" ), sTreePythonCodeEditor,
true, QStringLiteral(
"Whether quotes should be normalized when auto-formatting code using black" ) );
63 , mAPISFilesList( filenames )
90 setEdgeMode( QsciScintilla::EdgeLine );
91 setEdgeColumn( settingMaxLineLength->value() );
94 setWhitespaceVisibility( QsciScintilla::WsVisibleAfterIndent );
99 QsciLexerPython *pyLexer =
new QgsQsciLexerPython(
this );
101 pyLexer->setIndentationWarning( QsciLexerPython::Inconsistent );
102 pyLexer->setFoldComments(
true );
103 pyLexer->setFoldQuotes(
true );
105 pyLexer->setDefaultFont( font );
108 pyLexer->setFont( font, -1 );
110 font.setItalic(
true );
111 pyLexer->setFont( font, QsciLexerPython::Comment );
112 pyLexer->setFont( font, QsciLexerPython::CommentBlock );
114 font.setItalic(
false );
115 font.setBold(
true );
116 pyLexer->setFont( font, QsciLexerPython::SingleQuotedString );
117 pyLexer->setFont( font, QsciLexerPython::DoubleQuotedString );
119 pyLexer->setColor(
defaultColor, QsciLexerPython::Default );
134 std::unique_ptr< QsciAPIs > apis = std::make_unique< QsciAPIs >( pyLexer );
137 if ( mAPISFilesList.isEmpty() )
139 if ( settings.
value( QStringLiteral(
"pythonConsole/preloadAPI" ),
true ).toBool() )
142 apis->loadPrepared( mPapFile );
144 else if ( settings.
value( QStringLiteral(
"pythonConsole/usePreparedAPIFile" ),
false ).toBool() )
146 apis->loadPrepared( settings.
value( QStringLiteral(
"pythonConsole/preparedAPIFile" ) ).toString() );
150 const QStringList apiPaths = settings.
value( QStringLiteral(
"pythonConsole/userAPI" ) ).toStringList();
151 for (
const QString &path : apiPaths )
153 if ( !QFileInfo::exists( path ) )
155 QgsDebugError( QStringLiteral(
"The apis file %1 was not found" ).arg( path ) );
165 else if ( mAPISFilesList.length() == 1 && mAPISFilesList[0].right( 3 ) == QLatin1String(
"pap" ) )
167 if ( !QFileInfo::exists( mAPISFilesList[0] ) )
169 QgsDebugError( QStringLiteral(
"The apis file %1 not found" ).arg( mAPISFilesList.at( 0 ) ) );
172 mPapFile = mAPISFilesList[0];
173 apis->loadPrepared( mPapFile );
177 for (
const QString &path : std::as_const( mAPISFilesList ) )
179 if ( !QFileInfo::exists( path ) )
181 QgsDebugError( QStringLiteral(
"The apis file %1 was not found" ).arg( path ) );
191 pyLexer->setAPIs( apis.release() );
195 const int threshold = settings.
value( QStringLiteral(
"pythonConsole/autoCompThreshold" ), 2 ).toInt();
196 setAutoCompletionThreshold( threshold );
197 if ( !settings.
value(
"pythonConsole/autoCompleteEnabled",
true ).toBool() )
199 setAutoCompletionSource( AcsNone );
203 const QString autoCompleteSource = settings.
value( QStringLiteral(
"pythonConsole/autoCompleteSource" ), QStringLiteral(
"fromAPI" ) ).toString();
204 if ( autoCompleteSource == QLatin1String(
"fromDoc" ) )
205 setAutoCompletionSource( AcsDocument );
206 else if ( autoCompleteSource == QLatin1String(
"fromDocAPI" ) )
207 setAutoCompletionSource( AcsAll );
209 setAutoCompletionSource( AcsAPIs );
213 setIndentationsUseTabs(
false );
214 setIndentationGuides(
true );
229 bool autoCloseBracket = settings.
value( QStringLiteral(
"/pythonConsole/autoCloseBracket" ),
true ).toBool();
230 bool autoSurround = settings.
value( QStringLiteral(
"/pythonConsole/autoSurround" ),
true ).toBool();
231 bool autoInsertImport = settings.
value( QStringLiteral(
"/pythonConsole/autoInsertImport" ),
false ).toBool();
234 if ( event->key() == Qt::Key_Left ||
235 event->key() == Qt::Key_Right ||
236 event->key() == Qt::Key_Up ||
237 event->key() == Qt::Key_Down )
245 const QString eText =
event->text();
247 getCursorPosition( &line, &column );
251 if ( hasSelectedText() && autoSurround )
253 if ( sCompletionPairs.contains( eText ) )
255 int startLine, startPos, endLine, endPos;
256 getSelection( &startLine, &startPos, &endLine, &endPos );
259 if ( startLine != endLine && ( eText ==
"\"" || eText ==
"'" ) )
262 QString(
"%1%1%1%2%3%3%3" ).arg( eText, selectedText(), sCompletionPairs[eText] )
264 setSelection( startLine, startPos + 3, endLine, endPos + 3 );
269 QString(
"%1%2%3" ).arg( eText, selectedText(), sCompletionPairs[eText] )
271 setSelection( startLine, startPos + 1, endLine, endPos + 1 );
276 else if ( sCompletionSingleCharacters.contains( eText ) )
278 int startLine, startPos, endLine, endPos;
279 getSelection( &startLine, &startPos, &endLine, &endPos );
281 QString(
"%1%2%1" ).arg( eText, selectedText() )
283 setSelection( startLine, startPos + 1, endLine, endPos + 1 );
293 if ( autoInsertImport && eText ==
" " )
295 const QString lineText = text( line );
296 const thread_local QRegularExpression re( QStringLiteral(
"^from [\\w.]+$" ) );
297 if ( re.match( lineText.trimmed() ).hasMatch() )
299 insert( QStringLiteral(
" import" ) );
300 setCursorPosition( line, column + 7 );
306 else if ( autoCloseBracket )
312 if ( event->key() == Qt::Key_Backspace )
314 if ( sCompletionPairs.contains( prevChar ) && sCompletionPairs[prevChar] == nextChar )
316 setSelection( line, column - 1, line, column + 1 );
317 removeSelectedText();
331 else if ( sCompletionPairs.key( eText ) !=
"" && nextChar == eText )
333 setCursorPosition( line, column + 1 );
345 && sCompletionPairs.contains( eText )
346 && ( nextChar.isEmpty() || nextChar.at( 0 ).isSpace() || nextChar ==
":" || sCompletionPairs.key( nextChar ) !=
"" )
350 if ( !( ( eText ==
"\"" || eText ==
"'" ) && prevChar == eText ) )
353 insert( sCompletionPairs[eText] );
372 const QString
formatter = settingCodeFormatter->value();
373 const int maxLineLength = settingMaxLineLength->value();
375 QString newText = string;
377 QStringList missingModules;
379 if ( settingSortImports->value() )
381 const QString defineSortImports = QStringLiteral(
382 "def __qgis_sort_imports(script):\n"
385 " except ImportError:\n"
386 " return '_ImportError'\n"
387 " options={'line_length': %1, 'profile': '%2', 'known_first_party': ['qgis', 'console', 'processing', 'plugins']}\n"
388 " return isort.code(script, **options)\n" )
389 .arg( maxLineLength )
390 .arg(
formatter == QLatin1String(
"black" ) ? QStringLiteral(
"black" ) : QString() );
394 QgsDebugError( QStringLiteral(
"Error running script: %1" ).arg( defineSortImports ) );
402 if ( result == QLatin1String(
"_ImportError" ) )
404 missingModules << QStringLiteral(
"isort" );
413 QgsDebugError( QStringLiteral(
"Error running script: %1" ).arg( script ) );
418 if (
formatter == QLatin1String(
"autopep8" ) )
420 const int level = settingAutopep8Level->value();
422 const QString defineReformat = QStringLiteral(
423 "def __qgis_reformat(script):\n"
426 " except ImportError:\n"
427 " return '_ImportError'\n"
428 " options={'aggressive': %1, 'max_line_length': %2}\n"
429 " return autopep8.fix_code(script, options=options)\n" )
431 .arg( maxLineLength );
435 QgsDebugError( QStringLiteral(
"Error running script: %1" ).arg( defineReformat ) );
443 if ( result == QLatin1String(
"_ImportError" ) )
445 missingModules << QStringLiteral(
"autopep8" );
454 QgsDebugError( QStringLiteral(
"Error running script: %1" ).arg( script ) );
458 else if (
formatter == QLatin1String(
"black" ) )
460 const bool normalize = settingBlackNormalizeQuotes->value();
468 const QString defineReformat = QStringLiteral(
469 "def __qgis_reformat(script):\n"
472 " except ImportError:\n"
473 " return '_ImportError'\n"
474 " options={'string_normalization': %1, 'line_length': %2}\n"
475 " return black.format_str(script, mode=black.Mode(**options))\n" )
477 .arg( maxLineLength );
481 QgsDebugError( QStringLiteral(
"Error running script: %1" ).arg( defineReformat ) );
489 if ( result == QLatin1String(
"_ImportError" ) )
491 missingModules << QStringLiteral(
"black" );
500 QgsDebugError( QStringLiteral(
"Error running script: %1" ).arg( script ) );
505 if ( !missingModules.empty() )
507 if ( missingModules.size() == 1 )
513 const QString modules = missingModules.join( QLatin1String(
", " ) );
525 QAction *pyQgisHelpAction =
new QAction(
527 tr(
"Search Selection in PyQGIS Documentation" ),
529 pyQgisHelpAction->setEnabled( hasSelectedText() );
532 menu->addSeparator();
533 menu->addAction( pyQgisHelpAction );
538 switch ( autoCompletionSource() )
541 autoCompleteFromDocument();
545 autoCompleteFromAPIs();
549 autoCompleteFromAll();
559 mAPISFilesList = filenames;
566 QgsDebugMsgLevel( QStringLiteral(
"The script file: %1" ).arg( script ), 2 );
567 QFile file( script );
568 if ( !file.open( QIODevice::ReadOnly ) )
573 QTextStream in( &file );
574#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
575 in.setCodec(
"UTF-8" );
578 setText( in.readAll().trimmed() );
588 getCursorPosition( &line, &index );
589 int position = positionFromLineIndex( line, index );
595 if ( position >= length() && position > 0 )
597 long style = SendScintilla( QsciScintillaBase::SCI_GETSTYLEAT, position - 1 );
598 return style == QsciLexerPython::Comment
599 || style == QsciLexerPython::TripleSingleQuotedString
600 || style == QsciLexerPython::TripleDoubleQuotedString
601 || style == QsciLexerPython::TripleSingleQuotedFString
602 || style == QsciLexerPython::TripleDoubleQuotedFString
603 || style == QsciLexerPython::UnclosedString;
607 long style = SendScintilla( QsciScintillaBase::SCI_GETSTYLEAT, position );
608 return style == QsciLexerPython::Comment
609 || style == QsciLexerPython::DoubleQuotedString
610 || style == QsciLexerPython::SingleQuotedString
611 || style == QsciLexerPython::TripleSingleQuotedString
612 || style == QsciLexerPython::TripleDoubleQuotedString
613 || style == QsciLexerPython::CommentBlock
614 || style == QsciLexerPython::UnclosedString
615 || style == QsciLexerPython::DoubleQuotedFString
616 || style == QsciLexerPython::SingleQuotedFString
617 || style == QsciLexerPython::TripleSingleQuotedFString
618 || style == QsciLexerPython::TripleDoubleQuotedFString;
625 getCursorPosition( &line, &index );
626 int position = positionFromLineIndex( line, index );
631 return text( position - 1, position );
637 getCursorPosition( &line, &index );
638 int position = positionFromLineIndex( line, index );
639 if ( position >= length() )
643 return text( position, position + 1 );
670 const QString originalText = text();
672 const QString defineCheckSyntax = QStringLiteral(
673 "def __check_syntax(script):\n"
675 " compile(script.encode('utf-8'), '', 'exec')\n"
676 " except SyntaxError as detail:\n"
677 " eline = detail.lineno or 1\n"
679 " ecolumn = detail.offset or 1\n"
680 " edescr = detail.msg\n"
681 " return '!!!!'.join([str(eline), str(ecolumn), edescr])\n"
686 QgsDebugError( QStringLiteral(
"Error running script: %1" ).arg( defineCheckSyntax ) );
694 if ( result.size() == 0 )
700 const QStringList parts = result.split( QStringLiteral(
"!!!!" ) );
701 if ( parts.size() == 3 )
703 const int line = parts.at( 0 ).toInt();
704 const int column = parts.at( 1 ).toInt();
706 setCursorPosition( line, column - 1 );
707 ensureLineVisible( line );
714 QgsDebugError( QStringLiteral(
"Error running script: %1" ).arg( script ) );
721 if ( !hasSelectedText() )
724 QString text = selectedText();
725 text = text.replace( QLatin1String(
">>> " ), QString() ).replace( QLatin1String(
"... " ), QString() ).trimmed();
726 const QString version = QString(
Qgis::version() ).split(
'.' ).mid( 0, 2 ).join(
'.' );
727 QDesktopServices::openUrl( QUrl( QStringLiteral(
"https://qgis.org/pyqgis/%1/search.html?q=%2" ).arg( version, text ) ) );
738 int startLine, startPos, endLine, endPos;
739 if ( hasSelectedText() )
741 getSelection( &startLine, &startPos, &endLine, &endPos );
745 getCursorPosition( &startLine, &startPos );
751 bool allEmpty =
true;
752 bool allCommented =
true;
753 int minIndentation = -1;
754 for (
int line = startLine; line <= endLine; line++ )
756 const QString stripped = text( line ).trimmed();
757 if ( !stripped.isEmpty() )
760 if ( !stripped.startsWith(
'#' ) )
762 allCommented =
false;
764 if ( minIndentation == -1 || minIndentation > indentation( line ) )
766 minIndentation = indentation( line );
780 for (
int line = startLine; line <= endLine; line++ )
782 const QString stripped = text( line ).trimmed();
785 if ( stripped.isEmpty() )
792 insertAt( QStringLiteral(
"# " ), line, minIndentation );
797 if ( !stripped.startsWith(
'#' ) )
801 if ( stripped.startsWith( QLatin1String(
"# " ) ) )
809 setSelection( line, indentation( line ), line, indentation( line ) + delta );
810 removeSelectedText();
815 setSelection( startLine, startPos - delta, endLine, endPos - delta );
822QgsQsciLexerPython::QgsQsciLexerPython( QObject *parent )
823 : QsciLexerPython( parent )
828const char *QgsQsciLexerPython::keywords(
int set )
const
832 return "True False and as assert break class continue def del elif else except "
833 "finally for from global if import in is lambda None not or pass "
834 "raise return try while with yield async await nonlocal";
837 return QsciLexerPython::keywords( set );
static QString version()
Version string.
@ Warning
Warning message.
@ CheckSyntax
Language supports syntax checking.
@ Reformat
Language supports automatic code reformatting.
@ ToggleComment
Language supports comment toggling.
ScriptLanguage
Scripting languages.
static QString pkgDataPath()
Returns the common root path of all application data directories.
static QIcon getThemeIcon(const QString &name, const QColor &fillColor=QColor(), const QColor &strokeColor=QColor())
Helper to get a theme icon.
@ TripleSingleQuote
Triple single quote color.
@ CommentBlock
Comment block color.
@ Decoration
Decoration color.
@ Identifier
Identifier color.
@ DoubleQuote
Double quote color.
@ Default
Default text color.
@ Background
Background color.
@ SingleQuote
Single quote color.
@ Operator
Operator color.
@ TripleDoubleQuote
Triple double quote color.
void autoComplete()
Triggers the autocompletion popup.
QString characterAfterCursor() const
Returns the character after the cursor, or an empty string if the cursot is set at end.
bool isCursorInsideStringLiteralOrComment() const
Check whether the current cursor position is inside a string literal or a comment.
QString reformatCodeString(const QString &string) override
Applies code reformatting to a string and returns the result.
void searchSelectedTextInPyQGISDocs()
Searches the selected text in the official PyQGIS online documentation.
Qgis::ScriptLanguage language() const override
Returns the associated scripting language.
void loadAPIs(const QList< QString > &filenames)
Load APIs from one or more files.
void toggleComment() override
Toggle comment for the selected text.
void initializeLexer() override
Called when the dialect specific code lexer needs to be initialized (or reinitialized).
PRIVATE QgsCodeEditorPython(QWidget *parent=nullptr, const QList< QString > &filenames=QList< QString >(), QgsCodeEditor::Mode mode=QgsCodeEditor::Mode::ScriptEditor, QgsCodeEditor::Flags flags=QgsCodeEditor::Flag::CodeFolding)
Construct a new Python editor.
bool checkSyntax() override
Applies syntax checking to the editor.
void updateCapabilities()
Updates the editor capabilities.
Qgis::ScriptLanguageCapabilities languageCapabilities() const override
Returns the associated scripting language capabilities.
virtual void keyPressEvent(QKeyEvent *event) override
bool loadScript(const QString &script)
Loads a script file.
void populateContextMenu(QMenu *menu) override
Called when the context menu for the widget is about to be shown, after it has been fully populated w...
QString characterBeforeCursor() const
Returns the character before the cursor, or an empty string if cursor is set at start.
A text editor based on QScintilla2.
void keyPressEvent(QKeyEvent *event) override
virtual void populateContextMenu(QMenu *menu)
Called when the context menu for the widget is about to be shown, after it has been fully populated w...
void runPostLexerConfigurationTasks()
Performs tasks which must be run after a lexer has been set for the widget.
virtual void showMessage(const QString &title, const QString &message, Qgis::MessageLevel level)
Shows a user facing message (eg a warning message).
void setTitle(const QString &title)
Set the widget title.
void clearWarnings()
Clears all warning messages from the editor.
void setLineNumbersVisible(bool visible)
Sets whether line numbers should be visible in the editor.
QFont lexerFont() const
Returns the font to use in the lexer.
QColor lexerColor(QgsCodeEditorColorScheme::ColorRole role) const
Returns the color to use in the lexer for the specified role.
static QColor defaultColor(QgsCodeEditorColorScheme::ColorRole role, const QString &theme=QString())
Returns the default color for the specified role.
void addWarning(int lineNumber, const QString &warning)
Adds a warning message and indicator to the specified a lineNumber.
static QString stringToPythonLiteral(const QString &string)
Converts a string to a Python string literal.
static QString variantToPythonLiteral(const QVariant &value)
Converts a variant to a Python literal.
static bool run(const QString &command, const QString &messageOnError=QString())
Execute a Python statement.
static bool eval(const QString &command, QString &result)
Eval a Python statement.
static bool isValid()
Returns true if the runner has an instance (and thus is able to run commands)
A boolean settings entry.
An integer settings entry.
This class is a composition of two QSettings instances:
QVariant value(const QString &key, const QVariant &defaultValue=QVariant(), Section section=NoSection) const
Returns the value for setting key.
#define QgsDebugMsgLevel(str, level)
#define QgsDebugError(str)