QGIS API Documentation 3.41.0-Master (af5edcb665c)
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};
47const QStringList QgsCodeEditorPython::sCompletionSingleCharacters { "`", "*" };
49const QgsSettingsEntryString *QgsCodeEditorPython::settingCodeFormatter = new QgsSettingsEntryString( QStringLiteral( "formatter" ), sTreePythonCodeEditor, QStringLiteral( "autopep8" ), QStringLiteral( "Python code autoformatter" ) );
50const QgsSettingsEntryInteger *QgsCodeEditorPython::settingMaxLineLength = new QgsSettingsEntryInteger( QStringLiteral( "max-line-length" ), sTreePythonCodeEditor, 80, QStringLiteral( "Maximum line length" ) );
51const QgsSettingsEntryBool *QgsCodeEditorPython::settingSortImports = new QgsSettingsEntryBool( QStringLiteral( "sort-imports" ), sTreePythonCodeEditor, true, QStringLiteral( "Whether imports should be sorted when auto-formatting code" ) );
52const QgsSettingsEntryInteger *QgsCodeEditorPython::settingAutopep8Level = new QgsSettingsEntryInteger( QStringLiteral( "autopep8-level" ), sTreePythonCodeEditor, 1, QStringLiteral( "Autopep8 aggressive level" ) );
53const QgsSettingsEntryBool *QgsCodeEditorPython::settingBlackNormalizeQuotes = new QgsSettingsEntryBool( QStringLiteral( "black-normalize-quotes" ), sTreePythonCodeEditor, true, QStringLiteral( "Whether quotes should be normalized when auto-formatting code using black" ) );
54const QgsSettingsEntryString *QgsCodeEditorPython::settingExternalPythonEditorCommand = new QgsSettingsEntryString( QStringLiteral( "external-editor" ), sTreePythonCodeEditor, QString(), QStringLiteral( "Command to launch an external Python code editor. Use the token <file> to insert the filename, <line> to insert line number, and <col> to insert the column number." ) );
55const 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" ) );
57
58
59QgsCodeEditorPython::QgsCodeEditorPython( QWidget *parent, const QList<QString> &filenames, Mode mode, Flags flags )
60 : QgsCodeEditor( parent, QString(), false, false, flags, mode )
61 , mAPISFilesList( filenames )
62{
63 if ( !parent )
64 {
65 setTitle( tr( "Python Editor" ) );
66 }
67
68 setCaretWidth( 2 );
69
71
73
75}
76
81
86
88{
89 // current line
90 setEdgeMode( QsciScintilla::EdgeLine );
91 setEdgeColumn( settingMaxLineLength->value() );
93
94 setWhitespaceVisibility( QsciScintilla::WsVisibleAfterIndent );
95
96 SendScintilla( QsciScintillaBase::SCI_SETPROPERTY, "highlight.current.word", "1" );
97
98 QFont font = lexerFont();
100
101 QsciLexerPython *pyLexer = new QgsQsciLexerPython( this );
102
103 pyLexer->setIndentationWarning( QsciLexerPython::Inconsistent );
104 pyLexer->setFoldComments( true );
105 pyLexer->setFoldQuotes( true );
106
107 pyLexer->setDefaultFont( font );
108 pyLexer->setDefaultColor( defaultColor );
109 pyLexer->setDefaultPaper( lexerColor( QgsCodeEditorColorScheme::ColorRole::Background ) );
110 pyLexer->setFont( font, -1 );
111
112 font.setItalic( true );
113 pyLexer->setFont( font, QsciLexerPython::Comment );
114 pyLexer->setFont( font, QsciLexerPython::CommentBlock );
115
116 font.setItalic( false );
117 font.setBold( true );
118 pyLexer->setFont( font, QsciLexerPython::SingleQuotedString );
119 pyLexer->setFont( font, QsciLexerPython::DoubleQuotedString );
120
121 pyLexer->setColor( defaultColor, QsciLexerPython::Default );
122 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::Error ), QsciLexerPython::UnclosedString );
123 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::Class ), QsciLexerPython::ClassName );
124 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::Method ), QsciLexerPython::FunctionMethodName );
125 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::Number ), QsciLexerPython::Number );
126 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::Operator ), QsciLexerPython::Operator );
127 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::Identifier ), QsciLexerPython::Identifier );
128 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::Comment ), QsciLexerPython::Comment );
129 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::CommentBlock ), QsciLexerPython::CommentBlock );
130 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::Keyword ), QsciLexerPython::Keyword );
131 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::Decoration ), QsciLexerPython::Decorator );
132 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::SingleQuote ), QsciLexerPython::SingleQuotedString );
133 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::SingleQuote ), QsciLexerPython::SingleQuotedFString );
134 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::DoubleQuote ), QsciLexerPython::DoubleQuotedString );
135 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::DoubleQuote ), QsciLexerPython::DoubleQuotedFString );
136 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::TripleSingleQuote ), QsciLexerPython::TripleSingleQuotedString );
137 pyLexer->setColor( lexerColor( QgsCodeEditorColorScheme::ColorRole::TripleDoubleQuote ), QsciLexerPython::TripleDoubleQuotedString );
138
139 std::unique_ptr<QsciAPIs> apis = std::make_unique<QsciAPIs>( pyLexer );
140
141 QgsSettings settings;
142 if ( mAPISFilesList.isEmpty() )
143 {
144 if ( settings.value( QStringLiteral( "pythonConsole/preloadAPI" ), true ).toBool() )
145 {
146 mPapFile = QgsApplication::pkgDataPath() + QStringLiteral( "/python/qsci_apis/PyQGIS.pap" );
147 apis->loadPrepared( mPapFile );
148 }
149 else if ( settings.value( QStringLiteral( "pythonConsole/usePreparedAPIFile" ), false ).toBool() )
150 {
151 apis->loadPrepared( settings.value( QStringLiteral( "pythonConsole/preparedAPIFile" ) ).toString() );
152 }
153 else
154 {
155 const QStringList apiPaths = settings.value( QStringLiteral( "pythonConsole/userAPI" ) ).toStringList();
156 for ( const QString &path : apiPaths )
157 {
158 if ( !QFileInfo::exists( path ) )
159 {
160 QgsDebugError( QStringLiteral( "The apis file %1 was not found" ).arg( path ) );
161 }
162 else
163 {
164 apis->load( path );
165 }
166 }
167 apis->prepare();
168 }
169 }
170 else if ( mAPISFilesList.length() == 1 && mAPISFilesList[0].right( 3 ) == QLatin1String( "pap" ) )
171 {
172 if ( !QFileInfo::exists( mAPISFilesList[0] ) )
173 {
174 QgsDebugError( QStringLiteral( "The apis file %1 not found" ).arg( mAPISFilesList.at( 0 ) ) );
175 return;
176 }
177 mPapFile = mAPISFilesList[0];
178 apis->loadPrepared( mPapFile );
179 }
180 else
181 {
182 for ( const QString &path : std::as_const( mAPISFilesList ) )
183 {
184 if ( !QFileInfo::exists( path ) )
185 {
186 QgsDebugError( QStringLiteral( "The apis file %1 was not found" ).arg( path ) );
187 }
188 else
189 {
190 apis->load( path );
191 }
192 }
193 apis->prepare();
194 }
195 if ( apis )
196 pyLexer->setAPIs( apis.release() );
197
198 setLexer( pyLexer );
199
200 const int threshold = settings.value( QStringLiteral( "pythonConsole/autoCompThreshold" ), 2 ).toInt();
201 setAutoCompletionThreshold( threshold );
202 if ( !settings.value( "pythonConsole/autoCompleteEnabled", true ).toBool() )
203 {
204 setAutoCompletionSource( AcsNone );
205 }
206 else
207 {
208 const QString autoCompleteSource = settings.value( QStringLiteral( "pythonConsole/autoCompleteSource" ), QStringLiteral( "fromAPI" ) ).toString();
209 if ( autoCompleteSource == QLatin1String( "fromDoc" ) )
210 setAutoCompletionSource( AcsDocument );
211 else if ( autoCompleteSource == QLatin1String( "fromDocAPI" ) )
212 setAutoCompletionSource( AcsAll );
213 else
214 setAutoCompletionSource( AcsAPIs );
215 }
216
217 setLineNumbersVisible( true );
218 setIndentationsUseTabs( false );
219 setIndentationGuides( true );
220
222}
223
225{
226 // If editor is readOnly, use the default implementation
227 if ( isReadOnly() )
228 {
229 return QgsCodeEditor::keyPressEvent( event );
230 }
231
232 const QgsSettings settings;
233
234 bool autoCloseBracket = settings.value( QStringLiteral( "/pythonConsole/autoCloseBracket" ), true ).toBool();
235 bool autoSurround = settings.value( QStringLiteral( "/pythonConsole/autoSurround" ), true ).toBool();
236 bool autoInsertImport = settings.value( QStringLiteral( "/pythonConsole/autoInsertImport" ), false ).toBool();
237
238 // Get entered text and cursor position
239 const QString eText = event->text();
240 int line, column;
241 getCursorPosition( &line, &column );
242
243 // If some text is selected and user presses an opening character
244 // surround the selection with the opening-closing pair
245 if ( hasSelectedText() && autoSurround )
246 {
247 if ( sCompletionPairs.contains( eText ) )
248 {
249 int startLine, startPos, endLine, endPos;
250 getSelection( &startLine, &startPos, &endLine, &endPos );
251
252 // Special case for Multi line quotes (insert triple quotes)
253 if ( startLine != endLine && ( eText == "\"" || eText == "'" ) )
254 {
255 replaceSelectedText(
256 QString( "%1%1%1%2%3%3%3" ).arg( eText, selectedText(), sCompletionPairs[eText] )
257 );
258 setSelection( startLine, startPos + 3, endLine, endPos + 3 );
259 }
260 else
261 {
262 replaceSelectedText(
263 QString( "%1%2%3" ).arg( eText, selectedText(), sCompletionPairs[eText] )
264 );
265 setSelection( startLine, startPos + 1, endLine, endPos + 1 );
266 }
267 event->accept();
268 return;
269 }
270 else if ( sCompletionSingleCharacters.contains( eText ) )
271 {
272 int startLine, startPos, endLine, endPos;
273 getSelection( &startLine, &startPos, &endLine, &endPos );
274 replaceSelectedText(
275 QString( "%1%2%1" ).arg( eText, selectedText() )
276 );
277 setSelection( startLine, startPos + 1, endLine, endPos + 1 );
278 event->accept();
279 return;
280 }
281 }
282
283 // No selected text
284 else
285 {
286 // Automatically insert "import" after "from xxx " if option is enabled
287 if ( autoInsertImport && eText == " " )
288 {
289 const QString lineText = text( line );
290 const thread_local QRegularExpression re( QStringLiteral( "^from [\\w.]+$" ) );
291 if ( re.match( lineText.trimmed() ).hasMatch() )
292 {
293 insert( QStringLiteral( " import" ) );
294 setCursorPosition( line, column + 7 );
295 return QgsCodeEditor::keyPressEvent( event );
296 }
297 }
298
299 // Handle automatic bracket insertion/deletion if option is enabled
300 else if ( autoCloseBracket )
301 {
302 const QString prevChar = characterBeforeCursor();
303 const QString nextChar = characterAfterCursor();
304
305 // When backspace is pressed inside an opening/closing pair, remove both characters
306 if ( event->key() == Qt::Key_Backspace )
307 {
308 if ( sCompletionPairs.contains( prevChar ) && sCompletionPairs[prevChar] == nextChar )
309 {
310 setSelection( line, column - 1, line, column + 1 );
311 removeSelectedText();
312 event->accept();
313 // Update calltips (cursor position has changed)
314 callTip();
315 }
316 else
317 {
319 }
320 return;
321 }
322
323 // When closing character is entered inside an opening/closing pair, shift the cursor
324 else if ( sCompletionPairs.key( eText ) != "" && nextChar == eText )
325 {
326 setCursorPosition( line, column + 1 );
327 event->accept();
328
329 // Will hide calltips when a closing parenthesis is entered
330 callTip();
331 return;
332 }
333
334 // Else, if not inside a string or comment and an opening character
335 // is entered, also insert the closing character, provided the next
336 // character is a space, a colon, or a closing character
338 && sCompletionPairs.contains( eText )
339 && ( nextChar.isEmpty() || nextChar.at( 0 ).isSpace() || nextChar == ":" || sCompletionPairs.key( nextChar ) != "" ) )
340 {
341 // Check if user is not entering triple quotes
342 if ( !( ( eText == "\"" || eText == "'" ) && prevChar == eText ) )
343 {
345 insert( sCompletionPairs[eText] );
346 event->accept();
347 return;
348 }
349 }
350 }
351 }
352
353 // Let QgsCodeEditor handle the keyboard event
354 return QgsCodeEditor::keyPressEvent( event );
355}
356
357QString QgsCodeEditorPython::reformatCodeString( const QString &string )
358{
360 {
361 return string;
362 }
363
364 const QString formatter = settingCodeFormatter->value();
365 const int maxLineLength = settingMaxLineLength->value();
366
367 QString newText = string;
368
369 QStringList missingModules;
370
371 if ( settingSortImports->value() )
372 {
373 const QString defineSortImports = QStringLiteral(
374 "def __qgis_sort_imports(script):\n"
375 " try:\n"
376 " import isort\n"
377 " except ImportError:\n"
378 " return '_ImportError'\n"
379 " options={'line_length': %1, 'profile': '%2', 'known_first_party': ['qgis', 'console', 'processing', 'plugins']}\n"
380 " return isort.code(script, **options)\n"
381 )
382 .arg( maxLineLength )
383 .arg( formatter == QLatin1String( "black" ) ? QStringLiteral( "black" ) : QString() );
384
385 if ( !QgsPythonRunner::run( defineSortImports ) )
386 {
387 QgsDebugError( QStringLiteral( "Error running script: %1" ).arg( defineSortImports ) );
388 return string;
389 }
390
391 const QString script = QStringLiteral( "__qgis_sort_imports(%1)" ).arg( QgsProcessingUtils::stringToPythonLiteral( newText ) );
392 QString result;
393 if ( QgsPythonRunner::eval( script, result ) )
394 {
395 if ( result == QLatin1String( "_ImportError" ) )
396 {
397 missingModules << QStringLiteral( "isort" );
398 }
399 else
400 {
401 newText = result;
402 }
403 }
404 else
405 {
406 QgsDebugError( QStringLiteral( "Error running script: %1" ).arg( script ) );
407 return newText;
408 }
409 }
410
411 if ( formatter == QLatin1String( "autopep8" ) )
412 {
413 const int level = settingAutopep8Level->value();
414
415 const QString defineReformat = QStringLiteral(
416 "def __qgis_reformat(script):\n"
417 " try:\n"
418 " import autopep8\n"
419 " except ImportError:\n"
420 " return '_ImportError'\n"
421 " options={'aggressive': %1, 'max_line_length': %2}\n"
422 " return autopep8.fix_code(script, options=options)\n"
423 )
424 .arg( level )
425 .arg( maxLineLength );
426
427 if ( !QgsPythonRunner::run( defineReformat ) )
428 {
429 QgsDebugError( QStringLiteral( "Error running script: %1" ).arg( defineReformat ) );
430 return newText;
431 }
432
433 const QString script = QStringLiteral( "__qgis_reformat(%1)" ).arg( QgsProcessingUtils::stringToPythonLiteral( newText ) );
434 QString result;
435 if ( QgsPythonRunner::eval( script, result ) )
436 {
437 if ( result == QLatin1String( "_ImportError" ) )
438 {
439 missingModules << QStringLiteral( "autopep8" );
440 }
441 else
442 {
443 newText = result;
444 }
445 }
446 else
447 {
448 QgsDebugError( QStringLiteral( "Error running script: %1" ).arg( script ) );
449 return newText;
450 }
451 }
452 else if ( formatter == QLatin1String( "black" ) )
453 {
454 const bool normalize = settingBlackNormalizeQuotes->value();
455
456 if ( !checkSyntax() )
457 {
458 showMessage( tr( "Reformat Code" ), tr( "Code formatting failed -- the code contains syntax errors" ), Qgis::MessageLevel::Warning );
459 return newText;
460 }
461
462 const QString defineReformat = QStringLiteral(
463 "def __qgis_reformat(script):\n"
464 " try:\n"
465 " import black\n"
466 " except ImportError:\n"
467 " return '_ImportError'\n"
468 " options={'string_normalization': %1, 'line_length': %2}\n"
469 " return black.format_str(script, mode=black.Mode(**options))\n"
470 )
472 .arg( maxLineLength );
473
474 if ( !QgsPythonRunner::run( defineReformat ) )
475 {
476 QgsDebugError( QStringLiteral( "Error running script: %1" ).arg( defineReformat ) );
477 return string;
478 }
479
480 const QString script = QStringLiteral( "__qgis_reformat(%1)" ).arg( QgsProcessingUtils::stringToPythonLiteral( newText ) );
481 QString result;
482 if ( QgsPythonRunner::eval( script, result ) )
483 {
484 if ( result == QLatin1String( "_ImportError" ) )
485 {
486 missingModules << QStringLiteral( "black" );
487 }
488 else
489 {
490 newText = result;
491 }
492 }
493 else
494 {
495 QgsDebugError( QStringLiteral( "Error running script: %1" ).arg( script ) );
496 return newText;
497 }
498 }
499
500 if ( !missingModules.empty() )
501 {
502 if ( missingModules.size() == 1 )
503 {
504 showMessage( tr( "Reformat Code" ), tr( "The Python module %1 is missing" ).arg( missingModules.at( 0 ) ), Qgis::MessageLevel::Warning );
505 }
506 else
507 {
508 const QString modules = missingModules.join( QLatin1String( ", " ) );
509 showMessage( tr( "Reformat Code" ), tr( "The Python modules %1 are missing" ).arg( modules ), Qgis::MessageLevel::Warning );
510 }
511 }
512
513 return newText;
514}
515
517{
519
520 QString text = selectedText();
521 if ( text.isEmpty() )
522 {
523 text = wordAtPoint( mapFromGlobal( QCursor::pos() ) );
524 }
525 if ( text.isEmpty() )
526 {
527 return;
528 }
529
530 QAction *pyQgisHelpAction = new QAction(
531 QgsApplication::getThemeIcon( QStringLiteral( "console/iconHelpConsole.svg" ) ),
532 tr( "Search Selection in PyQGIS Documentation" ),
533 menu
534 );
535
536 pyQgisHelpAction->setEnabled( hasSelectedText() );
537 pyQgisHelpAction->setShortcut( QStringLiteral( "F1" ) );
538 connect( pyQgisHelpAction, &QAction::triggered, this, [text, this] { showApiDocumentation( text ); } );
539
540 menu->addSeparator();
541 menu->addAction( pyQgisHelpAction );
542}
543
545{
546 switch ( autoCompletionSource() )
547 {
548 case AcsDocument:
549 autoCompleteFromDocument();
550 break;
551
552 case AcsAPIs:
553 autoCompleteFromAPIs();
554 break;
555
556 case AcsAll:
557 autoCompleteFromAll();
558 break;
559
560 case AcsNone:
561 break;
562 }
563}
564
565void QgsCodeEditorPython::loadAPIs( const QList<QString> &filenames )
566{
567 mAPISFilesList = filenames;
568 //QgsDebugMsgLevel( QStringLiteral( "The apis files: %1" ).arg( mAPISFilesList[0] ), 2 );
570}
571
572bool QgsCodeEditorPython::loadScript( const QString &script )
573{
574 QgsDebugMsgLevel( QStringLiteral( "The script file: %1" ).arg( script ), 2 );
575 QFile file( script );
576 if ( !file.open( QIODevice::ReadOnly ) )
577 {
578 return false;
579 }
580
581 QTextStream in( &file );
582#if QT_VERSION < QT_VERSION_CHECK( 6, 0, 0 )
583 in.setCodec( "UTF-8" );
584#endif
585
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( QStringLiteral( "Error running script: %1" ).arg( defineCheckSyntax ) );
690 return true;
691 }
692
693 const QString script = QStringLiteral( "__check_syntax(%1)" ).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( QStringLiteral( "!!!!" ) );
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( QStringLiteral( "Error running script: %1" ).arg( script ) );
718 return true;
719 }
720}
721
726
728{
729 QString searchText = text;
730 searchText = searchText.replace( QLatin1String( ">>> " ), QString() ).replace( QLatin1String( "... " ), 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( QStringLiteral( "%1/%2.html" ).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( QStringLiteral( "https://qgis.org/pyqgis/%1/" ).arg( qgisVersion ) ) );
745 }
746 else
747 {
748 QDesktopServices::openUrl( QUrl( QStringLiteral( "https://qgis.org/pyqgis/%1/search.html?q=%2" ).arg( qgisVersion, searchText ) ) );
749 }
750}
751
753{
754 if ( isReadOnly() )
755 {
756 return;
757 }
758
759 beginUndoAction();
760 int startLine, startPos, endLine, endPos;
761 if ( hasSelectedText() )
762 {
763 getSelection( &startLine, &startPos, &endLine, &endPos );
764 }
765 else
766 {
767 getCursorPosition( &startLine, &startPos );
768 endLine = startLine;
769 endPos = startPos;
770 }
771
772 // Check comment state and minimum indentation for each selected line
773 bool allEmpty = true;
774 bool allCommented = true;
775 int minIndentation = -1;
776 for ( int line = startLine; line <= endLine; line++ )
777 {
778 const QString stripped = text( line ).trimmed();
779 if ( !stripped.isEmpty() )
780 {
781 allEmpty = false;
782 if ( !stripped.startsWith( '#' ) )
783 {
784 allCommented = false;
785 }
786 if ( minIndentation == -1 || minIndentation > indentation( line ) )
787 {
788 minIndentation = indentation( line );
789 }
790 }
791 }
792
793 // Special case, only empty lines
794 if ( allEmpty )
795 {
796 return;
797 }
798
799 // Selection shift to keep the same selected text after a # is added/removed
800 int delta = 0;
801
802 for ( int line = startLine; line <= endLine; line++ )
803 {
804 const QString stripped = text( line ).trimmed();
805
806 // Empty line
807 if ( stripped.isEmpty() )
808 {
809 continue;
810 }
811
812 if ( !allCommented )
813 {
814 insertAt( QStringLiteral( "# " ), line, minIndentation );
815 delta = -2;
816 }
817 else
818 {
819 if ( !stripped.startsWith( '#' ) )
820 {
821 continue;
822 }
823 if ( stripped.startsWith( QLatin1String( "# " ) ) )
824 {
825 delta = 2;
826 }
827 else
828 {
829 delta = 1;
830 }
831 setSelection( line, indentation( line ), line, indentation( line ) + delta );
832 removeSelectedText();
833 }
834 }
835
836 endUndoAction();
837 setSelection( startLine, startPos - delta, endLine, endPos - delta );
838}
839
841//
842// QgsQsciLexerPython
843//
844QgsQsciLexerPython::QgsQsciLexerPython( QObject *parent )
845 : QsciLexerPython( parent )
846{
847}
848
849const char *QgsQsciLexerPython::keywords( int set ) const
850{
851 if ( set == 1 )
852 {
853 return "True False and as assert break class continue def del elif else except "
854 "finally for from global if import in is lambda None not or pass "
855 "raise return try while with yield async await nonlocal";
856 }
857
858 return QsciLexerPython::keywords( set );
859}
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:4244
@ DeveloperToolsPanel
Embedded webview in the DevTools panel.
QFlags< ScriptLanguageCapability > ScriptLanguageCapabilities
Script language capabilities.
Definition qgis.h:4279
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