QGIS API Documentation 3.39.0-Master (47f7b3a4989)
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::Class ), QsciLexerPython::ClassName );
124 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::Method ), QsciLexerPython::FunctionMethodName );
125 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::Number ), QsciLexerPython::Number );
126 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::Operator ), QsciLexerPython::Operator );
127 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::Identifier ), QsciLexerPython::Identifier );
128 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::Comment ), QsciLexerPython::Comment );
129 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::CommentBlock ), QsciLexerPython::CommentBlock );
130 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::Keyword ), QsciLexerPython::Keyword );
131 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::Decoration ), QsciLexerPython::Decorator );
132 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::SingleQuote ), QsciLexerPython::SingleQuotedString );
133 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::DoubleQuote ), QsciLexerPython::DoubleQuotedString );
134 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::TripleSingleQuote ), QsciLexerPython::TripleSingleQuotedString );
135 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::TripleDoubleQuote ), QsciLexerPython::TripleDoubleQuotedString );
136
137 std::unique_ptr< QsciAPIs > apis = std::make_unique< QsciAPIs >( pyLexer );
138
139 QgsSettings settings;
140 if ( mAPISFilesList.isEmpty() )
141 {
142 if ( settings.value( QStringLiteral( "pythonConsole/preloadAPI" ), true ).toBool() )
143 {
144 mPapFile = QgsApplication::pkgDataPath() + QStringLiteral( "/python/qsci_apis/PyQGIS.pap" );
145 apis->loadPrepared( mPapFile );
146 }
147 else if ( settings.value( QStringLiteral( "pythonConsole/usePreparedAPIFile" ), false ).toBool() )
148 {
149 apis->loadPrepared( settings.value( QStringLiteral( "pythonConsole/preparedAPIFile" ) ).toString() );
150 }
151 else
152 {
153 const QStringList apiPaths = settings.value( QStringLiteral( "pythonConsole/userAPI" ) ).toStringList();
154 for ( const QString &path : apiPaths )
155 {
156 if ( !QFileInfo::exists( path ) )
157 {
158 QgsDebugError( QStringLiteral( "The apis file %1 was not found" ).arg( path ) );
159 }
160 else
161 {
162 apis->load( path );
163 }
164 }
165 apis->prepare();
166 }
167 }
168 else if ( mAPISFilesList.length() == 1 && mAPISFilesList[0].right( 3 ) == QLatin1String( "pap" ) )
169 {
170 if ( !QFileInfo::exists( mAPISFilesList[0] ) )
171 {
172 QgsDebugError( QStringLiteral( "The apis file %1 not found" ).arg( mAPISFilesList.at( 0 ) ) );
173 return;
174 }
175 mPapFile = mAPISFilesList[0];
176 apis->loadPrepared( mPapFile );
177 }
178 else
179 {
180 for ( const QString &path : std::as_const( mAPISFilesList ) )
181 {
182 if ( !QFileInfo::exists( path ) )
183 {
184 QgsDebugError( QStringLiteral( "The apis file %1 was not found" ).arg( path ) );
185 }
186 else
187 {
188 apis->load( path );
189 }
190 }
191 apis->prepare();
192 }
193 if ( apis )
194 pyLexer->setAPIs( apis.release() );
195
196 setLexer( pyLexer );
197
198 const int threshold = settings.value( QStringLiteral( "pythonConsole/autoCompThreshold" ), 2 ).toInt();
199 setAutoCompletionThreshold( threshold );
200 if ( !settings.value( "pythonConsole/autoCompleteEnabled", true ).toBool() )
201 {
202 setAutoCompletionSource( AcsNone );
203 }
204 else
205 {
206 const QString autoCompleteSource = settings.value( QStringLiteral( "pythonConsole/autoCompleteSource" ), QStringLiteral( "fromAPI" ) ).toString();
207 if ( autoCompleteSource == QLatin1String( "fromDoc" ) )
208 setAutoCompletionSource( AcsDocument );
209 else if ( autoCompleteSource == QLatin1String( "fromDocAPI" ) )
210 setAutoCompletionSource( AcsAll );
211 else
212 setAutoCompletionSource( AcsAPIs );
213 }
214
215 setLineNumbersVisible( true );
216 setIndentationsUseTabs( false );
217 setIndentationGuides( true );
218
220}
221
223{
224 // If editor is readOnly, use the default implementation
225 if ( isReadOnly() )
226 {
227 return QgsCodeEditor::keyPressEvent( event );
228 }
229
230 const QgsSettings settings;
231
232 bool autoCloseBracket = settings.value( QStringLiteral( "/pythonConsole/autoCloseBracket" ), true ).toBool();
233 bool autoSurround = settings.value( QStringLiteral( "/pythonConsole/autoSurround" ), true ).toBool();
234 bool autoInsertImport = settings.value( QStringLiteral( "/pythonConsole/autoInsertImport" ), false ).toBool();
235
236 // Update calltips when cursor position changes with left and right keys
237 if ( event->key() == Qt::Key_Left ||
238 event->key() == Qt::Key_Right ||
239 event->key() == Qt::Key_Up ||
240 event->key() == Qt::Key_Down )
241 {
243 callTip();
244 return;
245 }
246
247 // Get entered text and cursor position
248 const QString eText = event->text();
249 int line, column;
250 getCursorPosition( &line, &column );
251
252 // If some text is selected and user presses an opening character
253 // surround the selection with the opening-closing pair
254 if ( hasSelectedText() && autoSurround )
255 {
256 if ( sCompletionPairs.contains( eText ) )
257 {
258 int startLine, startPos, endLine, endPos;
259 getSelection( &startLine, &startPos, &endLine, &endPos );
260
261 // Special case for Multi line quotes (insert triple quotes)
262 if ( startLine != endLine && ( eText == "\"" || eText == "'" ) )
263 {
264 replaceSelectedText(
265 QString( "%1%1%1%2%3%3%3" ).arg( eText, selectedText(), sCompletionPairs[eText] )
266 );
267 setSelection( startLine, startPos + 3, endLine, endPos + 3 );
268 }
269 else
270 {
271 replaceSelectedText(
272 QString( "%1%2%3" ).arg( eText, selectedText(), sCompletionPairs[eText] )
273 );
274 setSelection( startLine, startPos + 1, endLine, endPos + 1 );
275 }
276 event->accept();
277 return;
278 }
279 else if ( sCompletionSingleCharacters.contains( eText ) )
280 {
281 int startLine, startPos, endLine, endPos;
282 getSelection( &startLine, &startPos, &endLine, &endPos );
283 replaceSelectedText(
284 QString( "%1%2%1" ).arg( eText, selectedText() )
285 );
286 setSelection( startLine, startPos + 1, endLine, endPos + 1 );
287 event->accept();
288 return;
289 }
290 }
291
292 // No selected text
293 else
294 {
295 // Automatically insert "import" after "from xxx " if option is enabled
296 if ( autoInsertImport && eText == " " )
297 {
298 const QString lineText = text( line );
299 const thread_local QRegularExpression re( QStringLiteral( "^from [\\w.]+$" ) );
300 if ( re.match( lineText.trimmed() ).hasMatch() )
301 {
302 insert( QStringLiteral( " import" ) );
303 setCursorPosition( line, column + 7 );
304 return QgsCodeEditor::keyPressEvent( event );
305 }
306 }
307
308 // Handle automatic bracket insertion/deletion if option is enabled
309 else if ( autoCloseBracket )
310 {
311 const QString prevChar = characterBeforeCursor();
312 const QString nextChar = characterAfterCursor();
313
314 // When backspace is pressed inside an opening/closing pair, remove both characters
315 if ( event->key() == Qt::Key_Backspace )
316 {
317 if ( sCompletionPairs.contains( prevChar ) && sCompletionPairs[prevChar] == nextChar )
318 {
319 setSelection( line, column - 1, line, column + 1 );
320 removeSelectedText();
321 event->accept();
322 }
323 else
324 {
326 }
327
328 // Update calltips (cursor position has changed)
329 callTip();
330 return;
331 }
332
333 // When closing character is entered inside an opening/closing pair, shift the cursor
334 else if ( sCompletionPairs.key( eText ) != "" && nextChar == eText )
335 {
336 setCursorPosition( line, column + 1 );
337 event->accept();
338
339 // Will hide calltips when a closing parenthesis is entered
340 callTip();
341 return;
342 }
343
344 // Else, if not inside a string or comment and an opening character
345 // is entered, also insert the closing character, provided the next
346 // character is a space, a colon, or a closing character
348 && sCompletionPairs.contains( eText )
349 && ( nextChar.isEmpty() || nextChar.at( 0 ).isSpace() || nextChar == ":" || sCompletionPairs.key( nextChar ) != "" )
350 )
351 {
352 // Check if user is not entering triple quotes
353 if ( !( ( eText == "\"" || eText == "'" ) && prevChar == eText ) )
354 {
356 insert( sCompletionPairs[eText] );
357 event->accept();
358 return;
359 }
360 }
361 }
362 }
363
364 // Let QgsCodeEditor handle the keyboard event
365 return QgsCodeEditor::keyPressEvent( event );
366}
367
368QString QgsCodeEditorPython::reformatCodeString( const QString &string )
369{
371 {
372 return string;
373 }
374
375 const QString formatter = settingCodeFormatter->value();
376 const int maxLineLength = settingMaxLineLength->value();
377
378 QString newText = string;
379
380 QStringList missingModules;
381
382 if ( settingSortImports->value() )
383 {
384 const QString defineSortImports = QStringLiteral(
385 "def __qgis_sort_imports(script):\n"
386 " try:\n"
387 " import isort\n"
388 " except ImportError:\n"
389 " return '_ImportError'\n"
390 " options={'line_length': %1, 'profile': '%2', 'known_first_party': ['qgis', 'console', 'processing', 'plugins']}\n"
391 " return isort.code(script, **options)\n" )
392 .arg( maxLineLength )
393 .arg( formatter == QLatin1String( "black" ) ? QStringLiteral( "black" ) : QString() );
394
395 if ( !QgsPythonRunner::run( defineSortImports ) )
396 {
397 QgsDebugError( QStringLiteral( "Error running script: %1" ).arg( defineSortImports ) );
398 return string;
399 }
400
401 const QString script = QStringLiteral( "__qgis_sort_imports(%1)" ).arg( QgsProcessingUtils::stringToPythonLiteral( newText ) );
402 QString result;
403 if ( QgsPythonRunner::eval( script, result ) )
404 {
405 if ( result == QLatin1String( "_ImportError" ) )
406 {
407 missingModules << QStringLiteral( "isort" );
408 }
409 else
410 {
411 newText = result;
412 }
413 }
414 else
415 {
416 QgsDebugError( QStringLiteral( "Error running script: %1" ).arg( script ) );
417 return newText;
418 }
419 }
420
421 if ( formatter == QLatin1String( "autopep8" ) )
422 {
423 const int level = settingAutopep8Level->value();
424
425 const QString defineReformat = QStringLiteral(
426 "def __qgis_reformat(script):\n"
427 " try:\n"
428 " import autopep8\n"
429 " except ImportError:\n"
430 " return '_ImportError'\n"
431 " options={'aggressive': %1, 'max_line_length': %2}\n"
432 " return autopep8.fix_code(script, options=options)\n" )
433 .arg( level )
434 .arg( maxLineLength );
435
436 if ( !QgsPythonRunner::run( defineReformat ) )
437 {
438 QgsDebugError( QStringLiteral( "Error running script: %1" ).arg( defineReformat ) );
439 return newText;
440 }
441
442 const QString script = QStringLiteral( "__qgis_reformat(%1)" ).arg( QgsProcessingUtils::stringToPythonLiteral( newText ) );
443 QString result;
444 if ( QgsPythonRunner::eval( script, result ) )
445 {
446 if ( result == QLatin1String( "_ImportError" ) )
447 {
448 missingModules << QStringLiteral( "autopep8" );
449 }
450 else
451 {
452 newText = result;
453 }
454 }
455 else
456 {
457 QgsDebugError( QStringLiteral( "Error running script: %1" ).arg( script ) );
458 return newText;
459 }
460 }
461 else if ( formatter == QLatin1String( "black" ) )
462 {
463 const bool normalize = settingBlackNormalizeQuotes->value();
464
465 if ( !checkSyntax() )
466 {
467 showMessage( tr( "Reformat Code" ), tr( "Code formatting failed -- the code contains syntax errors" ), Qgis::MessageLevel::Warning );
468 return newText;
469 }
470
471 const QString defineReformat = QStringLiteral(
472 "def __qgis_reformat(script):\n"
473 " try:\n"
474 " import black\n"
475 " except ImportError:\n"
476 " return '_ImportError'\n"
477 " options={'string_normalization': %1, 'line_length': %2}\n"
478 " return black.format_str(script, mode=black.Mode(**options))\n" )
480 .arg( maxLineLength );
481
482 if ( !QgsPythonRunner::run( defineReformat ) )
483 {
484 QgsDebugError( QStringLiteral( "Error running script: %1" ).arg( defineReformat ) );
485 return string;
486 }
487
488 const QString script = QStringLiteral( "__qgis_reformat(%1)" ).arg( QgsProcessingUtils::stringToPythonLiteral( newText ) );
489 QString result;
490 if ( QgsPythonRunner::eval( script, result ) )
491 {
492 if ( result == QLatin1String( "_ImportError" ) )
493 {
494 missingModules << QStringLiteral( "black" );
495 }
496 else
497 {
498 newText = result;
499 }
500 }
501 else
502 {
503 QgsDebugError( QStringLiteral( "Error running script: %1" ).arg( script ) );
504 return newText;
505 }
506 }
507
508 if ( !missingModules.empty() )
509 {
510 if ( missingModules.size() == 1 )
511 {
512 showMessage( tr( "Reformat Code" ), tr( "The Python module %1 is missing" ).arg( missingModules.at( 0 ) ), Qgis::MessageLevel::Warning );
513 }
514 else
515 {
516 const QString modules = missingModules.join( QLatin1String( ", " ) );
517 showMessage( tr( "Reformat Code" ), tr( "The Python modules %1 are missing" ).arg( modules ), Qgis::MessageLevel::Warning );
518 }
519 }
520
521 return newText;
522}
523
525{
527
528 QAction *pyQgisHelpAction = new QAction(
529 QgsApplication::getThemeIcon( QStringLiteral( "console/iconHelpConsole.svg" ) ),
530 tr( "Search Selection in PyQGIS Documentation" ),
531 menu );
532 pyQgisHelpAction->setEnabled( hasSelectedText() );
533 connect( pyQgisHelpAction, &QAction::triggered, this, &QgsCodeEditorPython::searchSelectedTextInPyQGISDocs );
534
535 menu->addSeparator();
536 menu->addAction( pyQgisHelpAction );
537}
538
540{
541 switch ( autoCompletionSource() )
542 {
543 case AcsDocument:
544 autoCompleteFromDocument();
545 break;
546
547 case AcsAPIs:
548 autoCompleteFromAPIs();
549 break;
550
551 case AcsAll:
552 autoCompleteFromAll();
553 break;
554
555 case AcsNone:
556 break;
557 }
558}
559
560void QgsCodeEditorPython::loadAPIs( const QList<QString> &filenames )
561{
562 mAPISFilesList = filenames;
563 //QgsDebugMsgLevel( QStringLiteral( "The apis files: %1" ).arg( mAPISFilesList[0] ), 2 );
565}
566
567bool QgsCodeEditorPython::loadScript( const QString &script )
568{
569 QgsDebugMsgLevel( QStringLiteral( "The script file: %1" ).arg( script ), 2 );
570 QFile file( script );
571 if ( !file.open( QIODevice::ReadOnly ) )
572 {
573 return false;
574 }
575
576 QTextStream in( &file );
577#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
578 in.setCodec( "UTF-8" );
579#endif
580
581 setText( in.readAll().trimmed() );
582 file.close();
583
585 return true;
586}
587
589{
590 int position = linearPosition();
591
592 // Special case: cursor at the end of the document. Style will always be Default,
593 // so we have to check the style of the previous character.
594 // It it is an unclosed string (triple string, unclosed, or comment),
595 // consider cursor is inside a string.
596 if ( position >= length() && position > 0 )
597 {
598 long style = SendScintilla( QsciScintillaBase::SCI_GETSTYLEAT, position - 1 );
599 return style == QsciLexerPython::Comment
600 || style == QsciLexerPython::TripleSingleQuotedString
601 || style == QsciLexerPython::TripleDoubleQuotedString
602 || style == QsciLexerPython::TripleSingleQuotedFString
603 || style == QsciLexerPython::TripleDoubleQuotedFString
604 || style == QsciLexerPython::UnclosedString;
605 }
606 else
607 {
608 long style = SendScintilla( QsciScintillaBase::SCI_GETSTYLEAT, position );
609 return style == QsciLexerPython::Comment
610 || style == QsciLexerPython::DoubleQuotedString
611 || style == QsciLexerPython::SingleQuotedString
612 || style == QsciLexerPython::TripleSingleQuotedString
613 || style == QsciLexerPython::TripleDoubleQuotedString
614 || style == QsciLexerPython::CommentBlock
615 || style == QsciLexerPython::UnclosedString
616 || style == QsciLexerPython::DoubleQuotedFString
617 || style == QsciLexerPython::SingleQuotedFString
618 || style == QsciLexerPython::TripleSingleQuotedFString
619 || style == QsciLexerPython::TripleDoubleQuotedFString;
620 }
621}
622
624{
625 int position = linearPosition();
626 if ( position <= 0 )
627 {
628 return QString();
629 }
630 return text( position - 1, position );
631}
632
634{
635 int position = linearPosition();
636 if ( position >= length() )
637 {
638 return QString();
639 }
640 return text( position, position + 1 );
641}
642
644{
646
648 return;
649
651
652 // we could potentially check for autopep8/black import here and reflect the capability accordingly.
653 // (current approach is to to always indicate this capability and raise a user-friendly warning
654 // when attempting to reformat if the libraries can't be imported)
656}
657
659{
661
663 {
664 return true;
665 }
666
667 const QString originalText = text();
668
669 const QString defineCheckSyntax = QStringLiteral(
670 "def __check_syntax(script):\n"
671 " try:\n"
672 " compile(script.encode('utf-8'), '', 'exec')\n"
673 " except SyntaxError as detail:\n"
674 " eline = detail.lineno or 1\n"
675 " eline -= 1\n"
676 " ecolumn = detail.offset or 1\n"
677 " edescr = detail.msg\n"
678 " return '!!!!'.join([str(eline), str(ecolumn), edescr])\n"
679 " return ''" );
680
681 if ( !QgsPythonRunner::run( defineCheckSyntax ) )
682 {
683 QgsDebugError( QStringLiteral( "Error running script: %1" ).arg( defineCheckSyntax ) );
684 return true;
685 }
686
687 const QString script = QStringLiteral( "__check_syntax(%1)" ).arg( QgsProcessingUtils::stringToPythonLiteral( originalText ) );
688 QString result;
689 if ( QgsPythonRunner::eval( script, result ) )
690 {
691 if ( result.size() == 0 )
692 {
693 return true;
694 }
695 else
696 {
697 const QStringList parts = result.split( QStringLiteral( "!!!!" ) );
698 if ( parts.size() == 3 )
699 {
700 const int line = parts.at( 0 ).toInt();
701 const int column = parts.at( 1 ).toInt();
702 addWarning( line, parts.at( 2 ) );
703 setCursorPosition( line, column - 1 );
704 ensureLineVisible( line );
705 }
706 return false;
707 }
708 }
709 else
710 {
711 QgsDebugError( QStringLiteral( "Error running script: %1" ).arg( script ) );
712 return true;
713 }
714}
715
717{
718 if ( !hasSelectedText() )
719 return;
720
721 QString text = selectedText();
722 text = text.replace( QLatin1String( ">>> " ), QString() ).replace( QLatin1String( "... " ), QString() ).trimmed(); // removing prompts
723 const QString version = QString( Qgis::version() ).split( '.' ).mid( 0, 2 ).join( '.' );
724 QDesktopServices::openUrl( QUrl( QStringLiteral( "https://qgis.org/pyqgis/%1/search.html?q=%2" ).arg( version, text ) ) );
725}
726
728{
729 if ( isReadOnly() )
730 {
731 return;
732 }
733
734 beginUndoAction();
735 int startLine, startPos, endLine, endPos;
736 if ( hasSelectedText() )
737 {
738 getSelection( &startLine, &startPos, &endLine, &endPos );
739 }
740 else
741 {
742 getCursorPosition( &startLine, &startPos );
743 endLine = startLine;
744 endPos = startPos;
745 }
746
747 // Check comment state and minimum indentation for each selected line
748 bool allEmpty = true;
749 bool allCommented = true;
750 int minIndentation = -1;
751 for ( int line = startLine; line <= endLine; line++ )
752 {
753 const QString stripped = text( line ).trimmed();
754 if ( !stripped.isEmpty() )
755 {
756 allEmpty = false;
757 if ( !stripped.startsWith( '#' ) )
758 {
759 allCommented = false;
760 }
761 if ( minIndentation == -1 || minIndentation > indentation( line ) )
762 {
763 minIndentation = indentation( line );
764 }
765 }
766 }
767
768 // Special case, only empty lines
769 if ( allEmpty )
770 {
771 return;
772 }
773
774 // Selection shift to keep the same selected text after a # is added/removed
775 int delta = 0;
776
777 for ( int line = startLine; line <= endLine; line++ )
778 {
779 const QString stripped = text( line ).trimmed();
780
781 // Empty line
782 if ( stripped.isEmpty() )
783 {
784 continue;
785 }
786
787 if ( !allCommented )
788 {
789 insertAt( QStringLiteral( "# " ), line, minIndentation );
790 delta = -2;
791 }
792 else
793 {
794 if ( !stripped.startsWith( '#' ) )
795 {
796 continue;
797 }
798 if ( stripped.startsWith( QLatin1String( "# " ) ) )
799 {
800 delta = 2;
801 }
802 else
803 {
804 delta = 1;
805 }
806 setSelection( line, indentation( line ), line, indentation( line ) + delta );
807 removeSelectedText();
808 }
809 }
810
811 endUndoAction();
812 setSelection( startLine, startPos - delta, endLine, endPos - delta );
813}
814
816//
817// QgsQsciLexerPython
818//
819QgsQsciLexerPython::QgsQsciLexerPython( QObject *parent )
820 : QsciLexerPython( parent )
821{
822
823}
824
825const char *QgsQsciLexerPython::keywords( int set ) const
826{
827 if ( set == 1 )
828 {
829 return "True False and as assert break class continue def del elif else except "
830 "finally for from global if import in is lambda None not or pass "
831 "raise return try while with yield async await nonlocal";
832 }
833
834 return QsciLexerPython::keywords( set );
835}
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:3889
QFlags< ScriptLanguageCapability > ScriptLanguageCapabilities
Script language capabilities.
Definition qgis.h:3924
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