18#include "moc_qgscodeeditorpython.cpp"
34#include <Qsci/qscilexerpython.h>
35#include <QDesktopServices>
40const QMap<QString, QString> QgsCodeEditorPython::sCompletionPairs
48const QStringList QgsCodeEditorPython::sCompletionSingleCharacters{
"`",
"*"};
50const QgsSettingsEntryString *QgsCodeEditorPython::settingCodeFormatter =
new QgsSettingsEntryString( QStringLiteral(
"formatter" ), sTreePythonCodeEditor, QStringLiteral(
"autopep8" ), QStringLiteral(
"Python code autoformatter" ) );
52const QgsSettingsEntryBool *QgsCodeEditorPython::settingSortImports =
new QgsSettingsEntryBool( QStringLiteral(
"sort-imports" ), sTreePythonCodeEditor,
true, QStringLiteral(
"Whether imports should be sorted when auto-formatting code" ) );
54const QgsSettingsEntryBool *QgsCodeEditorPython::settingBlackNormalizeQuotes =
new QgsSettingsEntryBool( QStringLiteral(
"black-normalize-quotes" ), sTreePythonCodeEditor,
true, QStringLiteral(
"Whether quotes should be normalized when auto-formatting code using black" ) );
55const 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." ) );
67 , mAPISFilesList( filenames )
96 setEdgeMode( QsciScintilla::EdgeLine );
97 setEdgeColumn( settingMaxLineLength->value() );
100 setWhitespaceVisibility( QsciScintilla::WsVisibleAfterIndent );
102 SendScintilla( QsciScintillaBase::SCI_SETPROPERTY,
"highlight.current.word",
"1" );
107 QsciLexerPython *pyLexer =
new QgsQsciLexerPython(
this );
109 pyLexer->setIndentationWarning( QsciLexerPython::Inconsistent );
110 pyLexer->setFoldComments(
true );
111 pyLexer->setFoldQuotes(
true );
113 pyLexer->setDefaultFont( font );
116 pyLexer->setFont( font, -1 );
118 font.setItalic(
true );
119 pyLexer->setFont( font, QsciLexerPython::Comment );
120 pyLexer->setFont( font, QsciLexerPython::CommentBlock );
122 font.setItalic(
false );
123 font.setBold(
true );
124 pyLexer->setFont( font, QsciLexerPython::SingleQuotedString );
125 pyLexer->setFont( font, QsciLexerPython::DoubleQuotedString );
127 pyLexer->setColor(
defaultColor, QsciLexerPython::Default );
145 std::unique_ptr< QsciAPIs > apis = std::make_unique< QsciAPIs >( pyLexer );
148 if ( mAPISFilesList.isEmpty() )
150 if ( settings.
value( QStringLiteral(
"pythonConsole/preloadAPI" ),
true ).toBool() )
153 apis->loadPrepared( mPapFile );
155 else if ( settings.
value( QStringLiteral(
"pythonConsole/usePreparedAPIFile" ),
false ).toBool() )
157 apis->loadPrepared( settings.
value( QStringLiteral(
"pythonConsole/preparedAPIFile" ) ).toString() );
161 const QStringList apiPaths = settings.
value( QStringLiteral(
"pythonConsole/userAPI" ) ).toStringList();
162 for (
const QString &path : apiPaths )
164 if ( !QFileInfo::exists( path ) )
166 QgsDebugError( QStringLiteral(
"The apis file %1 was not found" ).arg( path ) );
176 else if ( mAPISFilesList.length() == 1 && mAPISFilesList[0].right( 3 ) == QLatin1String(
"pap" ) )
178 if ( !QFileInfo::exists( mAPISFilesList[0] ) )
180 QgsDebugError( QStringLiteral(
"The apis file %1 not found" ).arg( mAPISFilesList.at( 0 ) ) );
183 mPapFile = mAPISFilesList[0];
184 apis->loadPrepared( mPapFile );
188 for (
const QString &path : std::as_const( mAPISFilesList ) )
190 if ( !QFileInfo::exists( path ) )
192 QgsDebugError( QStringLiteral(
"The apis file %1 was not found" ).arg( path ) );
202 pyLexer->setAPIs( apis.release() );
206 const int threshold = settings.
value( QStringLiteral(
"pythonConsole/autoCompThreshold" ), 2 ).toInt();
207 setAutoCompletionThreshold( threshold );
208 if ( !settings.
value(
"pythonConsole/autoCompleteEnabled",
true ).toBool() )
210 setAutoCompletionSource( AcsNone );
214 const QString autoCompleteSource = settings.
value( QStringLiteral(
"pythonConsole/autoCompleteSource" ), QStringLiteral(
"fromAPI" ) ).toString();
215 if ( autoCompleteSource == QLatin1String(
"fromDoc" ) )
216 setAutoCompletionSource( AcsDocument );
217 else if ( autoCompleteSource == QLatin1String(
"fromDocAPI" ) )
218 setAutoCompletionSource( AcsAll );
220 setAutoCompletionSource( AcsAPIs );
224 setIndentationsUseTabs(
false );
225 setIndentationGuides(
true );
240 bool autoCloseBracket = settings.
value( QStringLiteral(
"/pythonConsole/autoCloseBracket" ),
true ).toBool();
241 bool autoSurround = settings.
value( QStringLiteral(
"/pythonConsole/autoSurround" ),
true ).toBool();
242 bool autoInsertImport = settings.
value( QStringLiteral(
"/pythonConsole/autoInsertImport" ),
false ).toBool();
245 const QString eText =
event->text();
247 getCursorPosition( &line, &column );
251 if ( hasSelectedText() && autoSurround )
253 if ( sCompletionPairs.contains( eText ) )
255 int startLine, startPos, endLine, endPos;
256 getSelection( &startLine, &startPos, &endLine, &endPos );
259 if ( startLine != endLine && ( eText ==
"\"" || eText ==
"'" ) )
262 QString(
"%1%1%1%2%3%3%3" ).arg( eText, selectedText(), sCompletionPairs[eText] )
264 setSelection( startLine, startPos + 3, endLine, endPos + 3 );
269 QString(
"%1%2%3" ).arg( eText, selectedText(), sCompletionPairs[eText] )
271 setSelection( startLine, startPos + 1, endLine, endPos + 1 );
276 else if ( sCompletionSingleCharacters.contains( eText ) )
278 int startLine, startPos, endLine, endPos;
279 getSelection( &startLine, &startPos, &endLine, &endPos );
281 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( QStringLiteral(
"^from [\\w.]+$" ) );
297 if ( re.match( lineText.trimmed() ).hasMatch() )
299 insert( QStringLiteral(
" import" ) );
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 ) !=
"" )
349 if ( !( ( eText ==
"\"" || eText ==
"'" ) && prevChar == eText ) )
352 insert( sCompletionPairs[eText] );
371 const QString
formatter = settingCodeFormatter->value();
372 const int maxLineLength = settingMaxLineLength->value();
374 QString newText = string;
376 QStringList missingModules;
378 if ( settingSortImports->value() )
380 const QString defineSortImports = QStringLiteral(
381 "def __qgis_sort_imports(script):\n"
384 " except ImportError:\n"
385 " return '_ImportError'\n"
386 " options={'line_length': %1, 'profile': '%2', 'known_first_party': ['qgis', 'console', 'processing', 'plugins']}\n"
387 " return isort.code(script, **options)\n" )
388 .arg( maxLineLength )
389 .arg(
formatter == QLatin1String(
"black" ) ? QStringLiteral(
"black" ) : QString() );
393 QgsDebugError( QStringLiteral(
"Error running script: %1" ).arg( defineSortImports ) );
401 if ( result == QLatin1String(
"_ImportError" ) )
403 missingModules << QStringLiteral(
"isort" );
412 QgsDebugError( QStringLiteral(
"Error running script: %1" ).arg( script ) );
417 if (
formatter == QLatin1String(
"autopep8" ) )
419 const int level = settingAutopep8Level->value();
421 const QString defineReformat = QStringLiteral(
422 "def __qgis_reformat(script):\n"
425 " except ImportError:\n"
426 " return '_ImportError'\n"
427 " options={'aggressive': %1, 'max_line_length': %2}\n"
428 " return autopep8.fix_code(script, options=options)\n" )
430 .arg( maxLineLength );
434 QgsDebugError( QStringLiteral(
"Error running script: %1" ).arg( defineReformat ) );
442 if ( result == QLatin1String(
"_ImportError" ) )
444 missingModules << QStringLiteral(
"autopep8" );
453 QgsDebugError( QStringLiteral(
"Error running script: %1" ).arg( script ) );
457 else if (
formatter == QLatin1String(
"black" ) )
459 const bool normalize = settingBlackNormalizeQuotes->value();
467 const QString defineReformat = QStringLiteral(
468 "def __qgis_reformat(script):\n"
471 " except ImportError:\n"
472 " return '_ImportError'\n"
473 " options={'string_normalization': %1, 'line_length': %2}\n"
474 " return black.format_str(script, mode=black.Mode(**options))\n" )
476 .arg( maxLineLength );
480 QgsDebugError( QStringLiteral(
"Error running script: %1" ).arg( defineReformat ) );
488 if ( result == QLatin1String(
"_ImportError" ) )
490 missingModules << QStringLiteral(
"black" );
499 QgsDebugError( QStringLiteral(
"Error running script: %1" ).arg( script ) );
504 if ( !missingModules.empty() )
506 if ( missingModules.size() == 1 )
512 const QString modules = missingModules.join( QLatin1String(
", " ) );
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" ),
539 pyQgisHelpAction->setEnabled( hasSelectedText() );
540 pyQgisHelpAction->setShortcut( QStringLiteral(
"F1" ) );
541 connect( pyQgisHelpAction, &QAction::triggered,
this, [text,
this] {
showApiDocumentation( text );} );
543 menu->addSeparator();
544 menu->addAction( pyQgisHelpAction );
549 switch ( autoCompletionSource() )
552 autoCompleteFromDocument();
556 autoCompleteFromAPIs();
560 autoCompleteFromAll();
570 mAPISFilesList = filenames;
577 QgsDebugMsgLevel( QStringLiteral(
"The script file: %1" ).arg( script ), 2 );
578 QFile file( script );
579 if ( !file.open( QIODevice::ReadOnly ) )
584 QTextStream in( &file );
585#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
586 in.setCodec(
"UTF-8" );
589 setText( in.readAll().trimmed() );
604 if ( position >= length() && position > 0 )
606 long style = SendScintilla( QsciScintillaBase::SCI_GETSTYLEAT, position - 1 );
607 return style == QsciLexerPython::Comment
608 || style == QsciLexerPython::TripleSingleQuotedString
609 || style == QsciLexerPython::TripleDoubleQuotedString
610 || style == QsciLexerPython::TripleSingleQuotedFString
611 || style == QsciLexerPython::TripleDoubleQuotedFString
612 || style == QsciLexerPython::UnclosedString;
616 long style = SendScintilla( QsciScintillaBase::SCI_GETSTYLEAT, position );
617 return style == QsciLexerPython::Comment
618 || style == QsciLexerPython::DoubleQuotedString
619 || style == QsciLexerPython::SingleQuotedString
620 || style == QsciLexerPython::TripleSingleQuotedString
621 || style == QsciLexerPython::TripleDoubleQuotedString
622 || style == QsciLexerPython::CommentBlock
623 || style == QsciLexerPython::UnclosedString
624 || style == QsciLexerPython::DoubleQuotedFString
625 || style == QsciLexerPython::SingleQuotedFString
626 || style == QsciLexerPython::TripleSingleQuotedFString
627 || style == QsciLexerPython::TripleDoubleQuotedFString;
638 return text( position - 1, position );
644 if ( position >= length() )
648 return text( position, position + 1 );
675 const QString originalText = text();
677 const QString defineCheckSyntax = QStringLiteral(
678 "def __check_syntax(script):\n"
680 " compile(script.encode('utf-8'), '', 'exec')\n"
681 " except SyntaxError as detail:\n"
682 " eline = detail.lineno or 1\n"
684 " ecolumn = detail.offset or 1\n"
685 " edescr = detail.msg\n"
686 " 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 ) ) );
762 int startLine, startPos, endLine, endPos;
763 if ( hasSelectedText() )
765 getSelection( &startLine, &startPos, &endLine, &endPos );
769 getCursorPosition( &startLine, &startPos );
775 bool allEmpty =
true;
776 bool allCommented =
true;
777 int minIndentation = -1;
778 for (
int line = startLine; line <= endLine; line++ )
780 const QString stripped = text( line ).trimmed();
781 if ( !stripped.isEmpty() )
784 if ( !stripped.startsWith(
'#' ) )
786 allCommented =
false;
788 if ( minIndentation == -1 || minIndentation > indentation( line ) )
790 minIndentation = indentation( line );
804 for (
int line = startLine; line <= endLine; line++ )
806 const QString stripped = text( line ).trimmed();
809 if ( stripped.isEmpty() )
816 insertAt( QStringLiteral(
"# " ), line, minIndentation );
821 if ( !stripped.startsWith(
'#' ) )
825 if ( stripped.startsWith( QLatin1String(
"# " ) ) )
833 setSelection( line, indentation( line ), line, indentation( line ) + delta );
834 removeSelectedText();
839 setSelection( startLine, startPos - delta, endLine, endPos - delta );
846QgsQsciLexerPython::QgsQsciLexerPython( QObject *parent )
847 : QsciLexerPython( parent )
852const char *QgsQsciLexerPython::keywords(
int set )
const
856 return "True False and as assert break class continue def del elif else except "
857 "finally for from global if import in is lambda None not or pass "
858 "raise return try while with yield async await nonlocal";
861 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)