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() );
593 if ( position >= length() && position > 0 )
595 long style = SendScintilla( QsciScintillaBase::SCI_GETSTYLEAT, position - 1 );
596 return style == QsciLexerPython::Comment
597 || style == QsciLexerPython::TripleSingleQuotedString
598 || style == QsciLexerPython::TripleDoubleQuotedString
599 || style == QsciLexerPython::TripleSingleQuotedFString
600 || style == QsciLexerPython::TripleDoubleQuotedFString
601 || style == QsciLexerPython::UnclosedString;
605 long style = SendScintilla( QsciScintillaBase::SCI_GETSTYLEAT, position );
606 return style == QsciLexerPython::Comment
607 || style == QsciLexerPython::DoubleQuotedString
608 || style == QsciLexerPython::SingleQuotedString
609 || style == QsciLexerPython::TripleSingleQuotedString
610 || style == QsciLexerPython::TripleDoubleQuotedString
611 || style == QsciLexerPython::CommentBlock
612 || style == QsciLexerPython::UnclosedString
613 || style == QsciLexerPython::DoubleQuotedFString
614 || style == QsciLexerPython::SingleQuotedFString
615 || style == QsciLexerPython::TripleSingleQuotedFString
616 || style == QsciLexerPython::TripleDoubleQuotedFString;
627 return text( position - 1, position );
633 if ( position >= length() )
637 return text( position, position + 1 );
664 const QString originalText = text();
666 const QString defineCheckSyntax = QStringLiteral(
667 "def __check_syntax(script):\n"
669 " compile(script.encode('utf-8'), '', 'exec')\n"
670 " except SyntaxError as detail:\n"
671 " eline = detail.lineno or 1\n"
673 " ecolumn = detail.offset or 1\n"
674 " edescr = detail.msg\n"
675 " return '!!!!'.join([str(eline), str(ecolumn), edescr])\n"
680 QgsDebugError( QStringLiteral(
"Error running script: %1" ).arg( defineCheckSyntax ) );
688 if ( result.size() == 0 )
694 const QStringList parts = result.split( QStringLiteral(
"!!!!" ) );
695 if ( parts.size() == 3 )
697 const int line = parts.at( 0 ).toInt();
698 const int column = parts.at( 1 ).toInt();
700 setCursorPosition( line, column - 1 );
701 ensureLineVisible( line );
708 QgsDebugError( QStringLiteral(
"Error running script: %1" ).arg( script ) );
715 if ( !hasSelectedText() )
718 QString text = selectedText();
719 text = text.replace( QLatin1String(
">>> " ), QString() ).replace( QLatin1String(
"... " ), QString() ).trimmed();
720 const QString version = QString(
Qgis::version() ).split(
'.' ).mid( 0, 2 ).join(
'.' );
721 QDesktopServices::openUrl( QUrl( QStringLiteral(
"https://qgis.org/pyqgis/%1/search.html?q=%2" ).arg( version, text ) ) );
732 int startLine, startPos, endLine, endPos;
733 if ( hasSelectedText() )
735 getSelection( &startLine, &startPos, &endLine, &endPos );
739 getCursorPosition( &startLine, &startPos );
745 bool allEmpty =
true;
746 bool allCommented =
true;
747 int minIndentation = -1;
748 for (
int line = startLine; line <= endLine; line++ )
750 const QString stripped = text( line ).trimmed();
751 if ( !stripped.isEmpty() )
754 if ( !stripped.startsWith(
'#' ) )
756 allCommented =
false;
758 if ( minIndentation == -1 || minIndentation > indentation( line ) )
760 minIndentation = indentation( line );
774 for (
int line = startLine; line <= endLine; line++ )
776 const QString stripped = text( line ).trimmed();
779 if ( stripped.isEmpty() )
786 insertAt( QStringLiteral(
"# " ), line, minIndentation );
791 if ( !stripped.startsWith(
'#' ) )
795 if ( stripped.startsWith( QLatin1String(
"# " ) ) )
803 setSelection( line, indentation( line ), line, indentation( line ) + delta );
804 removeSelectedText();
809 setSelection( startLine, startPos - delta, endLine, endPos - delta );
816QgsQsciLexerPython::QgsQsciLexerPython( QObject *parent )
817 : QsciLexerPython( parent )
822const char *QgsQsciLexerPython::keywords(
int set )
const
826 return "True False and as assert break class continue def del elif else except "
827 "finally for from global if import in is lambda None not or pass "
828 "raise return try while with yield async await nonlocal";
831 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).
int linearPosition() const
Convenience function to return the cursor position as a linear index.
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)