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