29#include <QDesktopServices>
39#include <Qsci/qscilexerpython.h>
41#include "moc_qgscodeeditorpython.cpp"
43using namespace Qt::StringLiterals;
45const QMap<QString, QString> QgsCodeEditorPython::sCompletionPairs { {
"(",
")" }, {
"[",
"]" }, {
"{",
"}" }, {
"'",
"'" }, {
"\"",
"\"" } };
46const QStringList QgsCodeEditorPython::sCompletionSingleCharacters {
"`",
"*" };
51 =
new QgsSettingsEntryBool( u
"sort-imports"_s, sTreePythonCodeEditor,
true, u
"Whether imports should be sorted when auto-formatting code"_s );
54 =
new QgsSettingsEntryBool( u
"black-normalize-quotes"_s, sTreePythonCodeEditor,
true, u
"Whether quotes should be normalized when auto-formatting code using black"_s );
56 =
new QgsSettingsEntryString( u
"external-editor"_s, sTreePythonCodeEditor, QString(), u
"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."_s );
64 , mAPISFilesList( filenames )
93 setEdgeMode( QsciScintilla::EdgeLine );
94 setEdgeColumn( settingMaxLineLength->value() );
97 setWhitespaceVisibility( QsciScintilla::WsVisibleAfterIndent );
99 SendScintilla( QsciScintillaBase::SCI_SETPROPERTY,
"highlight.current.word",
"1" );
104 QsciLexerPython *pyLexer =
new QgsQsciLexerPython(
this );
106 pyLexer->setIndentationWarning( QsciLexerPython::Inconsistent );
107 pyLexer->setFoldComments(
true );
108 pyLexer->setFoldQuotes(
true );
110 pyLexer->setDefaultFont( font );
113 pyLexer->setFont( font, -1 );
115 font.setItalic(
true );
116 pyLexer->setFont( font, QsciLexerPython::Comment );
117 pyLexer->setFont( font, QsciLexerPython::CommentBlock );
119 font.setItalic(
false );
120 font.setBold(
true );
121 pyLexer->setFont( font, QsciLexerPython::SingleQuotedString );
122 pyLexer->setFont( font, QsciLexerPython::DoubleQuotedString );
124 pyLexer->setColor(
defaultColor, QsciLexerPython::Default );
145 const int threshold = settings.
value( u
"pythonConsole/autoCompThreshold"_s, 2 ).toInt();
146 setAutoCompletionThreshold( threshold );
147 if ( !settings.
value(
"pythonConsole/autoCompleteEnabled",
true ).toBool() )
149 setAutoCompletionSource( AcsNone );
153 const QString autoCompleteSource = settings.
value( u
"pythonConsole/autoCompleteSource"_s, u
"fromAPI"_s ).toString();
154 if ( autoCompleteSource ==
"fromDoc"_L1 )
155 setAutoCompletionSource( AcsDocument );
156 else if ( autoCompleteSource ==
"fromDocAPI"_L1 )
157 setAutoCompletionSource( AcsAll );
159 setAutoCompletionSource( AcsAPIs );
163 setIndentationsUseTabs(
false );
164 setIndentationGuides(
true );
168 mInitializedLexer =
false;
170 deferredInitializeLexer();
173void QgsCodeEditorPython::deferredInitializeLexer()
176 auto apis = std::make_unique<QsciAPIs>( lexer() );
178 if ( mAPISFilesList.isEmpty() )
180 if ( settings.
value( u
"pythonConsole/preloadAPI"_s,
true ).toBool() )
183 apis->loadPrepared( mPapFile );
185 else if ( settings.
value( u
"pythonConsole/usePreparedAPIFile"_s,
false ).toBool() )
187 apis->loadPrepared( settings.
value( u
"pythonConsole/preparedAPIFile"_s ).toString() );
191 const QStringList apiPaths = settings.
value( u
"pythonConsole/userAPI"_s ).toStringList();
192 for (
const QString &path : apiPaths )
194 if ( !QFileInfo::exists( path ) )
196 QgsDebugError( u
"The apis file %1 was not found"_s.arg( path ) );
206 else if ( mAPISFilesList.length() == 1 && mAPISFilesList[0].right( 3 ) ==
"pap"_L1 )
208 if ( !QFileInfo::exists( mAPISFilesList[0] ) )
210 QgsDebugError( u
"The apis file %1 not found"_s.arg( mAPISFilesList.at( 0 ) ) );
213 mPapFile = mAPISFilesList[0];
214 apis->loadPrepared( mPapFile );
218 for (
const QString &path : std::as_const( mAPISFilesList ) )
220 if ( !QFileInfo::exists( path ) )
222 QgsDebugError( u
"The apis file %1 was not found"_s.arg( path ) );
231 lexer()->setAPIs( apis.release() );
233 mInitializedLexer =
true;
246 bool autoCloseBracket = settings.
value( u
"/pythonConsole/autoCloseBracket"_s,
true ).toBool();
247 bool autoSurround = settings.
value( u
"/pythonConsole/autoSurround"_s,
true ).toBool();
248 bool autoInsertImport = settings.
value( u
"/pythonConsole/autoInsertImport"_s,
false ).toBool();
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 ==
"'" ) )
267 replaceSelectedText( QString(
"%1%1%1%2%3%3%3" ).arg( eText, selectedText(), sCompletionPairs[eText] ) );
268 setSelection( startLine, startPos + 3, endLine, endPos + 3 );
272 replaceSelectedText( QString(
"%1%2%3" ).arg( eText, selectedText(), sCompletionPairs[eText] ) );
273 setSelection( startLine, startPos + 1, endLine, endPos + 1 );
278 else if ( sCompletionSingleCharacters.contains( eText ) )
280 int startLine, startPos, endLine, endPos;
281 getSelection( &startLine, &startPos, &endLine, &endPos );
282 replaceSelectedText( 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( u
"^from [\\w.]+$"_s );
297 if ( re.match( lineText.trimmed() ).hasMatch() )
299 insert( u
" import"_s );
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();
330 else if ( sCompletionPairs.key( eText ) !=
"" && nextChar == eText )
332 setCursorPosition( line, column + 1 );
344 && sCompletionPairs.contains( eText )
345 && ( nextChar.isEmpty() || nextChar.at( 0 ).isSpace() || nextChar ==
":" || sCompletionPairs.key( nextChar ) !=
"" ) )
348 if ( !( ( eText ==
"\"" || eText ==
"'" ) && prevChar == eText ) )
351 insert( sCompletionPairs[eText] );
365 if ( !mInitializedLexer )
367 deferredInitializeLexer();
369 QgsCodeEditor::showEvent(
event );
379 const QString formatter = settingCodeFormatter->value();
380 const int maxLineLength = settingMaxLineLength->value();
382 QString newText = string;
384 QStringList missingModules;
386 if ( settingSortImports->value() )
388 const QString defineSortImports = QStringLiteral(
389 "def __qgis_sort_imports(script):\n"
392 " except ImportError:\n"
393 " return '_ImportError'\n"
394 " options={'line_length': %1, 'profile': '%2', 'known_first_party': ['qgis', 'console', 'processing', 'plugins']}\n"
395 " return isort.code(script, **options)\n"
397 .arg( maxLineLength )
398 .arg( formatter ==
"black"_L1 ? u
"black"_s : QString() );
402 QgsDebugError( u
"Error running script: %1"_s.arg( defineSortImports ) );
410 if ( result ==
"_ImportError"_L1 )
412 missingModules << u
"isort"_s;
421 QgsDebugError( u
"Error running script: %1"_s.arg( script ) );
426 if ( formatter ==
"autopep8"_L1 )
428 const int level = settingAutopep8Level->value();
430 const QString defineReformat = QStringLiteral(
431 "def __qgis_reformat(script):\n"
434 " except ImportError:\n"
435 " return '_ImportError'\n"
436 " options={'aggressive': %1, 'max_line_length': %2}\n"
437 " return autopep8.fix_code(script, options=options)\n"
440 .arg( maxLineLength );
444 QgsDebugError( u
"Error running script: %1"_s.arg( defineReformat ) );
452 if ( result ==
"_ImportError"_L1 )
454 missingModules << u
"autopep8"_s;
463 QgsDebugError( u
"Error running script: %1"_s.arg( script ) );
467 else if ( formatter ==
"black"_L1 )
469 const bool normalize = settingBlackNormalizeQuotes->value();
477 const QString defineReformat = QStringLiteral(
478 "def __qgis_reformat(script):\n"
481 " except ImportError:\n"
482 " return '_ImportError'\n"
483 " options={'string_normalization': %1, 'line_length': %2}\n"
484 " return black.format_str(script, mode=black.Mode(**options))\n"
487 .arg( maxLineLength );
491 QgsDebugError( u
"Error running script: %1"_s.arg( defineReformat ) );
499 if ( result ==
"_ImportError"_L1 )
501 missingModules << u
"black"_s;
510 QgsDebugError( u
"Error running script: %1"_s.arg( script ) );
515 if ( !missingModules.empty() )
517 if ( missingModules.size() == 1 )
523 const QString modules = missingModules.join(
", "_L1 );
535 QString text = selectedText();
536 if ( text.isEmpty() )
538 text = wordAtPoint( mapFromGlobal( QCursor::pos() ) );
540 if ( text.isEmpty() )
545 QAction *pyQgisHelpAction =
new QAction(
QgsApplication::getThemeIcon( u
"console/iconHelpConsole.svg"_s ), tr(
"Search Selection in PyQGIS Documentation" ), menu );
547 pyQgisHelpAction->setEnabled( hasSelectedText() );
548 pyQgisHelpAction->setShortcut( QKeySequence::StandardKey::HelpContents );
549 connect( pyQgisHelpAction, &QAction::triggered,
this, [text,
this] {
showApiDocumentation( text ); } );
551 menu->addSeparator();
552 menu->addAction( pyQgisHelpAction );
557 switch ( autoCompletionSource() )
560 autoCompleteFromDocument();
564 autoCompleteFromAPIs();
568 autoCompleteFromAll();
578 mAPISFilesList = filenames;
586 QFile file( script );
587 if ( !file.open( QIODevice::ReadOnly ) )
592 QTextStream in( &file );
593 setText( in.readAll().trimmed() );
608 if ( position >= length() && position > 0 )
610 long style = SendScintilla( QsciScintillaBase::SCI_GETSTYLEAT, position - 1 );
611 return style == QsciLexerPython::Comment
612 || style == QsciLexerPython::TripleSingleQuotedString
613 || style == QsciLexerPython::TripleDoubleQuotedString
614 || style == QsciLexerPython::TripleSingleQuotedFString
615 || style == QsciLexerPython::TripleDoubleQuotedFString
616 || style == QsciLexerPython::UnclosedString;
620 long style = SendScintilla( QsciScintillaBase::SCI_GETSTYLEAT, position );
621 return style == QsciLexerPython::Comment
622 || style == QsciLexerPython::DoubleQuotedString
623 || style == QsciLexerPython::SingleQuotedString
624 || style == QsciLexerPython::TripleSingleQuotedString
625 || style == QsciLexerPython::TripleDoubleQuotedString
626 || style == QsciLexerPython::CommentBlock
627 || style == QsciLexerPython::UnclosedString
628 || style == QsciLexerPython::DoubleQuotedFString
629 || style == QsciLexerPython::SingleQuotedFString
630 || style == QsciLexerPython::TripleSingleQuotedFString
631 || style == QsciLexerPython::TripleDoubleQuotedFString;
642 return text( position - 1, position );
648 if ( position >= length() )
652 return text( position, position + 1 );
679 const QString originalText = text();
681 const QString defineCheckSyntax = QStringLiteral(
682 "def __check_syntax(script):\n"
684 " compile(script.encode('utf-8'), '', 'exec')\n"
685 " except SyntaxError as detail:\n"
686 " eline = detail.lineno or 1\n"
688 " ecolumn = detail.offset or 1\n"
689 " edescr = detail.msg\n"
690 " return '!!!!'.join([str(eline), str(ecolumn), edescr])\n"
696 QgsDebugError( u
"Error running script: %1"_s.arg( defineCheckSyntax ) );
704 if ( result.size() == 0 )
710 const QStringList parts = result.split( u
"!!!!"_s );
711 if ( parts.size() == 3 )
713 const int line = parts.at( 0 ).toInt();
714 const int column = parts.at( 1 ).toInt();
716 setCursorPosition( line, column - 1 );
717 ensureLineVisible( line );
724 QgsDebugError( u
"Error running script: %1"_s.arg( script ) );
736 QString searchText = text;
737 searchText = searchText.replace(
">>> "_L1, QString() ).replace(
"... "_L1, QString() ).trimmed();
739 QRegularExpression qtExpression(
"^Q[A-Z][a-zA-Z]" );
741 if ( qtExpression.match( searchText ).hasMatch() )
743 const QString qtVersion = QString( qVersion() ).split(
'.' ).mid( 0, 2 ).join(
'.' );
744 QString baseUrl = QString(
"https://doc.qt.io/qt-%1" ).arg( qtVersion );
745 QDesktopServices::openUrl( QUrl( u
"%1/%2.html"_s.arg( baseUrl, searchText.toLower() ) ) );
748 const QString qgisVersion = QString(
Qgis::version() ).split(
'.' ).mid( 0, 2 ).join(
'.' );
749 if ( searchText.isEmpty() )
751 QDesktopServices::openUrl( QUrl( u
"https://qgis.org/pyqgis/%1/"_s.arg( qgisVersion ) ) );
755 QDesktopServices::openUrl( QUrl( u
"https://qgis.org/pyqgis/%1/search.html?q=%2"_s.arg( qgisVersion, searchText ) ) );
768QgsQsciLexerPython::QgsQsciLexerPython( QObject *parent )
769 : QsciLexerPython( parent )
772const char *QgsQsciLexerPython::keywords(
int set )
const
776 return "True False and as assert break class continue def del elif else except "
777 "finally for from global if import in is lambda None not or pass "
778 "raise return try while with yield async await nonlocal";
781 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.
DocumentationBrowser
Documentation API browser.
@ DeveloperToolsPanel
Embedded webview in the DevTools panel.
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 showEvent(QShowEvent *event) override
virtual void showApiDocumentation(const QString &item)
Displays the given text in the official APIs (PyQGIS, C++ QGIS or Qt) documentation.
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.
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.
QgsCodeEditor::Mode mode() const
Returns the code editor mode.
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 setText(const QString &text) override
void runPostLexerConfigurationTasks()
Performs tasks which must be run after a lexer has been set for the widget.
bool event(QEvent *event) override
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.
QgsCodeEditor(QWidget *parent=nullptr, const QString &title=QString(), bool folding=false, bool margin=false, QgsCodeEditor::Flags flags=QgsCodeEditor::Flags(), QgsCodeEditor::Mode mode=QgsCodeEditor::Mode::ScriptEditor)
Construct a new code editor.
void clearWarnings()
Clears all warning messages from the editor.
void helpRequested(const QString &word)
Emitted when documentation was requested for the specified word.
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.
void toggleLineComments(const QString &commentPrefix)
Toggles comment for selected lines with the given comment prefix.
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.
A template class for enum and flag settings entry.
An integer settings entry.
Stores settings for use within QGIS.
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)