18#include "moc_qgscodeeditorpython.cpp"
34#include <Qsci/qscilexerpython.h>
35#include <QDesktopServices>
40const 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." ) );
60 :
QgsCodeEditor( parent, QString(), false, false, flags, mode )
61 , mAPISFilesList( filenames )
90 setEdgeMode( QsciScintilla::EdgeLine );
91 setEdgeColumn( settingMaxLineLength->value() );
94 setWhitespaceVisibility( QsciScintilla::WsVisibleAfterIndent );
96 SendScintilla( QsciScintillaBase::SCI_SETPROPERTY,
"highlight.current.word",
"1" );
101 QsciLexerPython *pyLexer =
new QgsQsciLexerPython(
this );
103 pyLexer->setIndentationWarning( QsciLexerPython::Inconsistent );
104 pyLexer->setFoldComments(
true );
105 pyLexer->setFoldQuotes(
true );
107 pyLexer->setDefaultFont( font );
110 pyLexer->setFont( font, -1 );
112 font.setItalic(
true );
113 pyLexer->setFont( font, QsciLexerPython::Comment );
114 pyLexer->setFont( font, QsciLexerPython::CommentBlock );
116 font.setItalic(
false );
117 font.setBold(
true );
118 pyLexer->setFont( font, QsciLexerPython::SingleQuotedString );
119 pyLexer->setFont( font, QsciLexerPython::DoubleQuotedString );
121 pyLexer->setColor(
defaultColor, QsciLexerPython::Default );
139 std::unique_ptr<QsciAPIs> apis = std::make_unique<QsciAPIs>( pyLexer );
142 if ( mAPISFilesList.isEmpty() )
144 if ( settings.
value( QStringLiteral(
"pythonConsole/preloadAPI" ),
true ).toBool() )
147 apis->loadPrepared( mPapFile );
149 else if ( settings.
value( QStringLiteral(
"pythonConsole/usePreparedAPIFile" ),
false ).toBool() )
151 apis->loadPrepared( settings.
value( QStringLiteral(
"pythonConsole/preparedAPIFile" ) ).toString() );
155 const QStringList apiPaths = settings.
value( QStringLiteral(
"pythonConsole/userAPI" ) ).toStringList();
156 for (
const QString &path : apiPaths )
158 if ( !QFileInfo::exists( path ) )
160 QgsDebugError( QStringLiteral(
"The apis file %1 was not found" ).arg( path ) );
170 else if ( mAPISFilesList.length() == 1 && mAPISFilesList[0].right( 3 ) == QLatin1String(
"pap" ) )
172 if ( !QFileInfo::exists( mAPISFilesList[0] ) )
174 QgsDebugError( QStringLiteral(
"The apis file %1 not found" ).arg( mAPISFilesList.at( 0 ) ) );
177 mPapFile = mAPISFilesList[0];
178 apis->loadPrepared( mPapFile );
182 for (
const QString &path : std::as_const( mAPISFilesList ) )
184 if ( !QFileInfo::exists( path ) )
186 QgsDebugError( QStringLiteral(
"The apis file %1 was not found" ).arg( path ) );
196 pyLexer->setAPIs( apis.release() );
200 const int threshold = settings.
value( QStringLiteral(
"pythonConsole/autoCompThreshold" ), 2 ).toInt();
201 setAutoCompletionThreshold( threshold );
202 if ( !settings.
value(
"pythonConsole/autoCompleteEnabled",
true ).toBool() )
204 setAutoCompletionSource( AcsNone );
208 const QString autoCompleteSource = settings.
value( QStringLiteral(
"pythonConsole/autoCompleteSource" ), QStringLiteral(
"fromAPI" ) ).toString();
209 if ( autoCompleteSource == QLatin1String(
"fromDoc" ) )
210 setAutoCompletionSource( AcsDocument );
211 else if ( autoCompleteSource == QLatin1String(
"fromDocAPI" ) )
212 setAutoCompletionSource( AcsAll );
214 setAutoCompletionSource( AcsAPIs );
218 setIndentationsUseTabs(
false );
219 setIndentationGuides(
true );
234 bool autoCloseBracket = settings.
value( QStringLiteral(
"/pythonConsole/autoCloseBracket" ),
true ).toBool();
235 bool autoSurround = settings.
value( QStringLiteral(
"/pythonConsole/autoSurround" ),
true ).toBool();
236 bool autoInsertImport = settings.
value( QStringLiteral(
"/pythonConsole/autoInsertImport" ),
false ).toBool();
239 const QString eText =
event->text();
241 getCursorPosition( &line, &column );
245 if ( hasSelectedText() && autoSurround )
247 if ( sCompletionPairs.contains( eText ) )
249 int startLine, startPos, endLine, endPos;
250 getSelection( &startLine, &startPos, &endLine, &endPos );
253 if ( startLine != endLine && ( eText ==
"\"" || eText ==
"'" ) )
256 QString(
"%1%1%1%2%3%3%3" ).arg( eText, selectedText(), sCompletionPairs[eText] )
258 setSelection( startLine, startPos + 3, endLine, endPos + 3 );
263 QString(
"%1%2%3" ).arg( eText, selectedText(), sCompletionPairs[eText] )
265 setSelection( startLine, startPos + 1, endLine, endPos + 1 );
270 else if ( sCompletionSingleCharacters.contains( eText ) )
272 int startLine, startPos, endLine, endPos;
273 getSelection( &startLine, &startPos, &endLine, &endPos );
275 QString(
"%1%2%1" ).arg( eText, selectedText() )
277 setSelection( startLine, startPos + 1, endLine, endPos + 1 );
287 if ( autoInsertImport && eText ==
" " )
289 const QString lineText = text( line );
290 const thread_local QRegularExpression re( QStringLiteral(
"^from [\\w.]+$" ) );
291 if ( re.match( lineText.trimmed() ).hasMatch() )
293 insert( QStringLiteral(
" import" ) );
294 setCursorPosition( line, column + 7 );
300 else if ( autoCloseBracket )
306 if ( event->key() == Qt::Key_Backspace )
308 if ( sCompletionPairs.contains( prevChar ) && sCompletionPairs[prevChar] == nextChar )
310 setSelection( line, column - 1, line, column + 1 );
311 removeSelectedText();
324 else if ( sCompletionPairs.key( eText ) !=
"" && nextChar == eText )
326 setCursorPosition( line, column + 1 );
338 && sCompletionPairs.contains( eText )
339 && ( nextChar.isEmpty() || nextChar.at( 0 ).isSpace() || nextChar ==
":" || sCompletionPairs.key( nextChar ) !=
"" ) )
342 if ( !( ( eText ==
"\"" || eText ==
"'" ) && prevChar == eText ) )
345 insert( sCompletionPairs[eText] );
364 const QString formatter = settingCodeFormatter->value();
365 const int maxLineLength = settingMaxLineLength->value();
367 QString newText = string;
369 QStringList missingModules;
371 if ( settingSortImports->value() )
373 const QString defineSortImports = QStringLiteral(
374 "def __qgis_sort_imports(script):\n"
377 " except ImportError:\n"
378 " return '_ImportError'\n"
379 " options={'line_length': %1, 'profile': '%2', 'known_first_party': ['qgis', 'console', 'processing', 'plugins']}\n"
380 " return isort.code(script, **options)\n"
382 .arg( maxLineLength )
383 .arg( formatter == QLatin1String(
"black" ) ? QStringLiteral(
"black" ) : QString() );
387 QgsDebugError( QStringLiteral(
"Error running script: %1" ).arg( defineSortImports ) );
395 if ( result == QLatin1String(
"_ImportError" ) )
397 missingModules << QStringLiteral(
"isort" );
406 QgsDebugError( QStringLiteral(
"Error running script: %1" ).arg( script ) );
411 if ( formatter == QLatin1String(
"autopep8" ) )
413 const int level = settingAutopep8Level->value();
415 const QString defineReformat = QStringLiteral(
416 "def __qgis_reformat(script):\n"
419 " except ImportError:\n"
420 " return '_ImportError'\n"
421 " options={'aggressive': %1, 'max_line_length': %2}\n"
422 " 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"
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 QString text = selectedText();
521 if ( text.isEmpty() )
523 text = wordAtPoint( mapFromGlobal( QCursor::pos() ) );
525 if ( text.isEmpty() )
530 QAction *pyQgisHelpAction =
new QAction(
532 tr(
"Search Selection in PyQGIS Documentation" ),
536 pyQgisHelpAction->setEnabled( hasSelectedText() );
537 pyQgisHelpAction->setShortcut( QStringLiteral(
"F1" ) );
538 connect( pyQgisHelpAction, &QAction::triggered,
this, [text,
this] {
showApiDocumentation( text ); } );
540 menu->addSeparator();
541 menu->addAction( pyQgisHelpAction );
546 switch ( autoCompletionSource() )
549 autoCompleteFromDocument();
553 autoCompleteFromAPIs();
557 autoCompleteFromAll();
567 mAPISFilesList = filenames;
574 QgsDebugMsgLevel( QStringLiteral(
"The script file: %1" ).arg( script ), 2 );
575 QFile file( script );
576 if ( !file.open( QIODevice::ReadOnly ) )
581 QTextStream in( &file );
582#if QT_VERSION < QT_VERSION_CHECK( 6, 0, 0 )
583 in.setCodec(
"UTF-8" );
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( QStringLiteral(
"Error running script: %1" ).arg( defineCheckSyntax ) );
697 if ( result.size() == 0 )
703 const QStringList parts = result.split( QStringLiteral(
"!!!!" ) );
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( QStringLiteral(
"Error running script: %1" ).arg( script ) );
729 QString searchText = text;
730 searchText = searchText.replace( QLatin1String(
">>> " ), QString() ).replace( QLatin1String(
"... " ), 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( QStringLiteral(
"%1/%2.html" ).arg( baseUrl, searchText.toLower() ) ) );
741 const QString qgisVersion = QString(
Qgis::version() ).split(
'.' ).mid( 0, 2 ).join(
'.' );
742 if ( searchText.isEmpty() )
744 QDesktopServices::openUrl( QUrl( QStringLiteral(
"https://qgis.org/pyqgis/%1/" ).arg( qgisVersion ) ) );
748 QDesktopServices::openUrl( QUrl( QStringLiteral(
"https://qgis.org/pyqgis/%1/search.html?q=%2" ).arg( qgisVersion, searchText ) ) );
760 int startLine, startPos, endLine, endPos;
761 if ( hasSelectedText() )
763 getSelection( &startLine, &startPos, &endLine, &endPos );
767 getCursorPosition( &startLine, &startPos );
773 bool allEmpty =
true;
774 bool allCommented =
true;
775 int minIndentation = -1;
776 for (
int line = startLine; line <= endLine; line++ )
778 const QString stripped = text( line ).trimmed();
779 if ( !stripped.isEmpty() )
782 if ( !stripped.startsWith(
'#' ) )
784 allCommented =
false;
786 if ( minIndentation == -1 || minIndentation > indentation( line ) )
788 minIndentation = indentation( line );
802 for (
int line = startLine; line <= endLine; line++ )
804 const QString stripped = text( line ).trimmed();
807 if ( stripped.isEmpty() )
814 insertAt( QStringLiteral(
"# " ), line, minIndentation );
819 if ( !stripped.startsWith(
'#' ) )
823 if ( stripped.startsWith( QLatin1String(
"# " ) ) )
831 setSelection( line, indentation( line ), line, indentation( line ) + delta );
832 removeSelectedText();
837 setSelection( startLine, startPos - delta, endLine, endPos - delta );
844QgsQsciLexerPython::QgsQsciLexerPython( QObject *parent )
845 : QsciLexerPython( parent )
849const char *QgsQsciLexerPython::keywords(
int set )
const
853 return "True False and as assert break class continue def del elif else except "
854 "finally for from global if import in is lambda None not or pass "
855 "raise return try while with yield async await nonlocal";
858 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.
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 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.
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.
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)