QGIS API Documentation 3.40.0-Bratislava (b56115d8743)
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 // Get entered text and cursor position
240 const QString eText = event->text();
241 int line, column;
242 getCursorPosition( &line, &column );
243
244 // If some text is selected and user presses an opening character
245 // surround the selection with the opening-closing pair
246 if ( hasSelectedText() && autoSurround )
247 {
248 if ( sCompletionPairs.contains( eText ) )
249 {
250 int startLine, startPos, endLine, endPos;
251 getSelection( &startLine, &startPos, &endLine, &endPos );
252
253 // Special case for Multi line quotes (insert triple quotes)
254 if ( startLine != endLine && ( eText == "\"" || eText == "'" ) )
255 {
256 replaceSelectedText(
257 QString( "%1%1%1%2%3%3%3" ).arg( eText, selectedText(), sCompletionPairs[eText] )
258 );
259 setSelection( startLine, startPos + 3, endLine, endPos + 3 );
260 }
261 else
262 {
263 replaceSelectedText(
264 QString( "%1%2%3" ).arg( eText, selectedText(), sCompletionPairs[eText] )
265 );
266 setSelection( startLine, startPos + 1, endLine, endPos + 1 );
267 }
268 event->accept();
269 return;
270 }
271 else if ( sCompletionSingleCharacters.contains( eText ) )
272 {
273 int startLine, startPos, endLine, endPos;
274 getSelection( &startLine, &startPos, &endLine, &endPos );
275 replaceSelectedText(
276 QString( "%1%2%1" ).arg( eText, selectedText() )
277 );
278 setSelection( startLine, startPos + 1, endLine, endPos + 1 );
279 event->accept();
280 return;
281 }
282 }
283
284 // No selected text
285 else
286 {
287 // Automatically insert "import" after "from xxx " if option is enabled
288 if ( autoInsertImport && eText == " " )
289 {
290 const QString lineText = text( line );
291 const thread_local QRegularExpression re( QStringLiteral( "^from [\\w.]+$" ) );
292 if ( re.match( lineText.trimmed() ).hasMatch() )
293 {
294 insert( QStringLiteral( " import" ) );
295 setCursorPosition( line, column + 7 );
296 return QgsCodeEditor::keyPressEvent( event );
297 }
298 }
299
300 // Handle automatic bracket insertion/deletion if option is enabled
301 else if ( autoCloseBracket )
302 {
303 const QString prevChar = characterBeforeCursor();
304 const QString nextChar = characterAfterCursor();
305
306 // When backspace is pressed inside an opening/closing pair, remove both characters
307 if ( event->key() == Qt::Key_Backspace )
308 {
309 if ( sCompletionPairs.contains( prevChar ) && sCompletionPairs[prevChar] == nextChar )
310 {
311 setSelection( line, column - 1, line, column + 1 );
312 removeSelectedText();
313 event->accept();
314 // Update calltips (cursor position has changed)
315 callTip();
316 }
317 else
318 {
320 }
321 return;
322 }
323
324 // When closing character is entered inside an opening/closing pair, shift the cursor
325 else if ( sCompletionPairs.key( eText ) != "" && nextChar == eText )
326 {
327 setCursorPosition( line, column + 1 );
328 event->accept();
329
330 // Will hide calltips when a closing parenthesis is entered
331 callTip();
332 return;
333 }
334
335 // Else, if not inside a string or comment and an opening character
336 // is entered, also insert the closing character, provided the next
337 // character is a space, a colon, or a closing character
339 && sCompletionPairs.contains( eText )
340 && ( nextChar.isEmpty() || nextChar.at( 0 ).isSpace() || nextChar == ":" || sCompletionPairs.key( nextChar ) != "" )
341 )
342 {
343 // Check if user is not entering triple quotes
344 if ( !( ( eText == "\"" || eText == "'" ) && prevChar == eText ) )
345 {
347 insert( sCompletionPairs[eText] );
348 event->accept();
349 return;
350 }
351 }
352 }
353 }
354
355 // Let QgsCodeEditor handle the keyboard event
356 return QgsCodeEditor::keyPressEvent( event );
357}
358
359QString QgsCodeEditorPython::reformatCodeString( const QString &string )
360{
362 {
363 return string;
364 }
365
366 const QString formatter = settingCodeFormatter->value();
367 const int maxLineLength = settingMaxLineLength->value();
368
369 QString newText = string;
370
371 QStringList missingModules;
372
373 if ( settingSortImports->value() )
374 {
375 const QString defineSortImports = QStringLiteral(
376 "def __qgis_sort_imports(script):\n"
377 " try:\n"
378 " import isort\n"
379 " except ImportError:\n"
380 " return '_ImportError'\n"
381 " options={'line_length': %1, 'profile': '%2', 'known_first_party': ['qgis', 'console', 'processing', 'plugins']}\n"
382 " return isort.code(script, **options)\n" )
383 .arg( maxLineLength )
384 .arg( formatter == QLatin1String( "black" ) ? QStringLiteral( "black" ) : QString() );
385
386 if ( !QgsPythonRunner::run( defineSortImports ) )
387 {
388 QgsDebugError( QStringLiteral( "Error running script: %1" ).arg( defineSortImports ) );
389 return string;
390 }
391
392 const QString script = QStringLiteral( "__qgis_sort_imports(%1)" ).arg( QgsProcessingUtils::stringToPythonLiteral( newText ) );
393 QString result;
394 if ( QgsPythonRunner::eval( script, result ) )
395 {
396 if ( result == QLatin1String( "_ImportError" ) )
397 {
398 missingModules << QStringLiteral( "isort" );
399 }
400 else
401 {
402 newText = result;
403 }
404 }
405 else
406 {
407 QgsDebugError( QStringLiteral( "Error running script: %1" ).arg( script ) );
408 return newText;
409 }
410 }
411
412 if ( formatter == QLatin1String( "autopep8" ) )
413 {
414 const int level = settingAutopep8Level->value();
415
416 const QString defineReformat = QStringLiteral(
417 "def __qgis_reformat(script):\n"
418 " try:\n"
419 " import autopep8\n"
420 " except ImportError:\n"
421 " return '_ImportError'\n"
422 " options={'aggressive': %1, 'max_line_length': %2}\n"
423 " return autopep8.fix_code(script, options=options)\n" )
424 .arg( level )
425 .arg( maxLineLength );
426
427 if ( !QgsPythonRunner::run( defineReformat ) )
428 {
429 QgsDebugError( QStringLiteral( "Error running script: %1" ).arg( defineReformat ) );
430 return newText;
431 }
432
433 const QString script = QStringLiteral( "__qgis_reformat(%1)" ).arg( QgsProcessingUtils::stringToPythonLiteral( newText ) );
434 QString result;
435 if ( QgsPythonRunner::eval( script, result ) )
436 {
437 if ( result == QLatin1String( "_ImportError" ) )
438 {
439 missingModules << QStringLiteral( "autopep8" );
440 }
441 else
442 {
443 newText = result;
444 }
445 }
446 else
447 {
448 QgsDebugError( QStringLiteral( "Error running script: %1" ).arg( script ) );
449 return newText;
450 }
451 }
452 else if ( formatter == QLatin1String( "black" ) )
453 {
454 const bool normalize = settingBlackNormalizeQuotes->value();
455
456 if ( !checkSyntax() )
457 {
458 showMessage( tr( "Reformat Code" ), tr( "Code formatting failed -- the code contains syntax errors" ), Qgis::MessageLevel::Warning );
459 return newText;
460 }
461
462 const QString defineReformat = QStringLiteral(
463 "def __qgis_reformat(script):\n"
464 " try:\n"
465 " import black\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" )
471 .arg( maxLineLength );
472
473 if ( !QgsPythonRunner::run( defineReformat ) )
474 {
475 QgsDebugError( QStringLiteral( "Error running script: %1" ).arg( defineReformat ) );
476 return string;
477 }
478
479 const QString script = QStringLiteral( "__qgis_reformat(%1)" ).arg( QgsProcessingUtils::stringToPythonLiteral( newText ) );
480 QString result;
481 if ( QgsPythonRunner::eval( script, result ) )
482 {
483 if ( result == QLatin1String( "_ImportError" ) )
484 {
485 missingModules << QStringLiteral( "black" );
486 }
487 else
488 {
489 newText = result;
490 }
491 }
492 else
493 {
494 QgsDebugError( QStringLiteral( "Error running script: %1" ).arg( script ) );
495 return newText;
496 }
497 }
498
499 if ( !missingModules.empty() )
500 {
501 if ( missingModules.size() == 1 )
502 {
503 showMessage( tr( "Reformat Code" ), tr( "The Python module %1 is missing" ).arg( missingModules.at( 0 ) ), Qgis::MessageLevel::Warning );
504 }
505 else
506 {
507 const QString modules = missingModules.join( QLatin1String( ", " ) );
508 showMessage( tr( "Reformat Code" ), tr( "The Python modules %1 are missing" ).arg( modules ), Qgis::MessageLevel::Warning );
509 }
510 }
511
512 return newText;
513}
514
516{
518
519 QAction *pyQgisHelpAction = new QAction(
520 QgsApplication::getThemeIcon( QStringLiteral( "console/iconHelpConsole.svg" ) ),
521 tr( "Search Selection in PyQGIS Documentation" ),
522 menu );
523 pyQgisHelpAction->setEnabled( hasSelectedText() );
524 connect( pyQgisHelpAction, &QAction::triggered, this, &QgsCodeEditorPython::searchSelectedTextInPyQGISDocs );
525
526 menu->addSeparator();
527 menu->addAction( pyQgisHelpAction );
528}
529
531{
532 switch ( autoCompletionSource() )
533 {
534 case AcsDocument:
535 autoCompleteFromDocument();
536 break;
537
538 case AcsAPIs:
539 autoCompleteFromAPIs();
540 break;
541
542 case AcsAll:
543 autoCompleteFromAll();
544 break;
545
546 case AcsNone:
547 break;
548 }
549}
550
551void QgsCodeEditorPython::loadAPIs( const QList<QString> &filenames )
552{
553 mAPISFilesList = filenames;
554 //QgsDebugMsgLevel( QStringLiteral( "The apis files: %1" ).arg( mAPISFilesList[0] ), 2 );
556}
557
558bool QgsCodeEditorPython::loadScript( const QString &script )
559{
560 QgsDebugMsgLevel( QStringLiteral( "The script file: %1" ).arg( script ), 2 );
561 QFile file( script );
562 if ( !file.open( QIODevice::ReadOnly ) )
563 {
564 return false;
565 }
566
567 QTextStream in( &file );
568#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
569 in.setCodec( "UTF-8" );
570#endif
571
572 setText( in.readAll().trimmed() );
573 file.close();
574
576 return true;
577}
578
580{
581 int position = linearPosition();
582
583 // Special case: cursor at the end of the document. Style will always be Default,
584 // so we have to check the style of the previous character.
585 // It it is an unclosed string (triple string, unclosed, or comment),
586 // consider cursor is inside a string.
587 if ( position >= length() && position > 0 )
588 {
589 long style = SendScintilla( QsciScintillaBase::SCI_GETSTYLEAT, position - 1 );
590 return style == QsciLexerPython::Comment
591 || style == QsciLexerPython::TripleSingleQuotedString
592 || style == QsciLexerPython::TripleDoubleQuotedString
593 || style == QsciLexerPython::TripleSingleQuotedFString
594 || style == QsciLexerPython::TripleDoubleQuotedFString
595 || style == QsciLexerPython::UnclosedString;
596 }
597 else
598 {
599 long style = SendScintilla( QsciScintillaBase::SCI_GETSTYLEAT, position );
600 return style == QsciLexerPython::Comment
601 || style == QsciLexerPython::DoubleQuotedString
602 || style == QsciLexerPython::SingleQuotedString
603 || style == QsciLexerPython::TripleSingleQuotedString
604 || style == QsciLexerPython::TripleDoubleQuotedString
605 || style == QsciLexerPython::CommentBlock
606 || style == QsciLexerPython::UnclosedString
607 || style == QsciLexerPython::DoubleQuotedFString
608 || style == QsciLexerPython::SingleQuotedFString
609 || style == QsciLexerPython::TripleSingleQuotedFString
610 || style == QsciLexerPython::TripleDoubleQuotedFString;
611 }
612}
613
615{
616 int position = linearPosition();
617 if ( position <= 0 )
618 {
619 return QString();
620 }
621 return text( position - 1, position );
622}
623
625{
626 int position = linearPosition();
627 if ( position >= length() )
628 {
629 return QString();
630 }
631 return text( position, position + 1 );
632}
633
635{
637
639 return;
640
642
643 // we could potentially check for autopep8/black import here and reflect the capability accordingly.
644 // (current approach is to to always indicate this capability and raise a user-friendly warning
645 // when attempting to reformat if the libraries can't be imported)
647}
648
650{
652
654 {
655 return true;
656 }
657
658 const QString originalText = text();
659
660 const QString defineCheckSyntax = QStringLiteral(
661 "def __check_syntax(script):\n"
662 " try:\n"
663 " compile(script.encode('utf-8'), '', 'exec')\n"
664 " except SyntaxError as detail:\n"
665 " eline = detail.lineno or 1\n"
666 " eline -= 1\n"
667 " ecolumn = detail.offset or 1\n"
668 " edescr = detail.msg\n"
669 " return '!!!!'.join([str(eline), str(ecolumn), edescr])\n"
670 " return ''" );
671
672 if ( !QgsPythonRunner::run( defineCheckSyntax ) )
673 {
674 QgsDebugError( QStringLiteral( "Error running script: %1" ).arg( defineCheckSyntax ) );
675 return true;
676 }
677
678 const QString script = QStringLiteral( "__check_syntax(%1)" ).arg( QgsProcessingUtils::stringToPythonLiteral( originalText ) );
679 QString result;
680 if ( QgsPythonRunner::eval( script, result ) )
681 {
682 if ( result.size() == 0 )
683 {
684 return true;
685 }
686 else
687 {
688 const QStringList parts = result.split( QStringLiteral( "!!!!" ) );
689 if ( parts.size() == 3 )
690 {
691 const int line = parts.at( 0 ).toInt();
692 const int column = parts.at( 1 ).toInt();
693 addWarning( line, parts.at( 2 ) );
694 setCursorPosition( line, column - 1 );
695 ensureLineVisible( line );
696 }
697 return false;
698 }
699 }
700 else
701 {
702 QgsDebugError( QStringLiteral( "Error running script: %1" ).arg( script ) );
703 return true;
704 }
705}
706
708{
709 if ( !hasSelectedText() )
710 return;
711
712 QString text = selectedText();
713 text = text.replace( QLatin1String( ">>> " ), QString() ).replace( QLatin1String( "... " ), QString() ).trimmed(); // removing prompts
714 const QString version = QString( Qgis::version() ).split( '.' ).mid( 0, 2 ).join( '.' );
715 QDesktopServices::openUrl( QUrl( QStringLiteral( "https://qgis.org/pyqgis/%1/search.html?q=%2" ).arg( version, text ) ) );
716}
717
719{
720 if ( isReadOnly() )
721 {
722 return;
723 }
724
725 beginUndoAction();
726 int startLine, startPos, endLine, endPos;
727 if ( hasSelectedText() )
728 {
729 getSelection( &startLine, &startPos, &endLine, &endPos );
730 }
731 else
732 {
733 getCursorPosition( &startLine, &startPos );
734 endLine = startLine;
735 endPos = startPos;
736 }
737
738 // Check comment state and minimum indentation for each selected line
739 bool allEmpty = true;
740 bool allCommented = true;
741 int minIndentation = -1;
742 for ( int line = startLine; line <= endLine; line++ )
743 {
744 const QString stripped = text( line ).trimmed();
745 if ( !stripped.isEmpty() )
746 {
747 allEmpty = false;
748 if ( !stripped.startsWith( '#' ) )
749 {
750 allCommented = false;
751 }
752 if ( minIndentation == -1 || minIndentation > indentation( line ) )
753 {
754 minIndentation = indentation( line );
755 }
756 }
757 }
758
759 // Special case, only empty lines
760 if ( allEmpty )
761 {
762 return;
763 }
764
765 // Selection shift to keep the same selected text after a # is added/removed
766 int delta = 0;
767
768 for ( int line = startLine; line <= endLine; line++ )
769 {
770 const QString stripped = text( line ).trimmed();
771
772 // Empty line
773 if ( stripped.isEmpty() )
774 {
775 continue;
776 }
777
778 if ( !allCommented )
779 {
780 insertAt( QStringLiteral( "# " ), line, minIndentation );
781 delta = -2;
782 }
783 else
784 {
785 if ( !stripped.startsWith( '#' ) )
786 {
787 continue;
788 }
789 if ( stripped.startsWith( QLatin1String( "# " ) ) )
790 {
791 delta = 2;
792 }
793 else
794 {
795 delta = 1;
796 }
797 setSelection( line, indentation( line ), line, indentation( line ) + delta );
798 removeSelectedText();
799 }
800 }
801
802 endUndoAction();
803 setSelection( startLine, startPos - delta, endLine, endPos - delta );
804}
805
807//
808// QgsQsciLexerPython
809//
810QgsQsciLexerPython::QgsQsciLexerPython( QObject *parent )
811 : QsciLexerPython( parent )
812{
813
814}
815
816const char *QgsQsciLexerPython::keywords( int set ) const
817{
818 if ( set == 1 )
819 {
820 return "True False and as assert break class continue def del elif else except "
821 "finally for from global if import in is lambda None not or pass "
822 "raise return try while with yield async await nonlocal";
823 }
824
825 return QsciLexerPython::keywords( set );
826}
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:4173
QFlags< ScriptLanguageCapability > ScriptLanguageCapabilities
Script language capabilities.
Definition qgis.h:4208
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.
virtual void callTip() override
void runPostLexerConfigurationTasks()
Performs tasks which must be run after a lexer has been set for the widget.
virtual void showMessage(const QString &title, const QString &message, Qgis::MessageLevel level)
Shows a user facing message (eg a warning message).
int linearPosition() const
Convenience function to return the cursor position as a linear index.
void setTitle(const QString &title)
Set the widget title.
void clearWarnings()
Clears all warning messages from the editor.
void 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