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