18#include "moc_qgscodeeditorpython.cpp"
33#include <Qsci/qscilexerpython.h>
34#include <QDesktopServices>
39const QMap<QString, QString> QgsCodeEditorPython::sCompletionPairs
47const QStringList QgsCodeEditorPython::sCompletionSingleCharacters{
"`",
"*"};
49const QgsSettingsEntryString *QgsCodeEditorPython::settingCodeFormatter =
new QgsSettingsEntryString( QStringLiteral(
"formatter" ), sTreePythonCodeEditor, QStringLiteral(
"autopep8" ), QStringLiteral(
"Python code autoformatter" ) );
51const QgsSettingsEntryBool *QgsCodeEditorPython::settingSortImports =
new QgsSettingsEntryBool( QStringLiteral(
"sort-imports" ), sTreePythonCodeEditor,
true, QStringLiteral(
"Whether imports should be sorted when auto-formatting code" ) );
53const QgsSettingsEntryBool *QgsCodeEditorPython::settingBlackNormalizeQuotes =
new QgsSettingsEntryBool( QStringLiteral(
"black-normalize-quotes" ), sTreePythonCodeEditor,
true, QStringLiteral(
"Whether quotes should be normalized when auto-formatting code using black" ) );
54const 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." ) );
65 , mAPISFilesList( filenames )
92 setEdgeMode( QsciScintilla::EdgeLine );
93 setEdgeColumn( settingMaxLineLength->value() );
96 setWhitespaceVisibility( QsciScintilla::WsVisibleAfterIndent );
98 SendScintilla( QsciScintillaBase::SCI_SETPROPERTY,
"highlight.current.word",
"1" );
103 QsciLexerPython *pyLexer =
new QgsQsciLexerPython(
this );
105 pyLexer->setIndentationWarning( QsciLexerPython::Inconsistent );
106 pyLexer->setFoldComments(
true );
107 pyLexer->setFoldQuotes(
true );
109 pyLexer->setDefaultFont( font );
112 pyLexer->setFont( font, -1 );
114 font.setItalic(
true );
115 pyLexer->setFont( font, QsciLexerPython::Comment );
116 pyLexer->setFont( font, QsciLexerPython::CommentBlock );
118 font.setItalic(
false );
119 font.setBold(
true );
120 pyLexer->setFont( font, QsciLexerPython::SingleQuotedString );
121 pyLexer->setFont( font, QsciLexerPython::DoubleQuotedString );
123 pyLexer->setColor(
defaultColor, QsciLexerPython::Default );
141 std::unique_ptr< QsciAPIs > apis = std::make_unique< QsciAPIs >( pyLexer );
144 if ( mAPISFilesList.isEmpty() )
146 if ( settings.
value( QStringLiteral(
"pythonConsole/preloadAPI" ),
true ).toBool() )
149 apis->loadPrepared( mPapFile );
151 else if ( settings.
value( QStringLiteral(
"pythonConsole/usePreparedAPIFile" ),
false ).toBool() )
153 apis->loadPrepared( settings.
value( QStringLiteral(
"pythonConsole/preparedAPIFile" ) ).toString() );
157 const QStringList apiPaths = settings.
value( QStringLiteral(
"pythonConsole/userAPI" ) ).toStringList();
158 for (
const QString &path : apiPaths )
160 if ( !QFileInfo::exists( path ) )
162 QgsDebugError( QStringLiteral(
"The apis file %1 was not found" ).arg( path ) );
172 else if ( mAPISFilesList.length() == 1 && mAPISFilesList[0].right( 3 ) == QLatin1String(
"pap" ) )
174 if ( !QFileInfo::exists( mAPISFilesList[0] ) )
176 QgsDebugError( QStringLiteral(
"The apis file %1 not found" ).arg( mAPISFilesList.at( 0 ) ) );
179 mPapFile = mAPISFilesList[0];
180 apis->loadPrepared( mPapFile );
184 for (
const QString &path : std::as_const( mAPISFilesList ) )
186 if ( !QFileInfo::exists( path ) )
188 QgsDebugError( QStringLiteral(
"The apis file %1 was not found" ).arg( path ) );
198 pyLexer->setAPIs( apis.release() );
202 const int threshold = settings.
value( QStringLiteral(
"pythonConsole/autoCompThreshold" ), 2 ).toInt();
203 setAutoCompletionThreshold( threshold );
204 if ( !settings.
value(
"pythonConsole/autoCompleteEnabled",
true ).toBool() )
206 setAutoCompletionSource( AcsNone );
210 const QString autoCompleteSource = settings.
value( QStringLiteral(
"pythonConsole/autoCompleteSource" ), QStringLiteral(
"fromAPI" ) ).toString();
211 if ( autoCompleteSource == QLatin1String(
"fromDoc" ) )
212 setAutoCompletionSource( AcsDocument );
213 else if ( autoCompleteSource == QLatin1String(
"fromDocAPI" ) )
214 setAutoCompletionSource( AcsAll );
216 setAutoCompletionSource( AcsAPIs );
220 setIndentationsUseTabs(
false );
221 setIndentationGuides(
true );
236 bool autoCloseBracket = settings.
value( QStringLiteral(
"/pythonConsole/autoCloseBracket" ),
true ).toBool();
237 bool autoSurround = settings.
value( QStringLiteral(
"/pythonConsole/autoSurround" ),
true ).toBool();
238 bool autoInsertImport = settings.
value( QStringLiteral(
"/pythonConsole/autoInsertImport" ),
false ).toBool();
241 const QString eText =
event->text();
243 getCursorPosition( &line, &column );
247 if ( hasSelectedText() && autoSurround )
249 if ( sCompletionPairs.contains( eText ) )
251 int startLine, startPos, endLine, endPos;
252 getSelection( &startLine, &startPos, &endLine, &endPos );
255 if ( startLine != endLine && ( eText ==
"\"" || eText ==
"'" ) )
258 QString(
"%1%1%1%2%3%3%3" ).arg( eText, selectedText(), sCompletionPairs[eText] )
260 setSelection( startLine, startPos + 3, endLine, endPos + 3 );
265 QString(
"%1%2%3" ).arg( eText, selectedText(), sCompletionPairs[eText] )
267 setSelection( startLine, startPos + 1, endLine, endPos + 1 );
272 else if ( sCompletionSingleCharacters.contains( eText ) )
274 int startLine, startPos, endLine, endPos;
275 getSelection( &startLine, &startPos, &endLine, &endPos );
277 QString(
"%1%2%1" ).arg( eText, selectedText() )
279 setSelection( startLine, startPos + 1, endLine, endPos + 1 );
289 if ( autoInsertImport && eText ==
" " )
291 const QString lineText = text( line );
292 const thread_local QRegularExpression re( QStringLiteral(
"^from [\\w.]+$" ) );
293 if ( re.match( lineText.trimmed() ).hasMatch() )
295 insert( QStringLiteral(
" import" ) );
296 setCursorPosition( line, column + 7 );
302 else if ( autoCloseBracket )
308 if ( event->key() == Qt::Key_Backspace )
310 if ( sCompletionPairs.contains( prevChar ) && sCompletionPairs[prevChar] == nextChar )
312 setSelection( line, column - 1, line, column + 1 );
313 removeSelectedText();
326 else if ( sCompletionPairs.key( eText ) !=
"" && nextChar == eText )
328 setCursorPosition( line, column + 1 );
340 && sCompletionPairs.contains( eText )
341 && ( nextChar.isEmpty() || nextChar.at( 0 ).isSpace() || nextChar ==
":" || sCompletionPairs.key( nextChar ) !=
"" )
345 if ( !( ( eText ==
"\"" || eText ==
"'" ) && prevChar == eText ) )
348 insert( sCompletionPairs[eText] );
367 const QString
formatter = settingCodeFormatter->value();
368 const int maxLineLength = settingMaxLineLength->value();
370 QString newText = string;
372 QStringList missingModules;
374 if ( settingSortImports->value() )
376 const QString defineSortImports = QStringLiteral(
377 "def __qgis_sort_imports(script):\n"
380 " except ImportError:\n"
381 " return '_ImportError'\n"
382 " options={'line_length': %1, 'profile': '%2', 'known_first_party': ['qgis', 'console', 'processing', 'plugins']}\n"
383 " return isort.code(script, **options)\n" )
384 .arg( maxLineLength )
385 .arg(
formatter == QLatin1String(
"black" ) ? QStringLiteral(
"black" ) : QString() );
389 QgsDebugError( QStringLiteral(
"Error running script: %1" ).arg( defineSortImports ) );
397 if ( result == QLatin1String(
"_ImportError" ) )
399 missingModules << QStringLiteral(
"isort" );
408 QgsDebugError( QStringLiteral(
"Error running script: %1" ).arg( script ) );
413 if (
formatter == QLatin1String(
"autopep8" ) )
415 const int level = settingAutopep8Level->value();
417 const QString defineReformat = QStringLiteral(
418 "def __qgis_reformat(script):\n"
421 " except ImportError:\n"
422 " return '_ImportError'\n"
423 " options={'aggressive': %1, 'max_line_length': %2}\n"
424 " return autopep8.fix_code(script, options=options)\n" )
426 .arg( maxLineLength );
430 QgsDebugError( QStringLiteral(
"Error running script: %1" ).arg( defineReformat ) );
438 if ( result == QLatin1String(
"_ImportError" ) )
440 missingModules << QStringLiteral(
"autopep8" );
449 QgsDebugError( QStringLiteral(
"Error running script: %1" ).arg( script ) );
453 else if (
formatter == QLatin1String(
"black" ) )
455 const bool normalize = settingBlackNormalizeQuotes->value();
463 const QString defineReformat = QStringLiteral(
464 "def __qgis_reformat(script):\n"
467 " except ImportError:\n"
468 " return '_ImportError'\n"
469 " options={'string_normalization': %1, 'line_length': %2}\n"
470 " return black.format_str(script, mode=black.Mode(**options))\n" )
472 .arg( maxLineLength );
476 QgsDebugError( QStringLiteral(
"Error running script: %1" ).arg( defineReformat ) );
484 if ( result == QLatin1String(
"_ImportError" ) )
486 missingModules << QStringLiteral(
"black" );
495 QgsDebugError( QStringLiteral(
"Error running script: %1" ).arg( script ) );
500 if ( !missingModules.empty() )
502 if ( missingModules.size() == 1 )
508 const QString modules = missingModules.join( QLatin1String(
", " ) );
520 QAction *pyQgisHelpAction =
new QAction(
522 tr(
"Search Selection in PyQGIS Documentation" ),
524 pyQgisHelpAction->setEnabled( hasSelectedText() );
527 menu->addSeparator();
528 menu->addAction( pyQgisHelpAction );
533 switch ( autoCompletionSource() )
536 autoCompleteFromDocument();
540 autoCompleteFromAPIs();
544 autoCompleteFromAll();
554 mAPISFilesList = filenames;
561 QgsDebugMsgLevel( QStringLiteral(
"The script file: %1" ).arg( script ), 2 );
562 QFile file( script );
563 if ( !file.open( QIODevice::ReadOnly ) )
568 QTextStream in( &file );
569#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
570 in.setCodec(
"UTF-8" );
573 setText( in.readAll().trimmed() );
588 if ( position >= length() && position > 0 )
590 long style = SendScintilla( QsciScintillaBase::SCI_GETSTYLEAT, position - 1 );
591 return style == QsciLexerPython::Comment
592 || style == QsciLexerPython::TripleSingleQuotedString
593 || style == QsciLexerPython::TripleDoubleQuotedString
594 || style == QsciLexerPython::TripleSingleQuotedFString
595 || style == QsciLexerPython::TripleDoubleQuotedFString
596 || style == QsciLexerPython::UnclosedString;
600 long style = SendScintilla( QsciScintillaBase::SCI_GETSTYLEAT, position );
601 return style == QsciLexerPython::Comment
602 || style == QsciLexerPython::DoubleQuotedString
603 || style == QsciLexerPython::SingleQuotedString
604 || style == QsciLexerPython::TripleSingleQuotedString
605 || style == QsciLexerPython::TripleDoubleQuotedString
606 || style == QsciLexerPython::CommentBlock
607 || style == QsciLexerPython::UnclosedString
608 || style == QsciLexerPython::DoubleQuotedFString
609 || style == QsciLexerPython::SingleQuotedFString
610 || style == QsciLexerPython::TripleSingleQuotedFString
611 || style == QsciLexerPython::TripleDoubleQuotedFString;
622 return text( position - 1, position );
628 if ( position >= length() )
632 return text( position, position + 1 );
659 const QString originalText = text();
661 const QString defineCheckSyntax = QStringLiteral(
662 "def __check_syntax(script):\n"
664 " compile(script.encode('utf-8'), '', 'exec')\n"
665 " except SyntaxError as detail:\n"
666 " eline = detail.lineno or 1\n"
668 " ecolumn = detail.offset or 1\n"
669 " edescr = detail.msg\n"
670 " return '!!!!'.join([str(eline), str(ecolumn), edescr])\n"
675 QgsDebugError( QStringLiteral(
"Error running script: %1" ).arg( defineCheckSyntax ) );
683 if ( result.size() == 0 )
689 const QStringList parts = result.split( QStringLiteral(
"!!!!" ) );
690 if ( parts.size() == 3 )
692 const int line = parts.at( 0 ).toInt();
693 const int column = parts.at( 1 ).toInt();
695 setCursorPosition( line, column - 1 );
696 ensureLineVisible( line );
703 QgsDebugError( QStringLiteral(
"Error running script: %1" ).arg( script ) );
710 if ( !hasSelectedText() )
713 QString text = selectedText();
714 text = text.replace( QLatin1String(
">>> " ), QString() ).replace( QLatin1String(
"... " ), QString() ).trimmed();
715 const QString version = QString(
Qgis::version() ).split(
'.' ).mid( 0, 2 ).join(
'.' );
716 QDesktopServices::openUrl( QUrl( QStringLiteral(
"https://qgis.org/pyqgis/%1/search.html?q=%2" ).arg( version, text ) ) );
727 int startLine, startPos, endLine, endPos;
728 if ( hasSelectedText() )
730 getSelection( &startLine, &startPos, &endLine, &endPos );
734 getCursorPosition( &startLine, &startPos );
740 bool allEmpty =
true;
741 bool allCommented =
true;
742 int minIndentation = -1;
743 for (
int line = startLine; line <= endLine; line++ )
745 const QString stripped = text( line ).trimmed();
746 if ( !stripped.isEmpty() )
749 if ( !stripped.startsWith(
'#' ) )
751 allCommented =
false;
753 if ( minIndentation == -1 || minIndentation > indentation( line ) )
755 minIndentation = indentation( line );
769 for (
int line = startLine; line <= endLine; line++ )
771 const QString stripped = text( line ).trimmed();
774 if ( stripped.isEmpty() )
781 insertAt( QStringLiteral(
"# " ), line, minIndentation );
786 if ( !stripped.startsWith(
'#' ) )
790 if ( stripped.startsWith( QLatin1String(
"# " ) ) )
798 setSelection( line, indentation( line ), line, indentation( line ) + delta );
799 removeSelectedText();
804 setSelection( startLine, startPos - delta, endLine, endPos - delta );
811QgsQsciLexerPython::QgsQsciLexerPython( QObject *parent )
812 : QsciLexerPython( parent )
817const char *QgsQsciLexerPython::keywords(
int set )
const
821 return "True False and as assert break class continue def del elif else except "
822 "finally for from global if import in is lambda None not or pass "
823 "raise return try while with yield async await nonlocal";
826 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)