29#include <QDesktopServices>
39#include <Qsci/qscilexerpython.h>
41#include "moc_qgscodeeditorpython.cpp"
43const QMap<QString, QString> QgsCodeEditorPython::sCompletionPairs {
50const QStringList QgsCodeEditorPython::sCompletionSingleCharacters {
"`",
"*" };
52const QgsSettingsEntryString *QgsCodeEditorPython::settingCodeFormatter =
new QgsSettingsEntryString( QStringLiteral(
"formatter" ), sTreePythonCodeEditor, QStringLiteral(
"autopep8" ), QStringLiteral(
"Python code autoformatter" ) );
54const QgsSettingsEntryBool *QgsCodeEditorPython::settingSortImports =
new QgsSettingsEntryBool( QStringLiteral(
"sort-imports" ), sTreePythonCodeEditor,
true, QStringLiteral(
"Whether imports should be sorted when auto-formatting code" ) );
56const QgsSettingsEntryBool *QgsCodeEditorPython::settingBlackNormalizeQuotes =
new QgsSettingsEntryBool( QStringLiteral(
"black-normalize-quotes" ), sTreePythonCodeEditor,
true, QStringLiteral(
"Whether quotes should be normalized when auto-formatting code using black" ) );
57const 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 )
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 );
142 auto apis = std::make_unique<QsciAPIs>( pyLexer );
145 if ( mAPISFilesList.isEmpty() )
147 if ( settings.
value( QStringLiteral(
"pythonConsole/preloadAPI" ),
true ).toBool() )
150 apis->loadPrepared( mPapFile );
152 else if ( settings.
value( QStringLiteral(
"pythonConsole/usePreparedAPIFile" ),
false ).toBool() )
154 apis->loadPrepared( settings.
value( QStringLiteral(
"pythonConsole/preparedAPIFile" ) ).toString() );
158 const QStringList apiPaths = settings.
value( QStringLiteral(
"pythonConsole/userAPI" ) ).toStringList();
159 for (
const QString &path : apiPaths )
161 if ( !QFileInfo::exists( path ) )
163 QgsDebugError( QStringLiteral(
"The apis file %1 was not found" ).arg( path ) );
173 else if ( mAPISFilesList.length() == 1 && mAPISFilesList[0].right( 3 ) == QLatin1String(
"pap" ) )
175 if ( !QFileInfo::exists( mAPISFilesList[0] ) )
177 QgsDebugError( QStringLiteral(
"The apis file %1 not found" ).arg( mAPISFilesList.at( 0 ) ) );
180 mPapFile = mAPISFilesList[0];
181 apis->loadPrepared( mPapFile );
185 for (
const QString &path : std::as_const( mAPISFilesList ) )
187 if ( !QFileInfo::exists( path ) )
189 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 ) !=
"" ) )
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"
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"
427 .arg( maxLineLength );
431 QgsDebugError( QStringLiteral(
"Error running script: %1" ).arg( defineReformat ) );
439 if ( result == QLatin1String(
"_ImportError" ) )
441 missingModules << QStringLiteral(
"autopep8" );
450 QgsDebugError( QStringLiteral(
"Error running script: %1" ).arg( script ) );
454 else if ( formatter == QLatin1String(
"black" ) )
456 const bool normalize = settingBlackNormalizeQuotes->value();
464 const QString defineReformat = QStringLiteral(
465 "def __qgis_reformat(script):\n"
468 " except ImportError:\n"
469 " return '_ImportError'\n"
470 " options={'string_normalization': %1, 'line_length': %2}\n"
471 " return black.format_str(script, mode=black.Mode(**options))\n"
474 .arg( maxLineLength );
478 QgsDebugError( QStringLiteral(
"Error running script: %1" ).arg( defineReformat ) );
486 if ( result == QLatin1String(
"_ImportError" ) )
488 missingModules << QStringLiteral(
"black" );
497 QgsDebugError( QStringLiteral(
"Error running script: %1" ).arg( script ) );
502 if ( !missingModules.empty() )
504 if ( missingModules.size() == 1 )
510 const QString modules = missingModules.join( QLatin1String(
", " ) );
522 QString text = selectedText();
523 if ( text.isEmpty() )
525 text = wordAtPoint( mapFromGlobal( QCursor::pos() ) );
527 if ( text.isEmpty() )
532 QAction *pyQgisHelpAction =
new QAction(
534 tr(
"Search Selection in PyQGIS Documentation" ),
538 pyQgisHelpAction->setEnabled( hasSelectedText() );
539 pyQgisHelpAction->setShortcut( QKeySequence::StandardKey::HelpContents );
540 connect( pyQgisHelpAction, &QAction::triggered,
this, [text,
this] {
showApiDocumentation( text ); } );
542 menu->addSeparator();
543 menu->addAction( pyQgisHelpAction );
548 switch ( autoCompletionSource() )
551 autoCompleteFromDocument();
555 autoCompleteFromAPIs();
559 autoCompleteFromAll();
569 mAPISFilesList = filenames;
576 QgsDebugMsgLevel( QStringLiteral(
"The script file: %1" ).arg( script ), 2 );
577 QFile file( script );
578 if ( !file.open( QIODevice::ReadOnly ) )
583 QTextStream in( &file );
584#if QT_VERSION < QT_VERSION_CHECK( 6, 0, 0 )
585 in.setCodec(
"UTF-8" );
588 setText( in.readAll().trimmed() );
603 if ( position >= length() && position > 0 )
605 long style = SendScintilla( QsciScintillaBase::SCI_GETSTYLEAT, position - 1 );
606 return style == QsciLexerPython::Comment
607 || style == QsciLexerPython::TripleSingleQuotedString
608 || style == QsciLexerPython::TripleDoubleQuotedString
609 || style == QsciLexerPython::TripleSingleQuotedFString
610 || style == QsciLexerPython::TripleDoubleQuotedFString
611 || style == QsciLexerPython::UnclosedString;
615 long style = SendScintilla( QsciScintillaBase::SCI_GETSTYLEAT, position );
616 return style == QsciLexerPython::Comment
617 || style == QsciLexerPython::DoubleQuotedString
618 || style == QsciLexerPython::SingleQuotedString
619 || style == QsciLexerPython::TripleSingleQuotedString
620 || style == QsciLexerPython::TripleDoubleQuotedString
621 || style == QsciLexerPython::CommentBlock
622 || style == QsciLexerPython::UnclosedString
623 || style == QsciLexerPython::DoubleQuotedFString
624 || style == QsciLexerPython::SingleQuotedFString
625 || style == QsciLexerPython::TripleSingleQuotedFString
626 || style == QsciLexerPython::TripleDoubleQuotedFString;
637 return text( position - 1, position );
643 if ( position >= length() )
647 return text( position, position + 1 );
674 const QString originalText = text();
676 const QString defineCheckSyntax = QStringLiteral(
677 "def __check_syntax(script):\n"
679 " compile(script.encode('utf-8'), '', 'exec')\n"
680 " except SyntaxError as detail:\n"
681 " eline = detail.lineno or 1\n"
683 " ecolumn = detail.offset or 1\n"
684 " edescr = detail.msg\n"
685 " return '!!!!'.join([str(eline), str(ecolumn), edescr])\n"
691 QgsDebugError( QStringLiteral(
"Error running script: %1" ).arg( defineCheckSyntax ) );
699 if ( result.size() == 0 )
705 const QStringList parts = result.split( QStringLiteral(
"!!!!" ) );
706 if ( parts.size() == 3 )
708 const int line = parts.at( 0 ).toInt();
709 const int column = parts.at( 1 ).toInt();
711 setCursorPosition( line, column - 1 );
712 ensureLineVisible( line );
719 QgsDebugError( QStringLiteral(
"Error running script: %1" ).arg( script ) );
731 QString searchText = text;
732 searchText = searchText.replace( QLatin1String(
">>> " ), QString() ).replace( QLatin1String(
"... " ), QString() ).trimmed();
734 QRegularExpression qtExpression(
"^Q[A-Z][a-zA-Z]" );
736 if ( qtExpression.match( searchText ).hasMatch() )
738 const QString qtVersion = QString( qVersion() ).split(
'.' ).mid( 0, 2 ).join(
'.' );
739 QString baseUrl = QString(
"https://doc.qt.io/qt-%1" ).arg( qtVersion );
740 QDesktopServices::openUrl( QUrl( QStringLiteral(
"%1/%2.html" ).arg( baseUrl, searchText.toLower() ) ) );
743 const QString qgisVersion = QString(
Qgis::version() ).split(
'.' ).mid( 0, 2 ).join(
'.' );
744 if ( searchText.isEmpty() )
746 QDesktopServices::openUrl( QUrl( QStringLiteral(
"https://qgis.org/pyqgis/%1/" ).arg( qgisVersion ) ) );
750 QDesktopServices::openUrl( QUrl( QStringLiteral(
"https://qgis.org/pyqgis/%1/search.html?q=%2" ).arg( qgisVersion, searchText ) ) );
763QgsQsciLexerPython::QgsQsciLexerPython( QObject *parent )
764 : QsciLexerPython( parent )
768const char *QgsQsciLexerPython::keywords(
int set )
const
772 return "True False and as assert break class continue def del elif else except "
773 "finally for from global if import in is lambda None not or pass "
774 "raise return try while with yield async await nonlocal";
777 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)