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