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" ) );
53const QgsSettingsEntryString *QgsCodeEditorPython::settingExternalPythonEditorCommand =
new QgsSettingsEntryString( QStringLiteral(
"external-editor" ), sTreePythonCodeEditor, QString(), QStringLiteral(
"Command to launch an external Python code editor. Use the token <file> to insert the filename, <line> to insert line number, and <col> to insert the column number." ) );
64 , mAPISFilesList( filenames )
91 setEdgeMode( QsciScintilla::EdgeLine );
92 setEdgeColumn( settingMaxLineLength->value() );
95 setWhitespaceVisibility( QsciScintilla::WsVisibleAfterIndent );
97 SendScintilla( QsciScintillaBase::SCI_SETPROPERTY,
"highlight.current.word",
"1" );
102 QsciLexerPython *pyLexer =
new QgsQsciLexerPython(
this );
104 pyLexer->setIndentationWarning( QsciLexerPython::Inconsistent );
105 pyLexer->setFoldComments(
true );
106 pyLexer->setFoldQuotes(
true );
108 pyLexer->setDefaultFont( font );
111 pyLexer->setFont( font, -1 );
113 font.setItalic(
true );
114 pyLexer->setFont( font, QsciLexerPython::Comment );
115 pyLexer->setFont( font, QsciLexerPython::CommentBlock );
117 font.setItalic(
false );
118 font.setBold(
true );
119 pyLexer->setFont( font, QsciLexerPython::SingleQuotedString );
120 pyLexer->setFont( font, QsciLexerPython::DoubleQuotedString );
122 pyLexer->setColor(
defaultColor, QsciLexerPython::Default );
140 std::unique_ptr< QsciAPIs > apis = std::make_unique< QsciAPIs >( pyLexer );
143 if ( mAPISFilesList.isEmpty() )
145 if ( settings.
value( QStringLiteral(
"pythonConsole/preloadAPI" ),
true ).toBool() )
148 apis->loadPrepared( mPapFile );
150 else if ( settings.
value( QStringLiteral(
"pythonConsole/usePreparedAPIFile" ),
false ).toBool() )
152 apis->loadPrepared( settings.
value( QStringLiteral(
"pythonConsole/preparedAPIFile" ) ).toString() );
156 const QStringList apiPaths = settings.
value( QStringLiteral(
"pythonConsole/userAPI" ) ).toStringList();
157 for (
const QString &path : apiPaths )
159 if ( !QFileInfo::exists( path ) )
161 QgsDebugError( QStringLiteral(
"The apis file %1 was not found" ).arg( path ) );
171 else if ( mAPISFilesList.length() == 1 && mAPISFilesList[0].right( 3 ) == QLatin1String(
"pap" ) )
173 if ( !QFileInfo::exists( mAPISFilesList[0] ) )
175 QgsDebugError( QStringLiteral(
"The apis file %1 not found" ).arg( mAPISFilesList.at( 0 ) ) );
178 mPapFile = mAPISFilesList[0];
179 apis->loadPrepared( mPapFile );
183 for (
const QString &path : std::as_const( mAPISFilesList ) )
185 if ( !QFileInfo::exists( path ) )
187 QgsDebugError( QStringLiteral(
"The apis file %1 was not found" ).arg( path ) );
197 pyLexer->setAPIs( apis.release() );
201 const int threshold = settings.
value( QStringLiteral(
"pythonConsole/autoCompThreshold" ), 2 ).toInt();
202 setAutoCompletionThreshold( threshold );
203 if ( !settings.
value(
"pythonConsole/autoCompleteEnabled",
true ).toBool() )
205 setAutoCompletionSource( AcsNone );
209 const QString autoCompleteSource = settings.
value( QStringLiteral(
"pythonConsole/autoCompleteSource" ), QStringLiteral(
"fromAPI" ) ).toString();
210 if ( autoCompleteSource == QLatin1String(
"fromDoc" ) )
211 setAutoCompletionSource( AcsDocument );
212 else if ( autoCompleteSource == QLatin1String(
"fromDocAPI" ) )
213 setAutoCompletionSource( AcsAll );
215 setAutoCompletionSource( AcsAPIs );
219 setIndentationsUseTabs(
false );
220 setIndentationGuides(
true );
235 bool autoCloseBracket = settings.
value( QStringLiteral(
"/pythonConsole/autoCloseBracket" ),
true ).toBool();
236 bool autoSurround = settings.
value( QStringLiteral(
"/pythonConsole/autoSurround" ),
true ).toBool();
237 bool autoInsertImport = settings.
value( QStringLiteral(
"/pythonConsole/autoInsertImport" ),
false ).toBool();
240 if ( event->key() == Qt::Key_Left ||
241 event->key() == Qt::Key_Right ||
242 event->key() == Qt::Key_Up ||
243 event->key() == Qt::Key_Down )
251 const QString eText =
event->text();
253 getCursorPosition( &line, &column );
257 if ( hasSelectedText() && autoSurround )
259 if ( sCompletionPairs.contains( eText ) )
261 int startLine, startPos, endLine, endPos;
262 getSelection( &startLine, &startPos, &endLine, &endPos );
265 if ( startLine != endLine && ( eText ==
"\"" || eText ==
"'" ) )
268 QString(
"%1%1%1%2%3%3%3" ).arg( eText, selectedText(), sCompletionPairs[eText] )
270 setSelection( startLine, startPos + 3, endLine, endPos + 3 );
275 QString(
"%1%2%3" ).arg( eText, selectedText(), sCompletionPairs[eText] )
277 setSelection( startLine, startPos + 1, endLine, endPos + 1 );
282 else if ( sCompletionSingleCharacters.contains( eText ) )
284 int startLine, startPos, endLine, endPos;
285 getSelection( &startLine, &startPos, &endLine, &endPos );
287 QString(
"%1%2%1" ).arg( eText, selectedText() )
289 setSelection( startLine, startPos + 1, endLine, endPos + 1 );
299 if ( autoInsertImport && eText ==
" " )
301 const QString lineText = text( line );
302 const thread_local QRegularExpression re( QStringLiteral(
"^from [\\w.]+$" ) );
303 if ( re.match( lineText.trimmed() ).hasMatch() )
305 insert( QStringLiteral(
" import" ) );
306 setCursorPosition( line, column + 7 );
312 else if ( autoCloseBracket )
318 if ( event->key() == Qt::Key_Backspace )
320 if ( sCompletionPairs.contains( prevChar ) && sCompletionPairs[prevChar] == nextChar )
322 setSelection( line, column - 1, line, column + 1 );
323 removeSelectedText();
337 else if ( sCompletionPairs.key( eText ) !=
"" && nextChar == eText )
339 setCursorPosition( line, column + 1 );
351 && sCompletionPairs.contains( eText )
352 && ( nextChar.isEmpty() || nextChar.at( 0 ).isSpace() || nextChar ==
":" || sCompletionPairs.key( nextChar ) !=
"" )
356 if ( !( ( eText ==
"\"" || eText ==
"'" ) && prevChar == eText ) )
359 insert( sCompletionPairs[eText] );
378 const QString
formatter = settingCodeFormatter->value();
379 const int maxLineLength = settingMaxLineLength->value();
381 QString newText = string;
383 QStringList missingModules;
385 if ( settingSortImports->value() )
387 const QString defineSortImports = QStringLiteral(
388 "def __qgis_sort_imports(script):\n"
391 " except ImportError:\n"
392 " return '_ImportError'\n"
393 " options={'line_length': %1, 'profile': '%2', 'known_first_party': ['qgis', 'console', 'processing', 'plugins']}\n"
394 " return isort.code(script, **options)\n" )
395 .arg( maxLineLength )
396 .arg(
formatter == QLatin1String(
"black" ) ? QStringLiteral(
"black" ) : QString() );
400 QgsDebugError( QStringLiteral(
"Error running script: %1" ).arg( defineSortImports ) );
408 if ( result == QLatin1String(
"_ImportError" ) )
410 missingModules << QStringLiteral(
"isort" );
419 QgsDebugError( QStringLiteral(
"Error running script: %1" ).arg( script ) );
424 if (
formatter == QLatin1String(
"autopep8" ) )
426 const int level = settingAutopep8Level->value();
428 const QString defineReformat = QStringLiteral(
429 "def __qgis_reformat(script):\n"
432 " except ImportError:\n"
433 " return '_ImportError'\n"
434 " options={'aggressive': %1, 'max_line_length': %2}\n"
435 " return autopep8.fix_code(script, options=options)\n" )
437 .arg( maxLineLength );
441 QgsDebugError( QStringLiteral(
"Error running script: %1" ).arg( defineReformat ) );
449 if ( result == QLatin1String(
"_ImportError" ) )
451 missingModules << QStringLiteral(
"autopep8" );
460 QgsDebugError( QStringLiteral(
"Error running script: %1" ).arg( script ) );
464 else if (
formatter == QLatin1String(
"black" ) )
466 const bool normalize = settingBlackNormalizeQuotes->value();
474 const QString defineReformat = QStringLiteral(
475 "def __qgis_reformat(script):\n"
478 " except ImportError:\n"
479 " return '_ImportError'\n"
480 " options={'string_normalization': %1, 'line_length': %2}\n"
481 " return black.format_str(script, mode=black.Mode(**options))\n" )
483 .arg( maxLineLength );
487 QgsDebugError( QStringLiteral(
"Error running script: %1" ).arg( defineReformat ) );
495 if ( result == QLatin1String(
"_ImportError" ) )
497 missingModules << QStringLiteral(
"black" );
506 QgsDebugError( QStringLiteral(
"Error running script: %1" ).arg( script ) );
511 if ( !missingModules.empty() )
513 if ( missingModules.size() == 1 )
519 const QString modules = missingModules.join( QLatin1String(
", " ) );
531 QAction *pyQgisHelpAction =
new QAction(
533 tr(
"Search Selection in PyQGIS Documentation" ),
535 pyQgisHelpAction->setEnabled( hasSelectedText() );
538 menu->addSeparator();
539 menu->addAction( pyQgisHelpAction );
544 switch ( autoCompletionSource() )
547 autoCompleteFromDocument();
551 autoCompleteFromAPIs();
555 autoCompleteFromAll();
565 mAPISFilesList = filenames;
572 QgsDebugMsgLevel( QStringLiteral(
"The script file: %1" ).arg( script ), 2 );
573 QFile file( script );
574 if ( !file.open( QIODevice::ReadOnly ) )
579 QTextStream in( &file );
580#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
581 in.setCodec(
"UTF-8" );
584 setText( in.readAll().trimmed() );
599 if ( position >= length() && position > 0 )
601 long style = SendScintilla( QsciScintillaBase::SCI_GETSTYLEAT, position - 1 );
602 return style == QsciLexerPython::Comment
603 || style == QsciLexerPython::TripleSingleQuotedString
604 || style == QsciLexerPython::TripleDoubleQuotedString
605 || style == QsciLexerPython::TripleSingleQuotedFString
606 || style == QsciLexerPython::TripleDoubleQuotedFString
607 || style == QsciLexerPython::UnclosedString;
611 long style = SendScintilla( QsciScintillaBase::SCI_GETSTYLEAT, position );
612 return style == QsciLexerPython::Comment
613 || style == QsciLexerPython::DoubleQuotedString
614 || style == QsciLexerPython::SingleQuotedString
615 || style == QsciLexerPython::TripleSingleQuotedString
616 || style == QsciLexerPython::TripleDoubleQuotedString
617 || style == QsciLexerPython::CommentBlock
618 || style == QsciLexerPython::UnclosedString
619 || style == QsciLexerPython::DoubleQuotedFString
620 || style == QsciLexerPython::SingleQuotedFString
621 || style == QsciLexerPython::TripleSingleQuotedFString
622 || style == QsciLexerPython::TripleDoubleQuotedFString;
633 return text( position - 1, position );
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.
QFlags< ScriptLanguageCapability > ScriptLanguageCapabilities
Script language capabilities.
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 cursor 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...
QFlags< Flag > Flags
Flags controlling behavior of code editor.
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)