QGIS API Documentation 3.39.0-Master (d85f3c2a281)
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" ) );
53const 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." ) );
55
56
57QgsCodeEditorPython::QgsCodeEditorPython( QWidget *parent, const QList<QString> &filenames, Mode mode, Flags flags )
58 : QgsCodeEditor( parent,
59 QString(),
60 false,
61 false,
62 flags,
63 mode )
64 , mAPISFilesList( filenames )
65{
66 if ( !parent )
67 {
68 setTitle( tr( "Python Editor" ) );
69 }
70
71 setCaretWidth( 2 );
72
74
76}
77
82
87
89{
90 // current line
91 setEdgeMode( QsciScintilla::EdgeLine );
92 setEdgeColumn( settingMaxLineLength->value() );
94
95 setWhitespaceVisibility( QsciScintilla::WsVisibleAfterIndent );
96
97 SendScintilla( QsciScintillaBase::SCI_SETPROPERTY, "highlight.current.word", "1" );
98
99 QFont font = lexerFont();
101
102 QsciLexerPython *pyLexer = new QgsQsciLexerPython( this );
103
104 pyLexer->setIndentationWarning( QsciLexerPython::Inconsistent );
105 pyLexer->setFoldComments( true );
106 pyLexer->setFoldQuotes( true );
107
108 pyLexer->setDefaultFont( font );
109 pyLexer->setDefaultColor( defaultColor );
110 pyLexer->setDefaultPaper( lexerColor( QgsCodeEditorColorScheme::ColorRole::Background ) );
111 pyLexer->setFont( font, -1 );
112
113 font.setItalic( true );
114 pyLexer->setFont( font, QsciLexerPython::Comment );
115 pyLexer->setFont( font, QsciLexerPython::CommentBlock );
116
117 font.setItalic( false );
118 font.setBold( true );
119 pyLexer->setFont( font, QsciLexerPython::SingleQuotedString );
120 pyLexer->setFont( font, QsciLexerPython::DoubleQuotedString );
121
122 pyLexer->setColor( defaultColor, QsciLexerPython::Default );
123 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::Error ), QsciLexerPython::UnclosedString );
124 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::Class ), QsciLexerPython::ClassName );
125 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::Method ), QsciLexerPython::FunctionMethodName );
126 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::Number ), QsciLexerPython::Number );
127 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::Operator ), QsciLexerPython::Operator );
128 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::Identifier ), QsciLexerPython::Identifier );
129 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::Comment ), QsciLexerPython::Comment );
130 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::CommentBlock ), QsciLexerPython::CommentBlock );
131 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::Keyword ), QsciLexerPython::Keyword );
132 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::Decoration ), QsciLexerPython::Decorator );
133 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::SingleQuote ), QsciLexerPython::SingleQuotedString );
134 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::SingleQuote ), QsciLexerPython::SingleQuotedFString );
135 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::DoubleQuote ), QsciLexerPython::DoubleQuotedString );
136 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::DoubleQuote ), QsciLexerPython::DoubleQuotedFString );
137 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::TripleSingleQuote ), QsciLexerPython::TripleSingleQuotedString );
138 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::TripleDoubleQuote ), QsciLexerPython::TripleDoubleQuotedString );
139
140 std::unique_ptr< QsciAPIs > apis = std::make_unique< QsciAPIs >( pyLexer );
141
142 QgsSettings settings;
143 if ( mAPISFilesList.isEmpty() )
144 {
145 if ( settings.value( QStringLiteral( "pythonConsole/preloadAPI" ), true ).toBool() )
146 {
147 mPapFile = QgsApplication::pkgDataPath() + QStringLiteral( "/python/qsci_apis/PyQGIS.pap" );
148 apis->loadPrepared( mPapFile );
149 }
150 else if ( settings.value( QStringLiteral( "pythonConsole/usePreparedAPIFile" ), false ).toBool() )
151 {
152 apis->loadPrepared( settings.value( QStringLiteral( "pythonConsole/preparedAPIFile" ) ).toString() );
153 }
154 else
155 {
156 const QStringList apiPaths = settings.value( QStringLiteral( "pythonConsole/userAPI" ) ).toStringList();
157 for ( const QString &path : apiPaths )
158 {
159 if ( !QFileInfo::exists( path ) )
160 {
161 QgsDebugError( QStringLiteral( "The apis file %1 was not found" ).arg( path ) );
162 }
163 else
164 {
165 apis->load( path );
166 }
167 }
168 apis->prepare();
169 }
170 }
171 else if ( mAPISFilesList.length() == 1 && mAPISFilesList[0].right( 3 ) == QLatin1String( "pap" ) )
172 {
173 if ( !QFileInfo::exists( mAPISFilesList[0] ) )
174 {
175 QgsDebugError( QStringLiteral( "The apis file %1 not found" ).arg( mAPISFilesList.at( 0 ) ) );
176 return;
177 }
178 mPapFile = mAPISFilesList[0];
179 apis->loadPrepared( mPapFile );
180 }
181 else
182 {
183 for ( const QString &path : std::as_const( mAPISFilesList ) )
184 {
185 if ( !QFileInfo::exists( path ) )
186 {
187 QgsDebugError( QStringLiteral( "The apis file %1 was not found" ).arg( path ) );
188 }
189 else
190 {
191 apis->load( path );
192 }
193 }
194 apis->prepare();
195 }
196 if ( apis )
197 pyLexer->setAPIs( apis.release() );
198
199 setLexer( pyLexer );
200
201 const int threshold = settings.value( QStringLiteral( "pythonConsole/autoCompThreshold" ), 2 ).toInt();
202 setAutoCompletionThreshold( threshold );
203 if ( !settings.value( "pythonConsole/autoCompleteEnabled", true ).toBool() )
204 {
205 setAutoCompletionSource( AcsNone );
206 }
207 else
208 {
209 const QString autoCompleteSource = settings.value( QStringLiteral( "pythonConsole/autoCompleteSource" ), QStringLiteral( "fromAPI" ) ).toString();
210 if ( autoCompleteSource == QLatin1String( "fromDoc" ) )
211 setAutoCompletionSource( AcsDocument );
212 else if ( autoCompleteSource == QLatin1String( "fromDocAPI" ) )
213 setAutoCompletionSource( AcsAll );
214 else
215 setAutoCompletionSource( AcsAPIs );
216 }
217
218 setLineNumbersVisible( true );
219 setIndentationsUseTabs( false );
220 setIndentationGuides( true );
221
223}
224
226{
227 // If editor is readOnly, use the default implementation
228 if ( isReadOnly() )
229 {
230 return QgsCodeEditor::keyPressEvent( event );
231 }
232
233 const QgsSettings settings;
234
235 bool autoCloseBracket = settings.value( QStringLiteral( "/pythonConsole/autoCloseBracket" ), true ).toBool();
236 bool autoSurround = settings.value( QStringLiteral( "/pythonConsole/autoSurround" ), true ).toBool();
237 bool autoInsertImport = settings.value( QStringLiteral( "/pythonConsole/autoInsertImport" ), false ).toBool();
238
239 // Update calltips when cursor position changes with left and right keys
240 if ( event->key() == Qt::Key_Left ||
241 event->key() == Qt::Key_Right ||
242 event->key() == Qt::Key_Up ||
243 event->key() == Qt::Key_Down )
244 {
246 callTip();
247 return;
248 }
249
250 // Get entered text and cursor position
251 const QString eText = event->text();
252 int line, column;
253 getCursorPosition( &line, &column );
254
255 // If some text is selected and user presses an opening character
256 // surround the selection with the opening-closing pair
257 if ( hasSelectedText() && autoSurround )
258 {
259 if ( sCompletionPairs.contains( eText ) )
260 {
261 int startLine, startPos, endLine, endPos;
262 getSelection( &startLine, &startPos, &endLine, &endPos );
263
264 // Special case for Multi line quotes (insert triple quotes)
265 if ( startLine != endLine && ( eText == "\"" || eText == "'" ) )
266 {
267 replaceSelectedText(
268 QString( "%1%1%1%2%3%3%3" ).arg( eText, selectedText(), sCompletionPairs[eText] )
269 );
270 setSelection( startLine, startPos + 3, endLine, endPos + 3 );
271 }
272 else
273 {
274 replaceSelectedText(
275 QString( "%1%2%3" ).arg( eText, selectedText(), sCompletionPairs[eText] )
276 );
277 setSelection( startLine, startPos + 1, endLine, endPos + 1 );
278 }
279 event->accept();
280 return;
281 }
282 else if ( sCompletionSingleCharacters.contains( eText ) )
283 {
284 int startLine, startPos, endLine, endPos;
285 getSelection( &startLine, &startPos, &endLine, &endPos );
286 replaceSelectedText(
287 QString( "%1%2%1" ).arg( eText, selectedText() )
288 );
289 setSelection( startLine, startPos + 1, endLine, endPos + 1 );
290 event->accept();
291 return;
292 }
293 }
294
295 // No selected text
296 else
297 {
298 // Automatically insert "import" after "from xxx " if option is enabled
299 if ( autoInsertImport && eText == " " )
300 {
301 const QString lineText = text( line );
302 const thread_local QRegularExpression re( QStringLiteral( "^from [\\w.]+$" ) );
303 if ( re.match( lineText.trimmed() ).hasMatch() )
304 {
305 insert( QStringLiteral( " import" ) );
306 setCursorPosition( line, column + 7 );
307 return QgsCodeEditor::keyPressEvent( event );
308 }
309 }
310
311 // Handle automatic bracket insertion/deletion if option is enabled
312 else if ( autoCloseBracket )
313 {
314 const QString prevChar = characterBeforeCursor();
315 const QString nextChar = characterAfterCursor();
316
317 // When backspace is pressed inside an opening/closing pair, remove both characters
318 if ( event->key() == Qt::Key_Backspace )
319 {
320 if ( sCompletionPairs.contains( prevChar ) && sCompletionPairs[prevChar] == nextChar )
321 {
322 setSelection( line, column - 1, line, column + 1 );
323 removeSelectedText();
324 event->accept();
325 }
326 else
327 {
329 }
330
331 // Update calltips (cursor position has changed)
332 callTip();
333 return;
334 }
335
336 // When closing character is entered inside an opening/closing pair, shift the cursor
337 else if ( sCompletionPairs.key( eText ) != "" && nextChar == eText )
338 {
339 setCursorPosition( line, column + 1 );
340 event->accept();
341
342 // Will hide calltips when a closing parenthesis is entered
343 callTip();
344 return;
345 }
346
347 // Else, if not inside a string or comment and an opening character
348 // is entered, also insert the closing character, provided the next
349 // character is a space, a colon, or a closing character
351 && sCompletionPairs.contains( eText )
352 && ( nextChar.isEmpty() || nextChar.at( 0 ).isSpace() || nextChar == ":" || sCompletionPairs.key( nextChar ) != "" )
353 )
354 {
355 // Check if user is not entering triple quotes
356 if ( !( ( eText == "\"" || eText == "'" ) && prevChar == eText ) )
357 {
359 insert( sCompletionPairs[eText] );
360 event->accept();
361 return;
362 }
363 }
364 }
365 }
366
367 // Let QgsCodeEditor handle the keyboard event
368 return QgsCodeEditor::keyPressEvent( event );
369}
370
371QString QgsCodeEditorPython::reformatCodeString( const QString &string )
372{
374 {
375 return string;
376 }
377
378 const QString formatter = settingCodeFormatter->value();
379 const int maxLineLength = settingMaxLineLength->value();
380
381 QString newText = string;
382
383 QStringList missingModules;
384
385 if ( settingSortImports->value() )
386 {
387 const QString defineSortImports = QStringLiteral(
388 "def __qgis_sort_imports(script):\n"
389 " try:\n"
390 " import isort\n"
391 " except ImportError:\n"
392 " return '_ImportError'\n"
393 " options={'line_length': %1, 'profile': '%2', 'known_first_party': ['qgis', 'console', 'processing', 'plugins']}\n"
394 " return isort.code(script, **options)\n" )
395 .arg( maxLineLength )
396 .arg( formatter == QLatin1String( "black" ) ? QStringLiteral( "black" ) : QString() );
397
398 if ( !QgsPythonRunner::run( defineSortImports ) )
399 {
400 QgsDebugError( QStringLiteral( "Error running script: %1" ).arg( defineSortImports ) );
401 return string;
402 }
403
404 const QString script = QStringLiteral( "__qgis_sort_imports(%1)" ).arg( QgsProcessingUtils::stringToPythonLiteral( newText ) );
405 QString result;
406 if ( QgsPythonRunner::eval( script, result ) )
407 {
408 if ( result == QLatin1String( "_ImportError" ) )
409 {
410 missingModules << QStringLiteral( "isort" );
411 }
412 else
413 {
414 newText = result;
415 }
416 }
417 else
418 {
419 QgsDebugError( QStringLiteral( "Error running script: %1" ).arg( script ) );
420 return newText;
421 }
422 }
423
424 if ( formatter == QLatin1String( "autopep8" ) )
425 {
426 const int level = settingAutopep8Level->value();
427
428 const QString defineReformat = QStringLiteral(
429 "def __qgis_reformat(script):\n"
430 " try:\n"
431 " import autopep8\n"
432 " except ImportError:\n"
433 " return '_ImportError'\n"
434 " options={'aggressive': %1, 'max_line_length': %2}\n"
435 " return autopep8.fix_code(script, options=options)\n" )
436 .arg( level )
437 .arg( maxLineLength );
438
439 if ( !QgsPythonRunner::run( defineReformat ) )
440 {
441 QgsDebugError( QStringLiteral( "Error running script: %1" ).arg( defineReformat ) );
442 return newText;
443 }
444
445 const QString script = QStringLiteral( "__qgis_reformat(%1)" ).arg( QgsProcessingUtils::stringToPythonLiteral( newText ) );
446 QString result;
447 if ( QgsPythonRunner::eval( script, result ) )
448 {
449 if ( result == QLatin1String( "_ImportError" ) )
450 {
451 missingModules << QStringLiteral( "autopep8" );
452 }
453 else
454 {
455 newText = result;
456 }
457 }
458 else
459 {
460 QgsDebugError( QStringLiteral( "Error running script: %1" ).arg( script ) );
461 return newText;
462 }
463 }
464 else if ( formatter == QLatin1String( "black" ) )
465 {
466 const bool normalize = settingBlackNormalizeQuotes->value();
467
468 if ( !checkSyntax() )
469 {
470 showMessage( tr( "Reformat Code" ), tr( "Code formatting failed -- the code contains syntax errors" ), Qgis::MessageLevel::Warning );
471 return newText;
472 }
473
474 const QString defineReformat = QStringLiteral(
475 "def __qgis_reformat(script):\n"
476 " try:\n"
477 " import black\n"
478 " except ImportError:\n"
479 " return '_ImportError'\n"
480 " options={'string_normalization': %1, 'line_length': %2}\n"
481 " return black.format_str(script, mode=black.Mode(**options))\n" )
483 .arg( maxLineLength );
484
485 if ( !QgsPythonRunner::run( defineReformat ) )
486 {
487 QgsDebugError( QStringLiteral( "Error running script: %1" ).arg( defineReformat ) );
488 return string;
489 }
490
491 const QString script = QStringLiteral( "__qgis_reformat(%1)" ).arg( QgsProcessingUtils::stringToPythonLiteral( newText ) );
492 QString result;
493 if ( QgsPythonRunner::eval( script, result ) )
494 {
495 if ( result == QLatin1String( "_ImportError" ) )
496 {
497 missingModules << QStringLiteral( "black" );
498 }
499 else
500 {
501 newText = result;
502 }
503 }
504 else
505 {
506 QgsDebugError( QStringLiteral( "Error running script: %1" ).arg( script ) );
507 return newText;
508 }
509 }
510
511 if ( !missingModules.empty() )
512 {
513 if ( missingModules.size() == 1 )
514 {
515 showMessage( tr( "Reformat Code" ), tr( "The Python module %1 is missing" ).arg( missingModules.at( 0 ) ), Qgis::MessageLevel::Warning );
516 }
517 else
518 {
519 const QString modules = missingModules.join( QLatin1String( ", " ) );
520 showMessage( tr( "Reformat Code" ), tr( "The Python modules %1 are missing" ).arg( modules ), Qgis::MessageLevel::Warning );
521 }
522 }
523
524 return newText;
525}
526
528{
530
531 QAction *pyQgisHelpAction = new QAction(
532 QgsApplication::getThemeIcon( QStringLiteral( "console/iconHelpConsole.svg" ) ),
533 tr( "Search Selection in PyQGIS Documentation" ),
534 menu );
535 pyQgisHelpAction->setEnabled( hasSelectedText() );
536 connect( pyQgisHelpAction, &QAction::triggered, this, &QgsCodeEditorPython::searchSelectedTextInPyQGISDocs );
537
538 menu->addSeparator();
539 menu->addAction( pyQgisHelpAction );
540}
541
543{
544 switch ( autoCompletionSource() )
545 {
546 case AcsDocument:
547 autoCompleteFromDocument();
548 break;
549
550 case AcsAPIs:
551 autoCompleteFromAPIs();
552 break;
553
554 case AcsAll:
555 autoCompleteFromAll();
556 break;
557
558 case AcsNone:
559 break;
560 }
561}
562
563void QgsCodeEditorPython::loadAPIs( const QList<QString> &filenames )
564{
565 mAPISFilesList = filenames;
566 //QgsDebugMsgLevel( QStringLiteral( "The apis files: %1" ).arg( mAPISFilesList[0] ), 2 );
568}
569
570bool QgsCodeEditorPython::loadScript( const QString &script )
571{
572 QgsDebugMsgLevel( QStringLiteral( "The script file: %1" ).arg( script ), 2 );
573 QFile file( script );
574 if ( !file.open( QIODevice::ReadOnly ) )
575 {
576 return false;
577 }
578
579 QTextStream in( &file );
580#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
581 in.setCodec( "UTF-8" );
582#endif
583
584 setText( in.readAll().trimmed() );
585 file.close();
586
588 return true;
589}
590
592{
593 int position = linearPosition();
594
595 // Special case: cursor at the end of the document. Style will always be Default,
596 // so we have to check the style of the previous character.
597 // It it is an unclosed string (triple string, unclosed, or comment),
598 // consider cursor is inside a string.
599 if ( position >= length() && position > 0 )
600 {
601 long style = SendScintilla( QsciScintillaBase::SCI_GETSTYLEAT, position - 1 );
602 return style == QsciLexerPython::Comment
603 || style == QsciLexerPython::TripleSingleQuotedString
604 || style == QsciLexerPython::TripleDoubleQuotedString
605 || style == QsciLexerPython::TripleSingleQuotedFString
606 || style == QsciLexerPython::TripleDoubleQuotedFString
607 || style == QsciLexerPython::UnclosedString;
608 }
609 else
610 {
611 long style = SendScintilla( QsciScintillaBase::SCI_GETSTYLEAT, position );
612 return style == QsciLexerPython::Comment
613 || style == QsciLexerPython::DoubleQuotedString
614 || style == QsciLexerPython::SingleQuotedString
615 || style == QsciLexerPython::TripleSingleQuotedString
616 || style == QsciLexerPython::TripleDoubleQuotedString
617 || style == QsciLexerPython::CommentBlock
618 || style == QsciLexerPython::UnclosedString
619 || style == QsciLexerPython::DoubleQuotedFString
620 || style == QsciLexerPython::SingleQuotedFString
621 || style == QsciLexerPython::TripleSingleQuotedFString
622 || style == QsciLexerPython::TripleDoubleQuotedFString;
623 }
624}
625
627{
628 int position = linearPosition();
629 if ( position <= 0 )
630 {
631 return QString();
632 }
633 return text( position - 1, position );
634}
635
637{
638 int position = linearPosition();
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:156
@ CheckSyntax
Language supports syntax checking.
@ Reformat
Language supports automatic code reformatting.
@ ToggleComment
Language supports comment toggling.
ScriptLanguage
Scripting languages.
Definition qgis.h:4151
QFlags< ScriptLanguageCapability > ScriptLanguageCapabilities
Script language capabilities.
Definition qgis.h:4186
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 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.
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...
QFlags< Flag > Flags
Flags controlling behavior of code editor.
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 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:64
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