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 const QString eText =
event->text();
242 getCursorPosition( &line, &column );
246 if ( hasSelectedText() && autoSurround )
248 if ( sCompletionPairs.contains( eText ) )
250 int startLine, startPos, endLine, endPos;
251 getSelection( &startLine, &startPos, &endLine, &endPos );
254 if ( startLine != endLine && ( eText ==
"\"" || eText ==
"'" ) )
257 QString(
"%1%1%1%2%3%3%3" ).arg( eText, selectedText(), sCompletionPairs[eText] )
259 setSelection( startLine, startPos + 3, endLine, endPos + 3 );
264 QString(
"%1%2%3" ).arg( eText, selectedText(), sCompletionPairs[eText] )
266 setSelection( startLine, startPos + 1, endLine, endPos + 1 );
271 else if ( sCompletionSingleCharacters.contains( eText ) )
273 int startLine, startPos, endLine, endPos;
274 getSelection( &startLine, &startPos, &endLine, &endPos );
276 QString(
"%1%2%1" ).arg( eText, selectedText() )
278 setSelection( startLine, startPos + 1, endLine, endPos + 1 );
288 if ( autoInsertImport && eText ==
" " )
290 const QString lineText = text( line );
291 const thread_local QRegularExpression re( QStringLiteral(
"^from [\\w.]+$" ) );
292 if ( re.match( lineText.trimmed() ).hasMatch() )
294 insert( QStringLiteral(
" import" ) );
295 setCursorPosition( line, column + 7 );
301 else if ( autoCloseBracket )
307 if ( event->key() == Qt::Key_Backspace )
309 if ( sCompletionPairs.contains( prevChar ) && sCompletionPairs[prevChar] == nextChar )
311 setSelection( line, column - 1, line, column + 1 );
312 removeSelectedText();
325 else if ( sCompletionPairs.key( eText ) !=
"" && nextChar == eText )
327 setCursorPosition( line, column + 1 );
339 && sCompletionPairs.contains( eText )
340 && ( nextChar.isEmpty() || nextChar.at( 0 ).isSpace() || nextChar ==
":" || sCompletionPairs.key( nextChar ) !=
"" )
344 if ( !( ( eText ==
"\"" || eText ==
"'" ) && prevChar == eText ) )
347 insert( sCompletionPairs[eText] );
366 const QString
formatter = settingCodeFormatter->value();
367 const int maxLineLength = settingMaxLineLength->value();
369 QString newText = string;
371 QStringList missingModules;
373 if ( settingSortImports->value() )
375 const QString defineSortImports = QStringLiteral(
376 "def __qgis_sort_imports(script):\n"
379 " except ImportError:\n"
380 " return '_ImportError'\n"
381 " options={'line_length': %1, 'profile': '%2', 'known_first_party': ['qgis', 'console', 'processing', 'plugins']}\n"
382 " return isort.code(script, **options)\n" )
383 .arg( maxLineLength )
384 .arg(
formatter == QLatin1String(
"black" ) ? QStringLiteral(
"black" ) : QString() );
388 QgsDebugError( QStringLiteral(
"Error running script: %1" ).arg( defineSortImports ) );
396 if ( result == QLatin1String(
"_ImportError" ) )
398 missingModules << QStringLiteral(
"isort" );
407 QgsDebugError( QStringLiteral(
"Error running script: %1" ).arg( script ) );
412 if (
formatter == QLatin1String(
"autopep8" ) )
414 const int level = settingAutopep8Level->value();
416 const QString defineReformat = QStringLiteral(
417 "def __qgis_reformat(script):\n"
420 " except ImportError:\n"
421 " return '_ImportError'\n"
422 " options={'aggressive': %1, 'max_line_length': %2}\n"
423 " return autopep8.fix_code(script, options=options)\n" )
425 .arg( maxLineLength );
429 QgsDebugError( QStringLiteral(
"Error running script: %1" ).arg( defineReformat ) );
437 if ( result == QLatin1String(
"_ImportError" ) )
439 missingModules << QStringLiteral(
"autopep8" );
448 QgsDebugError( QStringLiteral(
"Error running script: %1" ).arg( script ) );
452 else if (
formatter == QLatin1String(
"black" ) )
454 const bool normalize = settingBlackNormalizeQuotes->value();
462 const QString defineReformat = QStringLiteral(
463 "def __qgis_reformat(script):\n"
466 " except ImportError:\n"
467 " return '_ImportError'\n"
468 " options={'string_normalization': %1, 'line_length': %2}\n"
469 " return black.format_str(script, mode=black.Mode(**options))\n" )
471 .arg( maxLineLength );
475 QgsDebugError( QStringLiteral(
"Error running script: %1" ).arg( defineReformat ) );
483 if ( result == QLatin1String(
"_ImportError" ) )
485 missingModules << QStringLiteral(
"black" );
494 QgsDebugError( QStringLiteral(
"Error running script: %1" ).arg( script ) );
499 if ( !missingModules.empty() )
501 if ( missingModules.size() == 1 )
507 const QString modules = missingModules.join( QLatin1String(
", " ) );
519 QAction *pyQgisHelpAction =
new QAction(
521 tr(
"Search Selection in PyQGIS Documentation" ),
523 pyQgisHelpAction->setEnabled( hasSelectedText() );
526 menu->addSeparator();
527 menu->addAction( pyQgisHelpAction );
532 switch ( autoCompletionSource() )
535 autoCompleteFromDocument();
539 autoCompleteFromAPIs();
543 autoCompleteFromAll();
553 mAPISFilesList = filenames;
560 QgsDebugMsgLevel( QStringLiteral(
"The script file: %1" ).arg( script ), 2 );
561 QFile file( script );
562 if ( !file.open( QIODevice::ReadOnly ) )
567 QTextStream in( &file );
568#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
569 in.setCodec(
"UTF-8" );
572 setText( in.readAll().trimmed() );
587 if ( position >= length() && position > 0 )
589 long style = SendScintilla( QsciScintillaBase::SCI_GETSTYLEAT, position - 1 );
590 return style == QsciLexerPython::Comment
591 || style == QsciLexerPython::TripleSingleQuotedString
592 || style == QsciLexerPython::TripleDoubleQuotedString
593 || style == QsciLexerPython::TripleSingleQuotedFString
594 || style == QsciLexerPython::TripleDoubleQuotedFString
595 || style == QsciLexerPython::UnclosedString;
599 long style = SendScintilla( QsciScintillaBase::SCI_GETSTYLEAT, position );
600 return style == QsciLexerPython::Comment
601 || style == QsciLexerPython::DoubleQuotedString
602 || style == QsciLexerPython::SingleQuotedString
603 || style == QsciLexerPython::TripleSingleQuotedString
604 || style == QsciLexerPython::TripleDoubleQuotedString
605 || style == QsciLexerPython::CommentBlock
606 || style == QsciLexerPython::UnclosedString
607 || style == QsciLexerPython::DoubleQuotedFString
608 || style == QsciLexerPython::SingleQuotedFString
609 || style == QsciLexerPython::TripleSingleQuotedFString
610 || style == QsciLexerPython::TripleDoubleQuotedFString;
621 return text( position - 1, position );
627 if ( position >= length() )
631 return text( position, position + 1 );
658 const QString originalText = text();
660 const QString defineCheckSyntax = QStringLiteral(
661 "def __check_syntax(script):\n"
663 " compile(script.encode('utf-8'), '', 'exec')\n"
664 " except SyntaxError as detail:\n"
665 " eline = detail.lineno or 1\n"
667 " ecolumn = detail.offset or 1\n"
668 " edescr = detail.msg\n"
669 " return '!!!!'.join([str(eline), str(ecolumn), edescr])\n"
674 QgsDebugError( QStringLiteral(
"Error running script: %1" ).arg( defineCheckSyntax ) );
682 if ( result.size() == 0 )
688 const QStringList parts = result.split( QStringLiteral(
"!!!!" ) );
689 if ( parts.size() == 3 )
691 const int line = parts.at( 0 ).toInt();
692 const int column = parts.at( 1 ).toInt();
694 setCursorPosition( line, column - 1 );
695 ensureLineVisible( line );
702 QgsDebugError( QStringLiteral(
"Error running script: %1" ).arg( script ) );
709 if ( !hasSelectedText() )
712 QString text = selectedText();
713 text = text.replace( QLatin1String(
">>> " ), QString() ).replace( QLatin1String(
"... " ), QString() ).trimmed();
714 const QString version = QString(
Qgis::version() ).split(
'.' ).mid( 0, 2 ).join(
'.' );
715 QDesktopServices::openUrl( QUrl( QStringLiteral(
"https://qgis.org/pyqgis/%1/search.html?q=%2" ).arg( version, text ) ) );
726 int startLine, startPos, endLine, endPos;
727 if ( hasSelectedText() )
729 getSelection( &startLine, &startPos, &endLine, &endPos );
733 getCursorPosition( &startLine, &startPos );
739 bool allEmpty =
true;
740 bool allCommented =
true;
741 int minIndentation = -1;
742 for (
int line = startLine; line <= endLine; line++ )
744 const QString stripped = text( line ).trimmed();
745 if ( !stripped.isEmpty() )
748 if ( !stripped.startsWith(
'#' ) )
750 allCommented =
false;
752 if ( minIndentation == -1 || minIndentation > indentation( line ) )
754 minIndentation = indentation( line );
768 for (
int line = startLine; line <= endLine; line++ )
770 const QString stripped = text( line ).trimmed();
773 if ( stripped.isEmpty() )
780 insertAt( QStringLiteral(
"# " ), line, minIndentation );
785 if ( !stripped.startsWith(
'#' ) )
789 if ( stripped.startsWith( QLatin1String(
"# " ) ) )
797 setSelection( line, indentation( line ), line, indentation( line ) + delta );
798 removeSelectedText();
803 setSelection( startLine, startPos - delta, endLine, endPos - delta );
810QgsQsciLexerPython::QgsQsciLexerPython( QObject *parent )
811 : QsciLexerPython( parent )
816const char *QgsQsciLexerPython::keywords(
int set )
const
820 return "True False and as assert break class continue def del elif else except "
821 "finally for from global if import in is lambda None not or pass "
822 "raise return try while with yield async await nonlocal";
825 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.
virtual void callTip() override
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)