29#include <QDesktopServices>
39#include <Qsci/qscilexerpython.h>
41#include "moc_qgscodeeditorpython.cpp"
43using namespace Qt::StringLiterals;
45const QMap<QString, QString> QgsCodeEditorPython::sCompletionPairs {
52const QStringList QgsCodeEditorPython::sCompletionSingleCharacters {
"`",
"*" };
56const QgsSettingsEntryBool *QgsCodeEditorPython::settingSortImports =
new QgsSettingsEntryBool( u
"sort-imports"_s, sTreePythonCodeEditor,
true, u
"Whether imports should be sorted when auto-formatting code"_s );
58const QgsSettingsEntryBool *QgsCodeEditorPython::settingBlackNormalizeQuotes =
new QgsSettingsEntryBool( u
"black-normalize-quotes"_s, sTreePythonCodeEditor,
true, u
"Whether quotes should be normalized when auto-formatting code using black"_s );
59const QgsSettingsEntryString *QgsCodeEditorPython::settingExternalPythonEditorCommand =
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 );
66 , mAPISFilesList( filenames )
95 setEdgeMode( QsciScintilla::EdgeLine );
96 setEdgeColumn( settingMaxLineLength->value() );
99 setWhitespaceVisibility( QsciScintilla::WsVisibleAfterIndent );
101 SendScintilla( QsciScintillaBase::SCI_SETPROPERTY,
"highlight.current.word",
"1" );
106 QsciLexerPython *pyLexer =
new QgsQsciLexerPython(
this );
108 pyLexer->setIndentationWarning( QsciLexerPython::Inconsistent );
109 pyLexer->setFoldComments(
true );
110 pyLexer->setFoldQuotes(
true );
112 pyLexer->setDefaultFont( font );
115 pyLexer->setFont( font, -1 );
117 font.setItalic(
true );
118 pyLexer->setFont( font, QsciLexerPython::Comment );
119 pyLexer->setFont( font, QsciLexerPython::CommentBlock );
121 font.setItalic(
false );
122 font.setBold(
true );
123 pyLexer->setFont( font, QsciLexerPython::SingleQuotedString );
124 pyLexer->setFont( font, QsciLexerPython::DoubleQuotedString );
126 pyLexer->setColor(
defaultColor, QsciLexerPython::Default );
144 auto apis = std::make_unique<QsciAPIs>( pyLexer );
147 if ( mAPISFilesList.isEmpty() )
149 if ( settings.
value( u
"pythonConsole/preloadAPI"_s,
true ).toBool() )
152 apis->loadPrepared( mPapFile );
154 else if ( settings.
value( u
"pythonConsole/usePreparedAPIFile"_s,
false ).toBool() )
156 apis->loadPrepared( settings.
value( u
"pythonConsole/preparedAPIFile"_s ).toString() );
160 const QStringList apiPaths = settings.
value( u
"pythonConsole/userAPI"_s ).toStringList();
161 for (
const QString &path : apiPaths )
163 if ( !QFileInfo::exists( path ) )
165 QgsDebugError( u
"The apis file %1 was not found"_s.arg( path ) );
175 else if ( mAPISFilesList.length() == 1 && mAPISFilesList[0].right( 3 ) ==
"pap"_L1 )
177 if ( !QFileInfo::exists( mAPISFilesList[0] ) )
179 QgsDebugError( u
"The apis file %1 not found"_s.arg( mAPISFilesList.at( 0 ) ) );
182 mPapFile = mAPISFilesList[0];
183 apis->loadPrepared( mPapFile );
187 for (
const QString &path : std::as_const( mAPISFilesList ) )
189 if ( !QFileInfo::exists( path ) )
191 QgsDebugError( u
"The apis file %1 was not found"_s.arg( path ) );
200 pyLexer->setAPIs( apis.release() );
204 const int threshold = settings.
value( u
"pythonConsole/autoCompThreshold"_s, 2 ).toInt();
205 setAutoCompletionThreshold( threshold );
206 if ( !settings.
value(
"pythonConsole/autoCompleteEnabled",
true ).toBool() )
208 setAutoCompletionSource( AcsNone );
212 const QString autoCompleteSource = settings.
value( u
"pythonConsole/autoCompleteSource"_s, u
"fromAPI"_s ).toString();
213 if ( autoCompleteSource ==
"fromDoc"_L1 )
214 setAutoCompletionSource( AcsDocument );
215 else if ( autoCompleteSource ==
"fromDocAPI"_L1 )
216 setAutoCompletionSource( AcsAll );
218 setAutoCompletionSource( AcsAPIs );
222 setIndentationsUseTabs(
false );
223 setIndentationGuides(
true );
238 bool autoCloseBracket = settings.
value( u
"/pythonConsole/autoCloseBracket"_s,
true ).toBool();
239 bool autoSurround = settings.
value( u
"/pythonConsole/autoSurround"_s,
true ).toBool();
240 bool autoInsertImport = settings.
value( u
"/pythonConsole/autoInsertImport"_s,
false ).toBool();
243 const QString eText =
event->text();
245 getCursorPosition( &line, &column );
249 if ( hasSelectedText() && autoSurround )
251 if ( sCompletionPairs.contains( eText ) )
253 int startLine, startPos, endLine, endPos;
254 getSelection( &startLine, &startPos, &endLine, &endPos );
257 if ( startLine != endLine && ( eText ==
"\"" || eText ==
"'" ) )
260 QString(
"%1%1%1%2%3%3%3" ).arg( eText, selectedText(), sCompletionPairs[eText] )
262 setSelection( startLine, startPos + 3, endLine, endPos + 3 );
267 QString(
"%1%2%3" ).arg( eText, selectedText(), sCompletionPairs[eText] )
269 setSelection( startLine, startPos + 1, endLine, endPos + 1 );
274 else if ( sCompletionSingleCharacters.contains( eText ) )
276 int startLine, startPos, endLine, endPos;
277 getSelection( &startLine, &startPos, &endLine, &endPos );
279 QString(
"%1%2%1" ).arg( eText, selectedText() )
281 setSelection( startLine, startPos + 1, endLine, endPos + 1 );
291 if ( autoInsertImport && eText ==
" " )
293 const QString lineText = text( line );
294 const thread_local QRegularExpression re( u
"^from [\\w.]+$"_s );
295 if ( re.match( lineText.trimmed() ).hasMatch() )
297 insert( u
" import"_s );
298 setCursorPosition( line, column + 7 );
304 else if ( autoCloseBracket )
310 if (
event->key() == Qt::Key_Backspace )
312 if ( sCompletionPairs.contains( prevChar ) && sCompletionPairs[prevChar] == nextChar )
314 setSelection( line, column - 1, line, column + 1 );
315 removeSelectedText();
328 else if ( sCompletionPairs.key( eText ) !=
"" && nextChar == eText )
330 setCursorPosition( line, column + 1 );
342 && sCompletionPairs.contains( eText )
343 && ( nextChar.isEmpty() || nextChar.at( 0 ).isSpace() || nextChar ==
":" || sCompletionPairs.key( nextChar ) !=
"" ) )
346 if ( !( ( eText ==
"\"" || eText ==
"'" ) && prevChar == eText ) )
349 insert( sCompletionPairs[eText] );
368 const QString formatter = settingCodeFormatter->value();
369 const int maxLineLength = settingMaxLineLength->value();
371 QString newText = string;
373 QStringList missingModules;
375 if ( settingSortImports->value() )
377 const QString defineSortImports = QStringLiteral(
378 "def __qgis_sort_imports(script):\n"
381 " except ImportError:\n"
382 " return '_ImportError'\n"
383 " options={'line_length': %1, 'profile': '%2', 'known_first_party': ['qgis', 'console', 'processing', 'plugins']}\n"
384 " return isort.code(script, **options)\n"
386 .arg( maxLineLength )
387 .arg( formatter ==
"black"_L1 ? u
"black"_s : QString() );
391 QgsDebugError( u
"Error running script: %1"_s.arg( defineSortImports ) );
399 if ( result ==
"_ImportError"_L1 )
401 missingModules << u
"isort"_s;
410 QgsDebugError( u
"Error running script: %1"_s.arg( script ) );
415 if ( formatter ==
"autopep8"_L1 )
417 const int level = settingAutopep8Level->value();
419 const QString defineReformat = QStringLiteral(
420 "def __qgis_reformat(script):\n"
423 " except ImportError:\n"
424 " return '_ImportError'\n"
425 " options={'aggressive': %1, 'max_line_length': %2}\n"
426 " return autopep8.fix_code(script, options=options)\n"
429 .arg( maxLineLength );
433 QgsDebugError( u
"Error running script: %1"_s.arg( defineReformat ) );
441 if ( result ==
"_ImportError"_L1 )
443 missingModules << u
"autopep8"_s;
452 QgsDebugError( u
"Error running script: %1"_s.arg( script ) );
456 else if ( formatter ==
"black"_L1 )
458 const bool normalize = settingBlackNormalizeQuotes->value();
466 const QString defineReformat = QStringLiteral(
467 "def __qgis_reformat(script):\n"
470 " except ImportError:\n"
471 " return '_ImportError'\n"
472 " options={'string_normalization': %1, 'line_length': %2}\n"
473 " return black.format_str(script, mode=black.Mode(**options))\n"
476 .arg( maxLineLength );
480 QgsDebugError( u
"Error running script: %1"_s.arg( defineReformat ) );
488 if ( result ==
"_ImportError"_L1 )
490 missingModules << u
"black"_s;
499 QgsDebugError( u
"Error running script: %1"_s.arg( script ) );
504 if ( !missingModules.empty() )
506 if ( missingModules.size() == 1 )
512 const QString modules = missingModules.join(
", "_L1 );
524 QString text = selectedText();
525 if ( text.isEmpty() )
527 text = wordAtPoint( mapFromGlobal( QCursor::pos() ) );
529 if ( text.isEmpty() )
534 QAction *pyQgisHelpAction =
new QAction(
536 tr(
"Search Selection in PyQGIS Documentation" ),
540 pyQgisHelpAction->setEnabled( hasSelectedText() );
541 pyQgisHelpAction->setShortcut( QKeySequence::StandardKey::HelpContents );
542 connect( pyQgisHelpAction, &QAction::triggered,
this, [text,
this] {
showApiDocumentation( text ); } );
544 menu->addSeparator();
545 menu->addAction( pyQgisHelpAction );
550 switch ( autoCompletionSource() )
553 autoCompleteFromDocument();
557 autoCompleteFromAPIs();
561 autoCompleteFromAll();
571 mAPISFilesList = filenames;
579 QFile file( script );
580 if ( !file.open( QIODevice::ReadOnly ) )
585 QTextStream in( &file );
586 setText( in.readAll().trimmed() );
601 if ( position >= length() && position > 0 )
603 long style = SendScintilla( QsciScintillaBase::SCI_GETSTYLEAT, position - 1 );
604 return style == QsciLexerPython::Comment
605 || style == QsciLexerPython::TripleSingleQuotedString
606 || style == QsciLexerPython::TripleDoubleQuotedString
607 || style == QsciLexerPython::TripleSingleQuotedFString
608 || style == QsciLexerPython::TripleDoubleQuotedFString
609 || style == QsciLexerPython::UnclosedString;
613 long style = SendScintilla( QsciScintillaBase::SCI_GETSTYLEAT, position );
614 return style == QsciLexerPython::Comment
615 || style == QsciLexerPython::DoubleQuotedString
616 || style == QsciLexerPython::SingleQuotedString
617 || style == QsciLexerPython::TripleSingleQuotedString
618 || style == QsciLexerPython::TripleDoubleQuotedString
619 || style == QsciLexerPython::CommentBlock
620 || style == QsciLexerPython::UnclosedString
621 || style == QsciLexerPython::DoubleQuotedFString
622 || style == QsciLexerPython::SingleQuotedFString
623 || style == QsciLexerPython::TripleSingleQuotedFString
624 || style == QsciLexerPython::TripleDoubleQuotedFString;
635 return text( position - 1, position );
641 if ( position >= length() )
645 return text( position, position + 1 );
672 const QString originalText = text();
674 const QString defineCheckSyntax = QStringLiteral(
675 "def __check_syntax(script):\n"
677 " compile(script.encode('utf-8'), '', 'exec')\n"
678 " except SyntaxError as detail:\n"
679 " eline = detail.lineno or 1\n"
681 " ecolumn = detail.offset or 1\n"
682 " edescr = detail.msg\n"
683 " return '!!!!'.join([str(eline), str(ecolumn), edescr])\n"
689 QgsDebugError( u
"Error running script: %1"_s.arg( defineCheckSyntax ) );
697 if ( result.size() == 0 )
703 const QStringList parts = result.split( u
"!!!!"_s );
704 if ( parts.size() == 3 )
706 const int line = parts.at( 0 ).toInt();
707 const int column = parts.at( 1 ).toInt();
709 setCursorPosition( line, column - 1 );
710 ensureLineVisible( line );
717 QgsDebugError( u
"Error running script: %1"_s.arg( script ) );
729 QString searchText = text;
730 searchText = searchText.replace(
">>> "_L1, QString() ).replace(
"... "_L1, QString() ).trimmed();
732 QRegularExpression qtExpression(
"^Q[A-Z][a-zA-Z]" );
734 if ( qtExpression.match( searchText ).hasMatch() )
736 const QString qtVersion = QString( qVersion() ).split(
'.' ).mid( 0, 2 ).join(
'.' );
737 QString baseUrl = QString(
"https://doc.qt.io/qt-%1" ).arg( qtVersion );
738 QDesktopServices::openUrl( QUrl( u
"%1/%2.html"_s.arg( baseUrl, searchText.toLower() ) ) );
741 const QString qgisVersion = QString(
Qgis::version() ).split(
'.' ).mid( 0, 2 ).join(
'.' );
742 if ( searchText.isEmpty() )
744 QDesktopServices::openUrl( QUrl( u
"https://qgis.org/pyqgis/%1/"_s.arg( qgisVersion ) ) );
748 QDesktopServices::openUrl( QUrl( u
"https://qgis.org/pyqgis/%1/search.html?q=%2"_s.arg( qgisVersion, searchText ) ) );
761QgsQsciLexerPython::QgsQsciLexerPython( QObject *parent )
762 : QsciLexerPython( parent )
766const char *QgsQsciLexerPython::keywords(
int set )
const
770 return "True False and as assert break class continue def del elif else except "
771 "finally for from global if import in is lambda None not or pass "
772 "raise return try while with yield async await nonlocal";
775 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.
@ 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.
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)