QGIS API Documentation 3.34.0-Prizren (ffbdd678812)
Loading...
Searching...
No Matches
qgscodeeditorpython.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgscodeeditorpython.cpp - A Python editor based on QScintilla
3 --------------------------------------
4 Date : 06-Oct-2013
5 Copyright : (C) 2013 by Salvatore Larosa
6 Email : lrssvtml (at) gmail (dot) com
7 ***************************************************************************
8 * *
9 * This program is free software; you can redistribute it and/or modify *
10 * it under the terms of the GNU General Public License as published by *
11 * the Free Software Foundation; either version 2 of the License, or *
12 * (at your option) any later version. *
13 * *
14 ***************************************************************************/
15
16#include "qgsapplication.h"
17#include "qgscodeeditorpython.h"
18#include "qgslogger.h"
19#include "qgssymbollayerutils.h"
20#include "qgis.h"
21#include "qgspythonrunner.h"
22#include "qgsprocessingutils.h"
24#include "qgssettings.h"
25#include <QWidget>
26#include <QString>
27#include <QFont>
28#include <QUrl>
29#include <QFileInfo>
30#include <QMessageBox>
31#include <QTextStream>
32#include <Qsci/qscilexerpython.h>
33#include <QDesktopServices>
34#include <QKeyEvent>
35#include <QAction>
36#include <QMenu>
37
38const QMap<QString, QString> QgsCodeEditorPython::sCompletionPairs
39{
40 {"(", ")"},
41 {"[", "]"},
42 {"{", "}"},
43 {"'", "'"},
44 {"\"", "\""}
45};
46const QStringList QgsCodeEditorPython::sCompletionSingleCharacters{"`", "*"};
48const QgsSettingsEntryString *QgsCodeEditorPython::settingCodeFormatter = new QgsSettingsEntryString( QStringLiteral( "formatter" ), sTreePythonCodeEditor, QStringLiteral( "autopep8" ), QStringLiteral( "Python code autoformatter" ) );
49const QgsSettingsEntryInteger *QgsCodeEditorPython::settingMaxLineLength = new QgsSettingsEntryInteger( QStringLiteral( "max-line-length" ), sTreePythonCodeEditor, 80, QStringLiteral( "Maximum line length" ) );
50const QgsSettingsEntryBool *QgsCodeEditorPython::settingSortImports = new QgsSettingsEntryBool( QStringLiteral( "sort-imports" ), sTreePythonCodeEditor, true, QStringLiteral( "Whether imports should be sorted when auto-formatting code" ) );
51const QgsSettingsEntryInteger *QgsCodeEditorPython::settingAutopep8Level = new QgsSettingsEntryInteger( QStringLiteral( "autopep8-level" ), sTreePythonCodeEditor, 1, QStringLiteral( "Autopep8 aggressive level" ) );
52const QgsSettingsEntryBool *QgsCodeEditorPython::settingBlackNormalizeQuotes = new QgsSettingsEntryBool( QStringLiteral( "black-normalize-quotes" ), sTreePythonCodeEditor, true, QStringLiteral( "Whether quotes should be normalized when auto-formatting code using black" ) );
54
55
56QgsCodeEditorPython::QgsCodeEditorPython( QWidget *parent, const QList<QString> &filenames, Mode mode, Flags flags )
57 : QgsCodeEditor( parent,
58 QString(),
59 false,
60 false,
61 flags,
62 mode )
63 , mAPISFilesList( filenames )
64{
65 if ( !parent )
66 {
67 setTitle( tr( "Python Editor" ) );
68 }
69
70 setCaretWidth( 2 );
71
73
75}
76
81
82Qgis::ScriptLanguageCapabilities QgsCodeEditorPython::languageCapabilities() const
83{
84 return mCapabilities;
85}
86
88{
89 // current line
90 setEdgeMode( QsciScintilla::EdgeLine );
91 setEdgeColumn( settingMaxLineLength->value() );
93
94 setWhitespaceVisibility( QsciScintilla::WsVisibleAfterIndent );
95
96 QFont font = lexerFont();
98
99 QsciLexerPython *pyLexer = new QgsQsciLexerPython( this );
100
101 pyLexer->setIndentationWarning( QsciLexerPython::Inconsistent );
102 pyLexer->setFoldComments( true );
103 pyLexer->setFoldQuotes( true );
104
105 pyLexer->setDefaultFont( font );
106 pyLexer->setDefaultColor( defaultColor );
107 pyLexer->setDefaultPaper( lexerColor( QgsCodeEditorColorScheme::ColorRole::Background ) );
108 pyLexer->setFont( font, -1 );
109
110 font.setItalic( true );
111 pyLexer->setFont( font, QsciLexerPython::Comment );
112 pyLexer->setFont( font, QsciLexerPython::CommentBlock );
113
114 font.setItalic( false );
115 font.setBold( true );
116 pyLexer->setFont( font, QsciLexerPython::SingleQuotedString );
117 pyLexer->setFont( font, QsciLexerPython::DoubleQuotedString );
118
119 pyLexer->setColor( defaultColor, QsciLexerPython::Default );
120 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::Class ), QsciLexerPython::ClassName );
121 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::Method ), QsciLexerPython::FunctionMethodName );
122 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::Number ), QsciLexerPython::Number );
123 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::Operator ), QsciLexerPython::Operator );
124 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::Identifier ), QsciLexerPython::Identifier );
125 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::Comment ), QsciLexerPython::Comment );
126 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::CommentBlock ), QsciLexerPython::CommentBlock );
127 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::Keyword ), QsciLexerPython::Keyword );
128 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::Decoration ), QsciLexerPython::Decorator );
129 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::SingleQuote ), QsciLexerPython::SingleQuotedString );
130 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::DoubleQuote ), QsciLexerPython::DoubleQuotedString );
131 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::TripleSingleQuote ), QsciLexerPython::TripleSingleQuotedString );
132 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::TripleDoubleQuote ), QsciLexerPython::TripleDoubleQuotedString );
133
134 std::unique_ptr< QsciAPIs > apis = std::make_unique< QsciAPIs >( pyLexer );
135
136 QgsSettings settings;
137 if ( mAPISFilesList.isEmpty() )
138 {
139 if ( settings.value( QStringLiteral( "pythonConsole/preloadAPI" ), true ).toBool() )
140 {
141 mPapFile = QgsApplication::pkgDataPath() + QStringLiteral( "/python/qsci_apis/PyQGIS.pap" );
142 apis->loadPrepared( mPapFile );
143 }
144 else if ( settings.value( QStringLiteral( "pythonConsole/usePreparedAPIFile" ), false ).toBool() )
145 {
146 apis->loadPrepared( settings.value( QStringLiteral( "pythonConsole/preparedAPIFile" ) ).toString() );
147 }
148 else
149 {
150 const QStringList apiPaths = settings.value( QStringLiteral( "pythonConsole/userAPI" ) ).toStringList();
151 for ( const QString &path : apiPaths )
152 {
153 if ( !QFileInfo::exists( path ) )
154 {
155 QgsDebugError( QStringLiteral( "The apis file %1 was not found" ).arg( path ) );
156 }
157 else
158 {
159 apis->load( path );
160 }
161 }
162 apis->prepare();
163 }
164 }
165 else if ( mAPISFilesList.length() == 1 && mAPISFilesList[0].right( 3 ) == QLatin1String( "pap" ) )
166 {
167 if ( !QFileInfo::exists( mAPISFilesList[0] ) )
168 {
169 QgsDebugError( QStringLiteral( "The apis file %1 not found" ).arg( mAPISFilesList.at( 0 ) ) );
170 return;
171 }
172 mPapFile = mAPISFilesList[0];
173 apis->loadPrepared( mPapFile );
174 }
175 else
176 {
177 for ( const QString &path : std::as_const( mAPISFilesList ) )
178 {
179 if ( !QFileInfo::exists( path ) )
180 {
181 QgsDebugError( QStringLiteral( "The apis file %1 was not found" ).arg( path ) );
182 }
183 else
184 {
185 apis->load( path );
186 }
187 }
188 apis->prepare();
189 }
190 if ( apis )
191 pyLexer->setAPIs( apis.release() );
192
193 setLexer( pyLexer );
194
195 const int threshold = settings.value( QStringLiteral( "pythonConsole/autoCompThreshold" ), 2 ).toInt();
196 setAutoCompletionThreshold( threshold );
197 if ( !settings.value( "pythonConsole/autoCompleteEnabled", true ).toBool() )
198 {
199 setAutoCompletionSource( AcsNone );
200 }
201 else
202 {
203 const QString autoCompleteSource = settings.value( QStringLiteral( "pythonConsole/autoCompleteSource" ), QStringLiteral( "fromAPI" ) ).toString();
204 if ( autoCompleteSource == QLatin1String( "fromDoc" ) )
205 setAutoCompletionSource( AcsDocument );
206 else if ( autoCompleteSource == QLatin1String( "fromDocAPI" ) )
207 setAutoCompletionSource( AcsAll );
208 else
209 setAutoCompletionSource( AcsAPIs );
210 }
211
212 setLineNumbersVisible( true );
213 setIndentationsUseTabs( false );
214 setIndentationGuides( true );
215
217}
218
220{
221 // If editor is readOnly, use the default implementation
222 if ( isReadOnly() )
223 {
224 return QgsCodeEditor::keyPressEvent( event );
225 }
226
227 const QgsSettings settings;
228
229 bool autoCloseBracket = settings.value( QStringLiteral( "/pythonConsole/autoCloseBracket" ), true ).toBool();
230 bool autoSurround = settings.value( QStringLiteral( "/pythonConsole/autoSurround" ), true ).toBool();
231 bool autoInsertImport = settings.value( QStringLiteral( "/pythonConsole/autoInsertImport" ), false ).toBool();
232
233 // Update calltips when cursor position changes with left and right keys
234 if ( event->key() == Qt::Key_Left ||
235 event->key() == Qt::Key_Right ||
236 event->key() == Qt::Key_Up ||
237 event->key() == Qt::Key_Down )
238 {
240 callTip();
241 return;
242 }
243
244 // Get entered text and cursor position
245 const QString eText = event->text();
246 int line, column;
247 getCursorPosition( &line, &column );
248
249 // If some text is selected and user presses an opening character
250 // surround the selection with the opening-closing pair
251 if ( hasSelectedText() && autoSurround )
252 {
253 if ( sCompletionPairs.contains( eText ) )
254 {
255 int startLine, startPos, endLine, endPos;
256 getSelection( &startLine, &startPos, &endLine, &endPos );
257
258 // Special case for Multi line quotes (insert triple quotes)
259 if ( startLine != endLine && ( eText == "\"" || eText == "'" ) )
260 {
261 replaceSelectedText(
262 QString( "%1%1%1%2%3%3%3" ).arg( eText, selectedText(), sCompletionPairs[eText] )
263 );
264 setSelection( startLine, startPos + 3, endLine, endPos + 3 );
265 }
266 else
267 {
268 replaceSelectedText(
269 QString( "%1%2%3" ).arg( eText, selectedText(), sCompletionPairs[eText] )
270 );
271 setSelection( startLine, startPos + 1, endLine, endPos + 1 );
272 }
273 event->accept();
274 return;
275 }
276 else if ( sCompletionSingleCharacters.contains( eText ) )
277 {
278 int startLine, startPos, endLine, endPos;
279 getSelection( &startLine, &startPos, &endLine, &endPos );
280 replaceSelectedText(
281 QString( "%1%2%1" ).arg( eText, selectedText() )
282 );
283 setSelection( startLine, startPos + 1, endLine, endPos + 1 );
284 event->accept();
285 return;
286 }
287 }
288
289 // No selected text
290 else
291 {
292 // Automatically insert "import" after "from xxx " if option is enabled
293 if ( autoInsertImport && eText == " " )
294 {
295 const QString lineText = text( line );
296 const thread_local QRegularExpression re( QStringLiteral( "^from [\\w.]+$" ) );
297 if ( re.match( lineText.trimmed() ).hasMatch() )
298 {
299 insert( QStringLiteral( " import" ) );
300 setCursorPosition( line, column + 7 );
301 return QgsCodeEditor::keyPressEvent( event );
302 }
303 }
304
305 // Handle automatic bracket insertion/deletion if option is enabled
306 else if ( autoCloseBracket )
307 {
308 const QString prevChar = characterBeforeCursor();
309 const QString nextChar = characterAfterCursor();
310
311 // When backspace is pressed inside an opening/closing pair, remove both characters
312 if ( event->key() == Qt::Key_Backspace )
313 {
314 if ( sCompletionPairs.contains( prevChar ) && sCompletionPairs[prevChar] == nextChar )
315 {
316 setSelection( line, column - 1, line, column + 1 );
317 removeSelectedText();
318 event->accept();
319 }
320 else
321 {
323 }
324
325 // Update calltips (cursor position has changed)
326 callTip();
327 return;
328 }
329
330 // When closing character is entered inside an opening/closing pair, shift the cursor
331 else if ( sCompletionPairs.key( eText ) != "" && nextChar == eText )
332 {
333 setCursorPosition( line, column + 1 );
334 event->accept();
335
336 // Will hide calltips when a closing parenthesis is entered
337 callTip();
338 return;
339 }
340
341 // Else, if not inside a string or comment and an opening character
342 // is entered, also insert the closing character, provided the next
343 // character is a space, a colon, or a closing character
345 && sCompletionPairs.contains( eText )
346 && ( nextChar.isEmpty() || nextChar.at( 0 ).isSpace() || nextChar == ":" || sCompletionPairs.key( nextChar ) != "" )
347 )
348 {
349 // Check if user is not entering triple quotes
350 if ( !( ( eText == "\"" || eText == "'" ) && prevChar == eText ) )
351 {
353 insert( sCompletionPairs[eText] );
354 event->accept();
355 return;
356 }
357 }
358 }
359 }
360
361 // Let QgsCodeEditor handle the keyboard event
362 return QgsCodeEditor::keyPressEvent( event );
363}
364
365QString QgsCodeEditorPython::reformatCodeString( const QString &string )
366{
368 {
369 return string;
370 }
371
372 const QString formatter = settingCodeFormatter->value();
373 const int maxLineLength = settingMaxLineLength->value();
374
375 QString newText = string;
376
377 QStringList missingModules;
378
379 if ( settingSortImports->value() )
380 {
381 const QString defineSortImports = QStringLiteral(
382 "def __qgis_sort_imports(script):\n"
383 " try:\n"
384 " import isort\n"
385 " except ImportError:\n"
386 " return '_ImportError'\n"
387 " options={'line_length': %1, 'profile': '%2', 'known_first_party': ['qgis', 'console', 'processing', 'plugins']}\n"
388 " return isort.code(script, **options)\n" )
389 .arg( maxLineLength )
390 .arg( formatter == QLatin1String( "black" ) ? QStringLiteral( "black" ) : QString() );
391
392 if ( !QgsPythonRunner::run( defineSortImports ) )
393 {
394 QgsDebugError( QStringLiteral( "Error running script: %1" ).arg( defineSortImports ) );
395 return string;
396 }
397
398 const QString script = QStringLiteral( "__qgis_sort_imports(%1)" ).arg( QgsProcessingUtils::stringToPythonLiteral( newText ) );
399 QString result;
400 if ( QgsPythonRunner::eval( script, result ) )
401 {
402 if ( result == QLatin1String( "_ImportError" ) )
403 {
404 missingModules << QStringLiteral( "isort" );
405 }
406 else
407 {
408 newText = result;
409 }
410 }
411 else
412 {
413 QgsDebugError( QStringLiteral( "Error running script: %1" ).arg( script ) );
414 return newText;
415 }
416 }
417
418 if ( formatter == QLatin1String( "autopep8" ) )
419 {
420 const int level = settingAutopep8Level->value();
421
422 const QString defineReformat = QStringLiteral(
423 "def __qgis_reformat(script):\n"
424 " try:\n"
425 " import autopep8\n"
426 " except ImportError:\n"
427 " return '_ImportError'\n"
428 " options={'aggressive': %1, 'max_line_length': %2}\n"
429 " return autopep8.fix_code(script, options=options)\n" )
430 .arg( level )
431 .arg( maxLineLength );
432
433 if ( !QgsPythonRunner::run( defineReformat ) )
434 {
435 QgsDebugError( QStringLiteral( "Error running script: %1" ).arg( defineReformat ) );
436 return newText;
437 }
438
439 const QString script = QStringLiteral( "__qgis_reformat(%1)" ).arg( QgsProcessingUtils::stringToPythonLiteral( newText ) );
440 QString result;
441 if ( QgsPythonRunner::eval( script, result ) )
442 {
443 if ( result == QLatin1String( "_ImportError" ) )
444 {
445 missingModules << QStringLiteral( "autopep8" );
446 }
447 else
448 {
449 newText = result;
450 }
451 }
452 else
453 {
454 QgsDebugError( QStringLiteral( "Error running script: %1" ).arg( script ) );
455 return newText;
456 }
457 }
458 else if ( formatter == QLatin1String( "black" ) )
459 {
460 const bool normalize = settingBlackNormalizeQuotes->value();
461
462 if ( !checkSyntax() )
463 {
464 showMessage( tr( "Reformat Code" ), tr( "Code formatting failed -- the code contains syntax errors" ), Qgis::MessageLevel::Warning );
465 return newText;
466 }
467
468 const QString defineReformat = QStringLiteral(
469 "def __qgis_reformat(script):\n"
470 " try:\n"
471 " import black\n"
472 " except ImportError:\n"
473 " return '_ImportError'\n"
474 " options={'string_normalization': %1, 'line_length': %2}\n"
475 " return black.format_str(script, mode=black.Mode(**options))\n" )
477 .arg( maxLineLength );
478
479 if ( !QgsPythonRunner::run( defineReformat ) )
480 {
481 QgsDebugError( QStringLiteral( "Error running script: %1" ).arg( defineReformat ) );
482 return string;
483 }
484
485 const QString script = QStringLiteral( "__qgis_reformat(%1)" ).arg( QgsProcessingUtils::stringToPythonLiteral( newText ) );
486 QString result;
487 if ( QgsPythonRunner::eval( script, result ) )
488 {
489 if ( result == QLatin1String( "_ImportError" ) )
490 {
491 missingModules << QStringLiteral( "black" );
492 }
493 else
494 {
495 newText = result;
496 }
497 }
498 else
499 {
500 QgsDebugError( QStringLiteral( "Error running script: %1" ).arg( script ) );
501 return newText;
502 }
503 }
504
505 if ( !missingModules.empty() )
506 {
507 if ( missingModules.size() == 1 )
508 {
509 showMessage( tr( "Reformat Code" ), tr( "The Python module %1 is missing" ).arg( missingModules.at( 0 ) ), Qgis::MessageLevel::Warning );
510 }
511 else
512 {
513 const QString modules = missingModules.join( QLatin1String( ", " ) );
514 showMessage( tr( "Reformat Code" ), tr( "The Python modules %1 are missing" ).arg( modules ), Qgis::MessageLevel::Warning );
515 }
516 }
517
518 return newText;
519}
520
522{
524
525 QAction *pyQgisHelpAction = new QAction(
526 QgsApplication::getThemeIcon( QStringLiteral( "console/iconHelpConsole.svg" ) ),
527 tr( "Search Selection in PyQGIS Documentation" ),
528 menu );
529 pyQgisHelpAction->setEnabled( hasSelectedText() );
530 connect( pyQgisHelpAction, &QAction::triggered, this, &QgsCodeEditorPython::searchSelectedTextInPyQGISDocs );
531
532 menu->addSeparator();
533 menu->addAction( pyQgisHelpAction );
534}
535
537{
538 switch ( autoCompletionSource() )
539 {
540 case AcsDocument:
541 autoCompleteFromDocument();
542 break;
543
544 case AcsAPIs:
545 autoCompleteFromAPIs();
546 break;
547
548 case AcsAll:
549 autoCompleteFromAll();
550 break;
551
552 case AcsNone:
553 break;
554 }
555}
556
557void QgsCodeEditorPython::loadAPIs( const QList<QString> &filenames )
558{
559 mAPISFilesList = filenames;
560 //QgsDebugMsgLevel( QStringLiteral( "The apis files: %1" ).arg( mAPISFilesList[0] ), 2 );
562}
563
564bool QgsCodeEditorPython::loadScript( const QString &script )
565{
566 QgsDebugMsgLevel( QStringLiteral( "The script file: %1" ).arg( script ), 2 );
567 QFile file( script );
568 if ( !file.open( QIODevice::ReadOnly ) )
569 {
570 return false;
571 }
572
573 QTextStream in( &file );
574#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
575 in.setCodec( "UTF-8" );
576#endif
577
578 setText( in.readAll().trimmed() );
579 file.close();
580
582 return true;
583}
584
586{
587 int line, index;
588 getCursorPosition( &line, &index );
589 int position = positionFromLineIndex( line, index );
590
591 // Special case: cursor at the end of the document. Style will always be Default,
592 // so we have to check the style of the previous character.
593 // It it is an unclosed string (triple string, unclosed, or comment),
594 // consider cursor is inside a string.
595 if ( position >= length() && position > 0 )
596 {
597 long style = SendScintilla( QsciScintillaBase::SCI_GETSTYLEAT, position - 1 );
598 return style == QsciLexerPython::Comment
599 || style == QsciLexerPython::TripleSingleQuotedString
600 || style == QsciLexerPython::TripleDoubleQuotedString
601 || style == QsciLexerPython::TripleSingleQuotedFString
602 || style == QsciLexerPython::TripleDoubleQuotedFString
603 || style == QsciLexerPython::UnclosedString;
604 }
605 else
606 {
607 long style = SendScintilla( QsciScintillaBase::SCI_GETSTYLEAT, position );
608 return style == QsciLexerPython::Comment
609 || style == QsciLexerPython::DoubleQuotedString
610 || style == QsciLexerPython::SingleQuotedString
611 || style == QsciLexerPython::TripleSingleQuotedString
612 || style == QsciLexerPython::TripleDoubleQuotedString
613 || style == QsciLexerPython::CommentBlock
614 || style == QsciLexerPython::UnclosedString
615 || style == QsciLexerPython::DoubleQuotedFString
616 || style == QsciLexerPython::SingleQuotedFString
617 || style == QsciLexerPython::TripleSingleQuotedFString
618 || style == QsciLexerPython::TripleDoubleQuotedFString;
619 }
620}
621
623{
624 int line, index;
625 getCursorPosition( &line, &index );
626 int position = positionFromLineIndex( line, index );
627 if ( position <= 0 )
628 {
629 return QString();
630 }
631 return text( position - 1, position );
632}
633
635{
636 int line, index;
637 getCursorPosition( &line, &index );
638 int position = positionFromLineIndex( line, index );
639 if ( position >= length() )
640 {
641 return QString();
642 }
643 return text( position, position + 1 );
644}
645
647{
649
651 return;
652
654
655 // we could potentially check for autopep8/black import here and reflect the capability accordingly.
656 // (current approach is to to always indicate this capability and raise a user-friendly warning
657 // when attempting to reformat if the libraries can't be imported)
659}
660
662{
664
666 {
667 return true;
668 }
669
670 const QString originalText = text();
671
672 const QString defineCheckSyntax = QStringLiteral(
673 "def __check_syntax(script):\n"
674 " try:\n"
675 " compile(script.encode('utf-8'), '', 'exec')\n"
676 " except SyntaxError as detail:\n"
677 " eline = detail.lineno or 1\n"
678 " eline -= 1\n"
679 " ecolumn = detail.offset or 1\n"
680 " edescr = detail.msg\n"
681 " return '!!!!'.join([str(eline), str(ecolumn), edescr])\n"
682 " return ''" );
683
684 if ( !QgsPythonRunner::run( defineCheckSyntax ) )
685 {
686 QgsDebugError( QStringLiteral( "Error running script: %1" ).arg( defineCheckSyntax ) );
687 return true;
688 }
689
690 const QString script = QStringLiteral( "__check_syntax(%1)" ).arg( QgsProcessingUtils::stringToPythonLiteral( originalText ) );
691 QString result;
692 if ( QgsPythonRunner::eval( script, result ) )
693 {
694 if ( result.size() == 0 )
695 {
696 return true;
697 }
698 else
699 {
700 const QStringList parts = result.split( QStringLiteral( "!!!!" ) );
701 if ( parts.size() == 3 )
702 {
703 const int line = parts.at( 0 ).toInt();
704 const int column = parts.at( 1 ).toInt();
705 addWarning( line, parts.at( 2 ) );
706 setCursorPosition( line, column - 1 );
707 ensureLineVisible( line );
708 }
709 return false;
710 }
711 }
712 else
713 {
714 QgsDebugError( QStringLiteral( "Error running script: %1" ).arg( script ) );
715 return true;
716 }
717}
718
720{
721 if ( !hasSelectedText() )
722 return;
723
724 QString text = selectedText();
725 text = text.replace( QLatin1String( ">>> " ), QString() ).replace( QLatin1String( "... " ), QString() ).trimmed(); // removing prompts
726 const QString version = QString( Qgis::version() ).split( '.' ).mid( 0, 2 ).join( '.' );
727 QDesktopServices::openUrl( QUrl( QStringLiteral( "https://qgis.org/pyqgis/%1/search.html?q=%2" ).arg( version, text ) ) );
728}
729
731{
732 if ( isReadOnly() )
733 {
734 return;
735 }
736
737 beginUndoAction();
738 int startLine, startPos, endLine, endPos;
739 if ( hasSelectedText() )
740 {
741 getSelection( &startLine, &startPos, &endLine, &endPos );
742 }
743 else
744 {
745 getCursorPosition( &startLine, &startPos );
746 endLine = startLine;
747 endPos = startPos;
748 }
749
750 // Check comment state and minimum indentation for each selected line
751 bool allEmpty = true;
752 bool allCommented = true;
753 int minIndentation = -1;
754 for ( int line = startLine; line <= endLine; line++ )
755 {
756 const QString stripped = text( line ).trimmed();
757 if ( !stripped.isEmpty() )
758 {
759 allEmpty = false;
760 if ( !stripped.startsWith( '#' ) )
761 {
762 allCommented = false;
763 }
764 if ( minIndentation == -1 || minIndentation > indentation( line ) )
765 {
766 minIndentation = indentation( line );
767 }
768 }
769 }
770
771 // Special case, only empty lines
772 if ( allEmpty )
773 {
774 return;
775 }
776
777 // Selection shift to keep the same selected text after a # is added/removed
778 int delta = 0;
779
780 for ( int line = startLine; line <= endLine; line++ )
781 {
782 const QString stripped = text( line ).trimmed();
783
784 // Empty line
785 if ( stripped.isEmpty() )
786 {
787 continue;
788 }
789
790 if ( !allCommented )
791 {
792 insertAt( QStringLiteral( "# " ), line, minIndentation );
793 delta = -2;
794 }
795 else
796 {
797 if ( !stripped.startsWith( '#' ) )
798 {
799 continue;
800 }
801 if ( stripped.startsWith( QLatin1String( "# " ) ) )
802 {
803 delta = 2;
804 }
805 else
806 {
807 delta = 1;
808 }
809 setSelection( line, indentation( line ), line, indentation( line ) + delta );
810 removeSelectedText();
811 }
812 }
813
814 endUndoAction();
815 setSelection( startLine, startPos - delta, endLine, endPos - delta );
816}
817
819//
820// QgsQsciLexerPython
821//
822QgsQsciLexerPython::QgsQsciLexerPython( QObject *parent )
823 : QsciLexerPython( parent )
824{
825
826}
827
828const char *QgsQsciLexerPython::keywords( int set ) const
829{
830 if ( set == 1 )
831 {
832 return "True False and as assert break class continue def del elif else except "
833 "finally for from global if import in is lambda None not or pass "
834 "raise return try while with yield async await nonlocal";
835 }
836
837 return QsciLexerPython::keywords( set );
838}
static QString version()
Version string.
Definition qgis.cpp:258
@ Warning
Warning message.
Definition qgis.h:101
@ CheckSyntax
Language supports syntax checking.
@ Reformat
Language supports automatic code reformatting.
@ ToggleComment
Language supports comment toggling.
ScriptLanguage
Scripting languages.
Definition qgis.h:3106
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.
@ DoubleQuote
Double quote color.
@ SingleQuote
Single quote 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 cursot 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.
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.
Mode
Code editor modes.
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...
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).
void setTitle(const QString &title)
Set the widget title.
void clearWarnings()
Clears all warning messages from the editor.
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.
An integer settings entry.
A string settings entry.
This class is a composition of two QSettings instances:
Definition qgssettings.h:63
QVariant value(const QString &key, const QVariant &defaultValue=QVariant(), Section section=NoSection) const
Returns the value for setting key.
#define QgsDebugMsgLevel(str, level)
Definition qgslogger.h:39
#define QgsDebugError(str)
Definition qgslogger.h:38