QGIS API Documentation 3.99.0-Master (2fe06baccd8)
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
43const QMap<QString, QString> QgsCodeEditorPython::sCompletionPairs {
44 { "(", ")" },
45 { "[", "]" },
46 { "{", "}" },
47 { "'", "'" },
48 { "\"", "\"" }
49};
50const QStringList QgsCodeEditorPython::sCompletionSingleCharacters { "`", "*" };
52const QgsSettingsEntryString *QgsCodeEditorPython::settingCodeFormatter = new QgsSettingsEntryString( QStringLiteral( "formatter" ), sTreePythonCodeEditor, QStringLiteral( "autopep8" ), QStringLiteral( "Python code autoformatter" ) );
53const QgsSettingsEntryInteger *QgsCodeEditorPython::settingMaxLineLength = new QgsSettingsEntryInteger( QStringLiteral( "max-line-length" ), sTreePythonCodeEditor, 80, QStringLiteral( "Maximum line length" ) );
54const QgsSettingsEntryBool *QgsCodeEditorPython::settingSortImports = new QgsSettingsEntryBool( QStringLiteral( "sort-imports" ), sTreePythonCodeEditor, true, QStringLiteral( "Whether imports should be sorted when auto-formatting code" ) );
55const QgsSettingsEntryInteger *QgsCodeEditorPython::settingAutopep8Level = new QgsSettingsEntryInteger( QStringLiteral( "autopep8-level" ), sTreePythonCodeEditor, 1, QStringLiteral( "Autopep8 aggressive level" ) );
56const QgsSettingsEntryBool *QgsCodeEditorPython::settingBlackNormalizeQuotes = new QgsSettingsEntryBool( QStringLiteral( "black-normalize-quotes" ), sTreePythonCodeEditor, true, QStringLiteral( "Whether quotes should be normalized when auto-formatting code using black" ) );
57const 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." ) );
58const QgsSettingsEntryEnumFlag<Qgis::DocumentationBrowser> *QgsCodeEditorPython::settingContextHelpBrowser = new QgsSettingsEntryEnumFlag<Qgis::DocumentationBrowser>( QStringLiteral( "context-help-browser" ), sTreePythonCodeEditor, Qgis::DocumentationBrowser::DeveloperToolsPanel, QStringLiteral( "Web browser used to display the api documentation" ) );
60
61
62QgsCodeEditorPython::QgsCodeEditorPython( QWidget *parent, const QList<QString> &filenames, Mode mode, Flags flags )
63 : QgsCodeEditor( parent, QString(), false, false, flags, mode )
64 , mAPISFilesList( filenames )
65{
66 if ( !parent )
67 {
68 setTitle( tr( "Python Editor" ) );
69 }
70
71 setCaretWidth( 2 );
72
74
76
78}
79
84
89
91{
92 // current line
93 setEdgeMode( QsciScintilla::EdgeLine );
94 setEdgeColumn( settingMaxLineLength->value() );
96
97 setWhitespaceVisibility( QsciScintilla::WsVisibleAfterIndent );
98
99 SendScintilla( QsciScintillaBase::SCI_SETPROPERTY, "highlight.current.word", "1" );
100
101 QFont font = lexerFont();
103
104 QsciLexerPython *pyLexer = new QgsQsciLexerPython( this );
105
106 pyLexer->setIndentationWarning( QsciLexerPython::Inconsistent );
107 pyLexer->setFoldComments( true );
108 pyLexer->setFoldQuotes( true );
109
110 pyLexer->setDefaultFont( font );
111 pyLexer->setDefaultColor( defaultColor );
112 pyLexer->setDefaultPaper( lexerColor( QgsCodeEditorColorScheme::ColorRole::Background ) );
113 pyLexer->setFont( font, -1 );
114
115 font.setItalic( true );
116 pyLexer->setFont( font, QsciLexerPython::Comment );
117 pyLexer->setFont( font, QsciLexerPython::CommentBlock );
118
119 font.setItalic( false );
120 font.setBold( true );
121 pyLexer->setFont( font, QsciLexerPython::SingleQuotedString );
122 pyLexer->setFont( font, QsciLexerPython::DoubleQuotedString );
123
124 pyLexer->setColor( defaultColor, QsciLexerPython::Default );
125 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::Error ), QsciLexerPython::UnclosedString );
126 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::Class ), QsciLexerPython::ClassName );
127 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::Method ), QsciLexerPython::FunctionMethodName );
128 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::Number ), QsciLexerPython::Number );
129 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::Operator ), QsciLexerPython::Operator );
130 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::Identifier ), QsciLexerPython::Identifier );
131 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::Comment ), QsciLexerPython::Comment );
132 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::CommentBlock ), QsciLexerPython::CommentBlock );
133 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::Keyword ), QsciLexerPython::Keyword );
134 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::Decoration ), QsciLexerPython::Decorator );
135 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::SingleQuote ), QsciLexerPython::SingleQuotedString );
136 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::SingleQuote ), QsciLexerPython::SingleQuotedFString );
137 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::DoubleQuote ), QsciLexerPython::DoubleQuotedString );
138 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::DoubleQuote ), QsciLexerPython::DoubleQuotedFString );
139 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::TripleSingleQuote ), QsciLexerPython::TripleSingleQuotedString );
140 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::TripleDoubleQuote ), QsciLexerPython::TripleDoubleQuotedString );
141
142 auto apis = std::make_unique<QsciAPIs>( pyLexer );
143
144 QgsSettings settings;
145 if ( mAPISFilesList.isEmpty() )
146 {
147 if ( settings.value( QStringLiteral( "pythonConsole/preloadAPI" ), true ).toBool() )
148 {
149 mPapFile = QgsApplication::pkgDataPath() + QStringLiteral( "/python/qsci_apis/PyQGIS.pap" );
150 apis->loadPrepared( mPapFile );
151 }
152 else if ( settings.value( QStringLiteral( "pythonConsole/usePreparedAPIFile" ), false ).toBool() )
153 {
154 apis->loadPrepared( settings.value( QStringLiteral( "pythonConsole/preparedAPIFile" ) ).toString() );
155 }
156 else
157 {
158 const QStringList apiPaths = settings.value( QStringLiteral( "pythonConsole/userAPI" ) ).toStringList();
159 for ( const QString &path : apiPaths )
160 {
161 if ( !QFileInfo::exists( path ) )
162 {
163 QgsDebugError( QStringLiteral( "The apis file %1 was not found" ).arg( path ) );
164 }
165 else
166 {
167 apis->load( path );
168 }
169 }
170 apis->prepare();
171 }
172 }
173 else if ( mAPISFilesList.length() == 1 && mAPISFilesList[0].right( 3 ) == QLatin1String( "pap" ) )
174 {
175 if ( !QFileInfo::exists( mAPISFilesList[0] ) )
176 {
177 QgsDebugError( QStringLiteral( "The apis file %1 not found" ).arg( mAPISFilesList.at( 0 ) ) );
178 return;
179 }
180 mPapFile = mAPISFilesList[0];
181 apis->loadPrepared( mPapFile );
182 }
183 else
184 {
185 for ( const QString &path : std::as_const( mAPISFilesList ) )
186 {
187 if ( !QFileInfo::exists( path ) )
188 {
189 QgsDebugError( QStringLiteral( "The apis file %1 was not found" ).arg( path ) );
190 }
191 else
192 {
193 apis->load( path );
194 }
195 }
196 apis->prepare();
197 }
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 {
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 );
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 // 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
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 )
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 )
426 .arg( level )
427 .arg( maxLineLength );
428
429 if ( !QgsPythonRunner::run( defineReformat ) )
430 {
431 QgsDebugError( QStringLiteral( "Error running script: %1" ).arg( defineReformat ) );
432 return newText;
433 }
434
435 const QString script = QStringLiteral( "__qgis_reformat(%1)" ).arg( QgsProcessingUtils::stringToPythonLiteral( newText ) );
436 QString result;
437 if ( QgsPythonRunner::eval( script, result ) )
438 {
439 if ( result == QLatin1String( "_ImportError" ) )
440 {
441 missingModules << QStringLiteral( "autopep8" );
442 }
443 else
444 {
445 newText = result;
446 }
447 }
448 else
449 {
450 QgsDebugError( QStringLiteral( "Error running script: %1" ).arg( script ) );
451 return newText;
452 }
453 }
454 else if ( formatter == QLatin1String( "black" ) )
455 {
456 const bool normalize = settingBlackNormalizeQuotes->value();
457
458 if ( !checkSyntax() )
459 {
460 showMessage( tr( "Reformat Code" ), tr( "Code formatting failed -- the code contains syntax errors" ), Qgis::MessageLevel::Warning );
461 return newText;
462 }
463
464 const QString defineReformat = QStringLiteral(
465 "def __qgis_reformat(script):\n"
466 " try:\n"
467 " import black\n"
468 " except ImportError:\n"
469 " return '_ImportError'\n"
470 " options={'string_normalization': %1, 'line_length': %2}\n"
471 " return black.format_str(script, mode=black.Mode(**options))\n"
472 )
474 .arg( maxLineLength );
475
476 if ( !QgsPythonRunner::run( defineReformat ) )
477 {
478 QgsDebugError( QStringLiteral( "Error running script: %1" ).arg( defineReformat ) );
479 return string;
480 }
481
482 const QString script = QStringLiteral( "__qgis_reformat(%1)" ).arg( QgsProcessingUtils::stringToPythonLiteral( newText ) );
483 QString result;
484 if ( QgsPythonRunner::eval( script, result ) )
485 {
486 if ( result == QLatin1String( "_ImportError" ) )
487 {
488 missingModules << QStringLiteral( "black" );
489 }
490 else
491 {
492 newText = result;
493 }
494 }
495 else
496 {
497 QgsDebugError( QStringLiteral( "Error running script: %1" ).arg( script ) );
498 return newText;
499 }
500 }
501
502 if ( !missingModules.empty() )
503 {
504 if ( missingModules.size() == 1 )
505 {
506 showMessage( tr( "Reformat Code" ), tr( "The Python module %1 is missing" ).arg( missingModules.at( 0 ) ), Qgis::MessageLevel::Warning );
507 }
508 else
509 {
510 const QString modules = missingModules.join( QLatin1String( ", " ) );
511 showMessage( tr( "Reformat Code" ), tr( "The Python modules %1 are missing" ).arg( modules ), Qgis::MessageLevel::Warning );
512 }
513 }
514
515 return newText;
516}
517
519{
521
522 QString text = selectedText();
523 if ( text.isEmpty() )
524 {
525 text = wordAtPoint( mapFromGlobal( QCursor::pos() ) );
526 }
527 if ( text.isEmpty() )
528 {
529 return;
530 }
531
532 QAction *pyQgisHelpAction = new QAction(
533 QgsApplication::getThemeIcon( QStringLiteral( "console/iconHelpConsole.svg" ) ),
534 tr( "Search Selection in PyQGIS Documentation" ),
535 menu
536 );
537
538 pyQgisHelpAction->setEnabled( hasSelectedText() );
539 pyQgisHelpAction->setShortcut( QKeySequence::StandardKey::HelpContents );
540 connect( pyQgisHelpAction, &QAction::triggered, this, [text, this] { showApiDocumentation( text ); } );
541
542 menu->addSeparator();
543 menu->addAction( pyQgisHelpAction );
544}
545
547{
548 switch ( autoCompletionSource() )
549 {
550 case AcsDocument:
551 autoCompleteFromDocument();
552 break;
553
554 case AcsAPIs:
555 autoCompleteFromAPIs();
556 break;
557
558 case AcsAll:
559 autoCompleteFromAll();
560 break;
561
562 case AcsNone:
563 break;
564 }
565}
566
567void QgsCodeEditorPython::loadAPIs( const QList<QString> &filenames )
568{
569 mAPISFilesList = filenames;
570 //QgsDebugMsgLevel( QStringLiteral( "The apis files: %1" ).arg( mAPISFilesList[0] ), 2 );
572}
573
574bool QgsCodeEditorPython::loadScript( const QString &script )
575{
576 QgsDebugMsgLevel( QStringLiteral( "The script file: %1" ).arg( script ), 2 );
577 QFile file( script );
578 if ( !file.open( QIODevice::ReadOnly ) )
579 {
580 return false;
581 }
582
583 QTextStream in( &file );
584#if QT_VERSION < QT_VERSION_CHECK( 6, 0, 0 )
585 in.setCodec( "UTF-8" );
586#endif
587
588 setText( in.readAll().trimmed() );
589 file.close();
590
592 return true;
593}
594
596{
597 int position = linearPosition();
598
599 // Special case: cursor at the end of the document. Style will always be Default,
600 // so we have to check the style of the previous character.
601 // It it is an unclosed string (triple string, unclosed, or comment),
602 // consider cursor is inside a string.
603 if ( position >= length() && position > 0 )
604 {
605 long style = SendScintilla( QsciScintillaBase::SCI_GETSTYLEAT, position - 1 );
606 return style == QsciLexerPython::Comment
607 || style == QsciLexerPython::TripleSingleQuotedString
608 || style == QsciLexerPython::TripleDoubleQuotedString
609 || style == QsciLexerPython::TripleSingleQuotedFString
610 || style == QsciLexerPython::TripleDoubleQuotedFString
611 || style == QsciLexerPython::UnclosedString;
612 }
613 else
614 {
615 long style = SendScintilla( QsciScintillaBase::SCI_GETSTYLEAT, position );
616 return style == QsciLexerPython::Comment
617 || style == QsciLexerPython::DoubleQuotedString
618 || style == QsciLexerPython::SingleQuotedString
619 || style == QsciLexerPython::TripleSingleQuotedString
620 || style == QsciLexerPython::TripleDoubleQuotedString
621 || style == QsciLexerPython::CommentBlock
622 || style == QsciLexerPython::UnclosedString
623 || style == QsciLexerPython::DoubleQuotedFString
624 || style == QsciLexerPython::SingleQuotedFString
625 || style == QsciLexerPython::TripleSingleQuotedFString
626 || style == QsciLexerPython::TripleDoubleQuotedFString;
627 }
628}
629
631{
632 int position = linearPosition();
633 if ( position <= 0 )
634 {
635 return QString();
636 }
637 return text( position - 1, position );
638}
639
641{
642 int position = linearPosition();
643 if ( position >= length() )
644 {
645 return QString();
646 }
647 return text( position, position + 1 );
648}
649
651{
653
655 return;
656
658
659 // we could potentially check for autopep8/black import here and reflect the capability accordingly.
660 // (current approach is to to always indicate this capability and raise a user-friendly warning
661 // when attempting to reformat if the libraries can't be imported)
663}
664
666{
668
670 {
671 return true;
672 }
673
674 const QString originalText = text();
675
676 const QString defineCheckSyntax = QStringLiteral(
677 "def __check_syntax(script):\n"
678 " try:\n"
679 " compile(script.encode('utf-8'), '', 'exec')\n"
680 " except SyntaxError as detail:\n"
681 " eline = detail.lineno or 1\n"
682 " eline -= 1\n"
683 " ecolumn = detail.offset or 1\n"
684 " edescr = detail.msg\n"
685 " return '!!!!'.join([str(eline), str(ecolumn), edescr])\n"
686 " return ''"
687 );
688
689 if ( !QgsPythonRunner::run( defineCheckSyntax ) )
690 {
691 QgsDebugError( QStringLiteral( "Error running script: %1" ).arg( defineCheckSyntax ) );
692 return true;
693 }
694
695 const QString script = QStringLiteral( "__check_syntax(%1)" ).arg( QgsProcessingUtils::stringToPythonLiteral( originalText ) );
696 QString result;
697 if ( QgsPythonRunner::eval( script, result ) )
698 {
699 if ( result.size() == 0 )
700 {
701 return true;
702 }
703 else
704 {
705 const QStringList parts = result.split( QStringLiteral( "!!!!" ) );
706 if ( parts.size() == 3 )
707 {
708 const int line = parts.at( 0 ).toInt();
709 const int column = parts.at( 1 ).toInt();
710 addWarning( line, parts.at( 2 ) );
711 setCursorPosition( line, column - 1 );
712 ensureLineVisible( line );
713 }
714 return false;
715 }
716 }
717 else
718 {
719 QgsDebugError( QStringLiteral( "Error running script: %1" ).arg( script ) );
720 return true;
721 }
722}
723
728
730{
731 QString searchText = text;
732 searchText = searchText.replace( QLatin1String( ">>> " ), QString() ).replace( QLatin1String( "... " ), QString() ).trimmed(); // removing prompts
733
734 QRegularExpression qtExpression( "^Q[A-Z][a-zA-Z]" );
735
736 if ( qtExpression.match( searchText ).hasMatch() )
737 {
738 const QString qtVersion = QString( qVersion() ).split( '.' ).mid( 0, 2 ).join( '.' );
739 QString baseUrl = QString( "https://doc.qt.io/qt-%1" ).arg( qtVersion );
740 QDesktopServices::openUrl( QUrl( QStringLiteral( "%1/%2.html" ).arg( baseUrl, searchText.toLower() ) ) );
741 return;
742 }
743 const QString qgisVersion = QString( Qgis::version() ).split( '.' ).mid( 0, 2 ).join( '.' );
744 if ( searchText.isEmpty() )
745 {
746 QDesktopServices::openUrl( QUrl( QStringLiteral( "https://qgis.org/pyqgis/%1/" ).arg( qgisVersion ) ) );
747 }
748 else
749 {
750 QDesktopServices::openUrl( QUrl( QStringLiteral( "https://qgis.org/pyqgis/%1/search.html?q=%2" ).arg( qgisVersion, searchText ) ) );
751 }
752}
753
755{
756 toggleLineComments( QStringLiteral( "#" ) );
757}
758
760//
761// QgsQsciLexerPython
762//
763QgsQsciLexerPython::QgsQsciLexerPython( QObject *parent )
764 : QsciLexerPython( parent )
765{
766}
767
768const char *QgsQsciLexerPython::keywords( int set ) const
769{
770 if ( set == 1 )
771 {
772 return "True False and as assert break class continue def del elif else except "
773 "finally for from global if import in is lambda None not or pass "
774 "raise return try while with yield async await nonlocal";
775 }
776
777 return QsciLexerPython::keywords( set );
778}
static QString version()
Version string.
Definition qgis.cpp:677
@ Warning
Warning message.
Definition qgis.h:158
@ CheckSyntax
Language supports syntax checking.
Definition qgis.h:4503
@ Reformat
Language supports automatic code reformatting.
Definition qgis.h:4502
@ ToggleComment
Language supports comment toggling.
Definition qgis.h:4504
ScriptLanguage
Scripting languages.
Definition qgis.h:4478
@ Python
Python.
Definition qgis.h:4484
@ DeveloperToolsPanel
Embedded webview in the DevTools panel.
Definition qgis.h:6013
QFlags< ScriptLanguageCapability > ScriptLanguageCapabilities
Script language capabilities.
Definition qgis.h:4513
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:65
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:61
#define QgsDebugError(str)
Definition qgslogger.h:57