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 );
137 std::unique_ptr< QsciAPIs > apis = std::make_unique< QsciAPIs >( pyLexer );
140 if ( mAPISFilesList.isEmpty() )
142 if ( settings.
value( QStringLiteral(
"pythonConsole/preloadAPI" ),
true ).toBool() )
145 apis->loadPrepared( mPapFile );
147 else if ( settings.
value( QStringLiteral(
"pythonConsole/usePreparedAPIFile" ),
false ).toBool() )
149 apis->loadPrepared( settings.
value( QStringLiteral(
"pythonConsole/preparedAPIFile" ) ).toString() );
153 const QStringList apiPaths = settings.
value( QStringLiteral(
"pythonConsole/userAPI" ) ).toStringList();
154 for (
const QString &path : apiPaths )
156 if ( !QFileInfo::exists( path ) )
158 QgsDebugError( QStringLiteral(
"The apis file %1 was not found" ).arg( path ) );
168 else if ( mAPISFilesList.length() == 1 && mAPISFilesList[0].right( 3 ) == QLatin1String(
"pap" ) )
170 if ( !QFileInfo::exists( mAPISFilesList[0] ) )
172 QgsDebugError( QStringLiteral(
"The apis file %1 not found" ).arg( mAPISFilesList.at( 0 ) ) );
175 mPapFile = mAPISFilesList[0];
176 apis->loadPrepared( mPapFile );
180 for (
const QString &path : std::as_const( mAPISFilesList ) )
182 if ( !QFileInfo::exists( path ) )
184 QgsDebugError( QStringLiteral(
"The apis file %1 was not found" ).arg( path ) );
194 pyLexer->setAPIs( apis.release() );
198 const int threshold = settings.
value( QStringLiteral(
"pythonConsole/autoCompThreshold" ), 2 ).toInt();
199 setAutoCompletionThreshold( threshold );
200 if ( !settings.
value(
"pythonConsole/autoCompleteEnabled",
true ).toBool() )
202 setAutoCompletionSource( AcsNone );
206 const QString autoCompleteSource = settings.
value( QStringLiteral(
"pythonConsole/autoCompleteSource" ), QStringLiteral(
"fromAPI" ) ).toString();
207 if ( autoCompleteSource == QLatin1String(
"fromDoc" ) )
208 setAutoCompletionSource( AcsDocument );
209 else if ( autoCompleteSource == QLatin1String(
"fromDocAPI" ) )
210 setAutoCompletionSource( AcsAll );
212 setAutoCompletionSource( AcsAPIs );
216 setIndentationsUseTabs(
false );
217 setIndentationGuides(
true );
232 bool autoCloseBracket = settings.
value( QStringLiteral(
"/pythonConsole/autoCloseBracket" ),
true ).toBool();
233 bool autoSurround = settings.
value( QStringLiteral(
"/pythonConsole/autoSurround" ),
true ).toBool();
234 bool autoInsertImport = settings.
value( QStringLiteral(
"/pythonConsole/autoInsertImport" ),
false ).toBool();
237 if ( event->key() == Qt::Key_Left ||
238 event->key() == Qt::Key_Right ||
239 event->key() == Qt::Key_Up ||
240 event->key() == Qt::Key_Down )
248 const QString eText =
event->text();
250 getCursorPosition( &line, &column );
254 if ( hasSelectedText() && autoSurround )
256 if ( sCompletionPairs.contains( eText ) )
258 int startLine, startPos, endLine, endPos;
259 getSelection( &startLine, &startPos, &endLine, &endPos );
262 if ( startLine != endLine && ( eText ==
"\"" || eText ==
"'" ) )
265 QString(
"%1%1%1%2%3%3%3" ).arg( eText, selectedText(), sCompletionPairs[eText] )
267 setSelection( startLine, startPos + 3, endLine, endPos + 3 );
272 QString(
"%1%2%3" ).arg( eText, selectedText(), sCompletionPairs[eText] )
274 setSelection( startLine, startPos + 1, endLine, endPos + 1 );
279 else if ( sCompletionSingleCharacters.contains( eText ) )
281 int startLine, startPos, endLine, endPos;
282 getSelection( &startLine, &startPos, &endLine, &endPos );
284 QString(
"%1%2%1" ).arg( eText, selectedText() )
286 setSelection( startLine, startPos + 1, endLine, endPos + 1 );
296 if ( autoInsertImport && eText ==
" " )
298 const QString lineText = text( line );
299 const thread_local QRegularExpression re( QStringLiteral(
"^from [\\w.]+$" ) );
300 if ( re.match( lineText.trimmed() ).hasMatch() )
302 insert( QStringLiteral(
" import" ) );
303 setCursorPosition( line, column + 7 );
309 else if ( autoCloseBracket )
315 if ( event->key() == Qt::Key_Backspace )
317 if ( sCompletionPairs.contains( prevChar ) && sCompletionPairs[prevChar] == nextChar )
319 setSelection( line, column - 1, line, column + 1 );
320 removeSelectedText();
334 else if ( sCompletionPairs.key( eText ) !=
"" && nextChar == eText )
336 setCursorPosition( line, column + 1 );
348 && sCompletionPairs.contains( eText )
349 && ( nextChar.isEmpty() || nextChar.at( 0 ).isSpace() || nextChar ==
":" || sCompletionPairs.key( nextChar ) !=
"" )
353 if ( !( ( eText ==
"\"" || eText ==
"'" ) && prevChar == eText ) )
356 insert( sCompletionPairs[eText] );
375 const QString
formatter = settingCodeFormatter->value();
376 const int maxLineLength = settingMaxLineLength->value();
378 QString newText = string;
380 QStringList missingModules;
382 if ( settingSortImports->value() )
384 const QString defineSortImports = QStringLiteral(
385 "def __qgis_sort_imports(script):\n"
388 " except ImportError:\n"
389 " return '_ImportError'\n"
390 " options={'line_length': %1, 'profile': '%2', 'known_first_party': ['qgis', 'console', 'processing', 'plugins']}\n"
391 " return isort.code(script, **options)\n" )
392 .arg( maxLineLength )
393 .arg(
formatter == QLatin1String(
"black" ) ? QStringLiteral(
"black" ) : QString() );
397 QgsDebugError( QStringLiteral(
"Error running script: %1" ).arg( defineSortImports ) );
405 if ( result == QLatin1String(
"_ImportError" ) )
407 missingModules << QStringLiteral(
"isort" );
416 QgsDebugError( QStringLiteral(
"Error running script: %1" ).arg( script ) );
421 if (
formatter == QLatin1String(
"autopep8" ) )
423 const int level = settingAutopep8Level->value();
425 const QString defineReformat = QStringLiteral(
426 "def __qgis_reformat(script):\n"
429 " except ImportError:\n"
430 " return '_ImportError'\n"
431 " options={'aggressive': %1, 'max_line_length': %2}\n"
432 " return autopep8.fix_code(script, options=options)\n" )
434 .arg( maxLineLength );
438 QgsDebugError( QStringLiteral(
"Error running script: %1" ).arg( defineReformat ) );
446 if ( result == QLatin1String(
"_ImportError" ) )
448 missingModules << QStringLiteral(
"autopep8" );
457 QgsDebugError( QStringLiteral(
"Error running script: %1" ).arg( script ) );
461 else if (
formatter == QLatin1String(
"black" ) )
463 const bool normalize = settingBlackNormalizeQuotes->value();
471 const QString defineReformat = QStringLiteral(
472 "def __qgis_reformat(script):\n"
475 " except ImportError:\n"
476 " return '_ImportError'\n"
477 " options={'string_normalization': %1, 'line_length': %2}\n"
478 " return black.format_str(script, mode=black.Mode(**options))\n" )
480 .arg( maxLineLength );
484 QgsDebugError( QStringLiteral(
"Error running script: %1" ).arg( defineReformat ) );
492 if ( result == QLatin1String(
"_ImportError" ) )
494 missingModules << QStringLiteral(
"black" );
503 QgsDebugError( QStringLiteral(
"Error running script: %1" ).arg( script ) );
508 if ( !missingModules.empty() )
510 if ( missingModules.size() == 1 )
516 const QString modules = missingModules.join( QLatin1String(
", " ) );
528 QAction *pyQgisHelpAction =
new QAction(
530 tr(
"Search Selection in PyQGIS Documentation" ),
532 pyQgisHelpAction->setEnabled( hasSelectedText() );
535 menu->addSeparator();
536 menu->addAction( pyQgisHelpAction );
541 switch ( autoCompletionSource() )
544 autoCompleteFromDocument();
548 autoCompleteFromAPIs();
552 autoCompleteFromAll();
562 mAPISFilesList = filenames;
569 QgsDebugMsgLevel( QStringLiteral(
"The script file: %1" ).arg( script ), 2 );
570 QFile file( script );
571 if ( !file.open( QIODevice::ReadOnly ) )
576 QTextStream in( &file );
577#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
578 in.setCodec(
"UTF-8" );
581 setText( in.readAll().trimmed() );
596 if ( position >= length() && position > 0 )
598 long style = SendScintilla( QsciScintillaBase::SCI_GETSTYLEAT, position - 1 );
599 return style == QsciLexerPython::Comment
600 || style == QsciLexerPython::TripleSingleQuotedString
601 || style == QsciLexerPython::TripleDoubleQuotedString
602 || style == QsciLexerPython::TripleSingleQuotedFString
603 || style == QsciLexerPython::TripleDoubleQuotedFString
604 || style == QsciLexerPython::UnclosedString;
608 long style = SendScintilla( QsciScintillaBase::SCI_GETSTYLEAT, position );
609 return style == QsciLexerPython::Comment
610 || style == QsciLexerPython::DoubleQuotedString
611 || style == QsciLexerPython::SingleQuotedString
612 || style == QsciLexerPython::TripleSingleQuotedString
613 || style == QsciLexerPython::TripleDoubleQuotedString
614 || style == QsciLexerPython::CommentBlock
615 || style == QsciLexerPython::UnclosedString
616 || style == QsciLexerPython::DoubleQuotedFString
617 || style == QsciLexerPython::SingleQuotedFString
618 || style == QsciLexerPython::TripleSingleQuotedFString
619 || style == QsciLexerPython::TripleDoubleQuotedFString;
630 return text( position - 1, position );
636 if ( position >= length() )
640 return text( position, position + 1 );
667 const QString originalText = text();
669 const QString defineCheckSyntax = QStringLiteral(
670 "def __check_syntax(script):\n"
672 " compile(script.encode('utf-8'), '', 'exec')\n"
673 " except SyntaxError as detail:\n"
674 " eline = detail.lineno or 1\n"
676 " ecolumn = detail.offset or 1\n"
677 " edescr = detail.msg\n"
678 " return '!!!!'.join([str(eline), str(ecolumn), edescr])\n"
683 QgsDebugError( QStringLiteral(
"Error running script: %1" ).arg( defineCheckSyntax ) );
691 if ( result.size() == 0 )
697 const QStringList parts = result.split( QStringLiteral(
"!!!!" ) );
698 if ( parts.size() == 3 )
700 const int line = parts.at( 0 ).toInt();
701 const int column = parts.at( 1 ).toInt();
703 setCursorPosition( line, column - 1 );
704 ensureLineVisible( line );
711 QgsDebugError( QStringLiteral(
"Error running script: %1" ).arg( script ) );
718 if ( !hasSelectedText() )
721 QString text = selectedText();
722 text = text.replace( QLatin1String(
">>> " ), QString() ).replace( QLatin1String(
"... " ), QString() ).trimmed();
723 const QString version = QString(
Qgis::version() ).split(
'.' ).mid( 0, 2 ).join(
'.' );
724 QDesktopServices::openUrl( QUrl( QStringLiteral(
"https://qgis.org/pyqgis/%1/search.html?q=%2" ).arg( version, text ) ) );
735 int startLine, startPos, endLine, endPos;
736 if ( hasSelectedText() )
738 getSelection( &startLine, &startPos, &endLine, &endPos );
742 getCursorPosition( &startLine, &startPos );
748 bool allEmpty =
true;
749 bool allCommented =
true;
750 int minIndentation = -1;
751 for (
int line = startLine; line <= endLine; line++ )
753 const QString stripped = text( line ).trimmed();
754 if ( !stripped.isEmpty() )
757 if ( !stripped.startsWith(
'#' ) )
759 allCommented =
false;
761 if ( minIndentation == -1 || minIndentation > indentation( line ) )
763 minIndentation = indentation( line );
777 for (
int line = startLine; line <= endLine; line++ )
779 const QString stripped = text( line ).trimmed();
782 if ( stripped.isEmpty() )
789 insertAt( QStringLiteral(
"# " ), line, minIndentation );
794 if ( !stripped.startsWith(
'#' ) )
798 if ( stripped.startsWith( QLatin1String(
"# " ) ) )
806 setSelection( line, indentation( line ), line, indentation( line ) + delta );
807 removeSelectedText();
812 setSelection( startLine, startPos - delta, endLine, endPos - delta );
819QgsQsciLexerPython::QgsQsciLexerPython( QObject *parent )
820 : QsciLexerPython( parent )
825const char *QgsQsciLexerPython::keywords(
int set )
const
829 return "True False and as assert break class continue def del elif else except "
830 "finally for from global if import in is lambda None not or pass "
831 "raise return try while with yield async await nonlocal";
834 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)