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  auto 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( QKeySequence::StandardKey::HelpContents );
 
  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 ) ) );
 
 
  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.
 
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 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.
 
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)