Salome HOME
updated copyright message
[modules/gui.git] / tools / PyEditor / src / PyEditor_Editor.cxx
1 // Copyright (C) 2015-2023  OPEN CASCADE
2 //
3 // This library is free software; you can redistribute it and/or
4 // modify it under the terms of the GNU Lesser General Public
5 // License as published by the Free Software Foundation; either
6 // version 2.1 of the License, or (at your option) any later version.
7 //
8 // This library is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
11 // Lesser General Public License for more details.
12 //
13 // You should have received a copy of the GNU Lesser General Public
14 // License along with this library; if not, write to the Free Software
15 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
16 //
17 // See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com
18 //
19 // File   : PyEditor_Editor.cxx
20 // Author : Maxim GLIBIN, Open CASCADE S.A.S. (maxim.glibin@opencascade.com)
21 //
22
23 #include "PyEditor_Editor.h"
24 #include "PyEditor_LineNumberArea.h"
25 #include "PyEditor_PyHighlighter.h"
26 #include "PyEditor_Completer.h"
27 #include "PyEditor_Settings.h"
28 #include "PyEditor_Keywords.h"
29
30 #include <QMenu>
31 #include <QPainter>
32 #include <QTextBlock>
33 #include <QScrollBar>
34
35 #include <iostream>
36
37 /*!
38   \class PyEditor_Editor
39   \brief Widget to show / edit Python scripts.
40 */
41
42 /*!
43   \brief Constructor.
44   \param parent parent widget
45 */
46 PyEditor_Editor::PyEditor_Editor( QWidget* parent )
47   : QPlainTextEdit( parent ),
48     myCompletionPolicy( Always )
49 {
50   myStdKeywords = new PyEditor_StandardKeywords( this );
51   myUserKeywords = new PyEditor_Keywords( this );
52   myUserKeywords->append( "print", 0, Qt::red );
53
54   // Set up line number area
55   myLineNumberArea = new PyEditor_LineNumberArea( this );
56   myLineNumberArea->setMouseTracking( true );
57
58   // Set up syntax highighter
59   mySyntaxHighlighter = new PyEditor_PyHighlighter( this->document(),
60                                                     myStdKeywords, myUserKeywords );
61
62   // Set-up settings
63   PyEditor_Settings* settings = PyEditor_Settings::settings();
64   if ( settings )
65     setSettings( *settings );
66
67   myCompleter = new PyEditor_Completer( this, myStdKeywords, myUserKeywords );
68
69   // Connect signals
70   connect( this, SIGNAL( blockCountChanged( int ) ), this, SLOT( updateLineNumberAreaWidth( int ) ) );
71   connect( this, SIGNAL( updateRequest( QRect, int ) ), this, SLOT( updateLineNumberArea( QRect, int ) ) );
72   connect( this, SIGNAL( cursorPositionChanged() ), this, SLOT( updateHighlightCurrentLine() ) );
73   connect( this, SIGNAL( cursorPositionChanged() ), this, SLOT( matchParentheses() ) );
74 }
75
76 /*!
77   \brief Destructor.
78 */
79 PyEditor_Editor::~PyEditor_Editor()
80 {
81 }
82
83 /*!
84   \brief Get editor settings.
85   \return settings object
86 */
87 const PyEditor_Settings& PyEditor_Editor::settings() const
88 {
89   return mySettings;
90 }
91
92 /*!
93   \brief Set editor settings.
94   \param settings new settings
95 */
96 void PyEditor_Editor::setSettings( const PyEditor_Settings& settings )
97 {
98   mySettings.copyFrom( settings );
99
100   // Set font size
101   QFont aFont = font();
102   aFont.setFamily( mySettings.font().family() );
103   aFont.setPointSize( mySettings.font().pointSize() );
104   setFont( aFont );
105
106   // Set line wrap mode
107   setLineWrapMode( mySettings.textWrapping() ? QPlainTextEdit::WidgetWidth : QPlainTextEdit::NoWrap );
108
109   // Center the cursor on screen
110   setCenterOnScroll( mySettings.centerCursorOnScroll() );
111
112   // Set size white spaces
113   setTabStopWidth( mySettings.tabSize() * 10 );
114
115   // Set completion policy
116   setCompletionPolicy( (CompletionPolicy)mySettings.completionPolicy() );
117
118   // Update current line highlight
119   updateHighlightCurrentLine();
120
121   // Update line numbers area
122   updateLineNumberAreaWidth( 0 );
123
124   mySyntaxHighlighter->rehighlight();
125   viewport()->update();
126 }
127
128 /*!
129   \brief Gets the current completion policy
130   \return completion policy
131 */
132 PyEditor_Editor::CompletionPolicy PyEditor_Editor::completionPolicy() const
133 {
134   return myCompletionPolicy;
135 }
136
137 /*!
138   \brief Sets the current completion policy
139   \param policy completion policy
140 */
141 void PyEditor_Editor::setCompletionPolicy( const CompletionPolicy& policy )
142 {
143   myCompletionPolicy = policy;
144 }
145
146 /*!
147   \brief Gets the all user keywords.
148   \param event key press event
149   \return keyword string list
150 */
151 QStringList PyEditor_Editor::keywords() const
152 {
153   return myUserKeywords->keywords();
154 }
155
156 /*!
157   \brief Add the user keywords.
158   \param kws keywords string list
159   \param type keywords type
160   \param color keywords color
161 */
162 void PyEditor_Editor::appendKeywords( const QStringList& kws, int type, const QColor& color )
163 {
164   myUserKeywords->append( kws, type, color );
165 }
166
167 /*!
168   \brief Remove the user keywords.
169   \param kws keywords string list
170 */
171 void PyEditor_Editor::removeKeywords( const QStringList& kws )
172 {
173   myUserKeywords->remove( kws );
174 }
175
176 /*!
177   Delete current selection contents.
178 */
179 void PyEditor_Editor::deleteSelected()
180 {
181   QTextCursor aCursor = textCursor();
182   if ( aCursor.hasSelection() )
183     aCursor.removeSelectedText();
184   setTextCursor( aCursor );
185 }
186
187 /*!
188   \brief Process key press event.
189   Reimplemented from QPlainTextEdit.
190   \param event key press event
191 */
192 void PyEditor_Editor::keyPressEvent( QKeyEvent* event )
193 {
194   if ( event->type() == QEvent::KeyPress )
195   {
196     int aKey = event->key();
197     Qt::KeyboardModifiers aCtrl = event->modifiers() & Qt::ControlModifier;
198     Qt::KeyboardModifiers aShift = event->modifiers() & Qt::ShiftModifier;
199     
200     if ( aKey == Qt::Key_Tab || ( aKey == Qt::Key_Backtab || ( aKey == Qt::Key_Tab && aShift ) ) )
201     {
202       QTextCursor aCursor = textCursor();
203       aCursor.beginEditBlock();
204       tabIndentation( aKey == Qt::Key_Backtab );
205       aCursor.endEditBlock();
206       event->accept();
207     }
208     else if ( aKey == Qt::Key_Enter || aKey == Qt::Key_Return )
209     {
210       QTextCursor aCursor = textCursor();
211       aCursor.beginEditBlock();
212       if ( lineIndent() == 0 )
213       {
214         QPlainTextEdit::keyPressEvent( event );
215       }
216       aCursor.endEditBlock();
217       event->accept();
218     }
219     else if ( aKey == Qt::Key_Space && aCtrl && !aShift &&
220               ( completionPolicy() == Manual || completionPolicy() == Always ) )
221     {
222       myCompleter->perform();
223       event->accept();
224     }
225     else if ( event == QKeySequence::MoveToStartOfLine || event == QKeySequence::SelectStartOfLine )
226     {
227       QTextCursor aCursor = this->textCursor();
228       if ( QTextLayout* aLayout = aCursor.block().layout() )
229       {
230         if ( aLayout->lineForTextPosition( aCursor.position() - aCursor.block().position() ).lineNumber() == 0 )
231         {
232           handleHome( event == QKeySequence::SelectStartOfLine );
233         }
234       }
235     }
236     else if ( ( aKey == Qt::Key_Colon || ( aKey == Qt::Key_Space && !aCtrl && !aShift ) ) &&
237               !textCursor().hasSelection() )
238     {
239       QTextCursor aCursor = textCursor();
240       aCursor.movePosition( QTextCursor::StartOfBlock, QTextCursor::KeepAnchor );
241       
242       QString aSelectedText = aCursor.selectedText();
243       int numSpaces = findFirstNonSpace( aSelectedText );
244       int amountChars = aSelectedText.size() - findFirstNonSpace( aSelectedText );
245       QString aLeadingText = aSelectedText.right( amountChars );
246       
247       QStringList aReservedWords;
248       aReservedWords.append( "except" );
249       if ( aKey == Qt::Key_Colon )
250       {
251         aReservedWords.append( "else" );
252         aReservedWords.append( "finally" );
253       }
254       else if ( aKey == Qt::Key_Space )
255       {
256         aReservedWords.append( "elif" );
257       }
258       
259       if ( aReservedWords.contains( aLeadingText ) )
260       {
261         QString aPreviousText = aCursor.block().previous().text();
262         int numSpacesPrevious = findFirstNonSpace( aPreviousText );
263         if ( numSpaces == numSpacesPrevious )
264         {
265           tabIndentation( true );
266           aCursor.movePosition( QTextCursor::EndOfBlock );
267           setTextCursor( aCursor );
268         }
269       }
270       QPlainTextEdit::keyPressEvent( event );
271     }
272     else
273     {
274       QPlainTextEdit::keyPressEvent( event );
275     }
276   }
277 }
278
279 /*!
280   \brief Handle resize event.
281   Reimplemented from QPlainTextEdit.
282   \param event resize event
283 */
284 void PyEditor_Editor::resizeEvent( QResizeEvent* event )
285 {
286   QPlainTextEdit::resizeEvent( event );
287
288   // Change size geometry of line number area
289   QRect aContentsRect = contentsRect();
290   myLineNumberArea->setGeometry( QRect( aContentsRect.left(),
291                                         aContentsRect.top(),
292                                         lineNumberAreaWidth(),
293                                         aContentsRect.height() ) );
294 }
295
296 /*!
297   \brief Paint event.
298   Reimplemented from QPlainTextEdit.
299   \param event paint event
300 */
301 void PyEditor_Editor::paintEvent( QPaintEvent* event )
302 {
303   QPlainTextEdit::paintEvent( event );
304
305   QTextBlock aBlock( firstVisibleBlock() );
306   QPointF anOffset( contentOffset() );
307   QPainter aPainter( this->viewport() );
308
309   int aTabSpaces = this->tabStopWidth() / 10;
310
311   // Visualization tab spaces
312   if ( mySettings.tabSpaceVisible() )
313   {
314     qreal aTop = blockBoundingGeometry( aBlock ).translated( anOffset ).top();
315     while ( aBlock.isValid() && aTop <= event->rect().bottom() )
316     {
317       if ( aBlock.isVisible() && blockBoundingGeometry( aBlock ).translated( anOffset ).toRect().intersects( event->rect() ) )
318       {
319         QString aText = aBlock.text();
320         if ( aText.contains( QRegExp( "\\w+" ) ) )
321           aText.remove( QRegExp( "(?!\\w+)\\s+$" ) );
322         
323         int aColumn = 0;
324         int anIndex = 0;
325         while ( anIndex != -1 )
326         {
327           anIndex = aText.indexOf( QRegExp( QString( "^\\s{%1}" ).arg( aTabSpaces ) ), 0 );
328           if ( anIndex != -1 )
329           {
330             aColumn = aColumn + aTabSpaces;
331             aText = aText.mid( aTabSpaces );
332     
333             if ( aText.startsWith( ' ' ) )
334             {
335               QTextCursor aCursor( aBlock );
336               aCursor.setPosition( aBlock.position() + aColumn );
337       
338               QRect aRect = cursorRect( aCursor );
339               aPainter.setPen( QPen( Qt::darkGray, 1, Qt::DotLine ) );
340               aPainter.drawLine( aRect.x() + 1, aRect.top(), aRect.x() + 1, aRect.bottom() );
341             }
342           }
343         }
344       }
345       aBlock = aBlock.next();
346     }
347   }
348   
349   // Vertical edge line
350   if ( mySettings.verticalEdge() )
351   {
352     const QRect aRect = event->rect();
353     const QFont aFont = currentCharFormat().font();
354     int aNumberColumn =  QFontMetrics( aFont ).averageCharWidth() * mySettings.numberColumns() + anOffset.x() + document()->documentMargin();
355     aPainter.setPen( QPen( Qt::lightGray, 1, Qt::SolidLine ) );
356     aPainter.drawLine( aNumberColumn, aRect.top(), aNumberColumn, aRect.bottom() );
357   }
358 }
359
360 void PyEditor_Editor::contextMenuEvent( QContextMenuEvent* event )
361 {
362   QMenu* menu = createStandardContextMenu();
363   emit customizeMenu( menu );
364   menu->exec( event->globalPos() );
365   delete menu;
366 }
367
368 /*!
369   \brief Indent and tab text.
370   \param isShift flag defines reverse tab direction
371 */
372 void PyEditor_Editor::tabIndentation( bool isShift )
373 {
374   QTextCursor aCursor = textCursor();
375   int aTabSpaces = this->tabStopWidth()/10;
376
377   if ( !aCursor.hasSelection() )
378   {
379     if ( !isShift )
380     {
381       int N = aCursor.columnNumber() % aTabSpaces;
382       aCursor.insertText( QString( aTabSpaces - N, QLatin1Char( ' ' ) ) );
383     }
384     else
385     {
386       QTextBlock aCurrentBlock = document()->findBlock( aCursor.position() );
387       int anIndentPos = findFirstNonSpace( aCurrentBlock.text() );
388       aCursor.setPosition( aCurrentBlock.position() + anIndentPos );
389       setTextCursor( aCursor );
390       
391       //if ( aCurrCursorColumnPos <= anIndentPos )
392       //{
393       int aColumnPos = aCursor.columnNumber();
394       if ( aColumnPos != 0 )
395       {
396         int N = aCursor.columnNumber() % aTabSpaces;
397         if ( N == 0 ) N = aTabSpaces;
398         aCursor.movePosition( QTextCursor::Left, QTextCursor::KeepAnchor, N );
399         aCursor.removeSelectedText();
400       }
401       setTextCursor( aCursor );
402       //}
403     }
404   }
405   else
406   {
407     indentSelection( isShift );
408   }
409 }
410
411 /*!
412   \brief Indent and tab selected text.
413   \param isShift flag defines reverse tab direction
414 */
415 void PyEditor_Editor::indentSelection( bool isShift )
416 {
417   QTextCursor aCursor = this->textCursor();
418
419   int aCursorStart = aCursor.selectionStart();
420   int aCursorEnd = aCursor.selectionEnd();
421
422   QTextBlock aStartBlock = document()->findBlock( aCursorStart );
423   QTextBlock anEndBlock = document()->findBlock( aCursorEnd - 1 ).next();
424
425   int aTabSpaces = this->tabStopWidth()/10;
426
427   for ( QTextBlock aBlock = aStartBlock; aBlock.isValid() && aBlock != anEndBlock; aBlock = aBlock.next() )
428   {
429     QString aText = aBlock.text();
430     int anIndentPos = findFirstNonSpace( aText );
431     int N = ( anIndentPos % aTabSpaces );
432     
433     aCursor.setPosition( aBlock.position() + anIndentPos );
434     if ( !isShift )
435     {
436       aCursor.insertText( QString( aTabSpaces - N, QLatin1Char( ' ' ) ) );
437       setTextCursor( aCursor );
438     }
439     else
440     {
441       int aColumnPos = aCursor.columnNumber();
442       if ( aColumnPos != 0 )
443       {
444         int blockN = aColumnPos % aTabSpaces;
445         if ( blockN == 0 ) blockN = aTabSpaces;
446         aCursor.movePosition( QTextCursor::Left, QTextCursor::KeepAnchor, blockN );
447         aCursor.removeSelectedText();
448         setTextCursor( aCursor );
449       }
450     }
451   }
452
453   // Reselect the selected lines
454   aCursor.setPosition( aStartBlock.position() );
455   aCursor.setPosition( anEndBlock.previous().position(), QTextCursor::KeepAnchor );
456   aCursor.movePosition( QTextCursor::EndOfBlock, QTextCursor::KeepAnchor );
457   setTextCursor( aCursor );
458 }
459
460 /*!
461   \brief Find first non white-space symbol in text.
462   \param text input text
463   \return index of first non white-space symbol
464 */
465 int PyEditor_Editor::findFirstNonSpace( const QString& text )
466 {
467   int i = 0;
468   while ( i < text.size() )
469   {
470     if ( !text.at(i).isSpace() )
471       return i;
472     ++i;
473   }
474   return i;
475 }
476
477 /*!
478   \brief Indent line.
479   \return error code
480 */
481 int PyEditor_Editor::lineIndent()
482 {
483   int aTabSpaces = this->tabStopWidth() / 10;
484
485   QTextCursor aCursor = textCursor();
486   aCursor.insertBlock();
487   setTextCursor( aCursor );
488
489   QTextBlock aCurrentBlock = aCursor.block();
490   if ( aCurrentBlock == document()->begin() )
491     return 0;
492
493   QTextBlock aPreviousBlock = aCurrentBlock.previous();
494
495   QString aPreviousText;
496   forever
497   {
498     if ( aPreviousBlock == document()->begin() )
499     {
500       aPreviousText = aPreviousBlock.text();
501       if ( aPreviousText.isEmpty() && aPreviousText.trimmed().isEmpty() )
502         return -1;
503       break;
504     }
505     
506     // If the text of this block is not empty then break the loop.
507     aPreviousText = aPreviousBlock.text();
508     if ( !aPreviousText.isEmpty() && !aPreviousText.trimmed().isEmpty() )
509       break;
510     
511     aPreviousBlock = aPreviousBlock.previous();
512   }
513   
514   int aTabIndentation = 0;
515   int anAmountIndentation = -1;
516   int i = 0;
517   while ( i < aPreviousText.size() )
518   {
519     if ( !aPreviousText.at(i).isSpace() )
520     {
521       anAmountIndentation = findFirstNonSpace( aPreviousText );
522       break;
523     }
524     else
525     {
526       ++aTabIndentation;
527     }
528     ++i;
529   }
530   
531   if ( anAmountIndentation == -1 )
532   {
533     if ( aTabIndentation > 0 )
534       anAmountIndentation = aTabIndentation;
535     else
536       return 0;
537   }
538   
539   const QString aPreviousTrimmed = aPreviousText.trimmed();
540   if ( aPreviousTrimmed.endsWith( ":" ) )
541   {
542     anAmountIndentation += aTabSpaces;
543   }
544   else
545   {
546     if ( aPreviousTrimmed == "continue"
547       || aPreviousTrimmed == "break"
548       || aPreviousTrimmed == "pass"
549       || aPreviousTrimmed == "return"
550       || aPreviousTrimmed == "raise"
551       || aPreviousTrimmed.startsWith( "raise " )
552       || aPreviousTrimmed.startsWith( "return " ) )
553       anAmountIndentation -= aTabSpaces;
554   }
555   
556   aCursor.insertText( QString( anAmountIndentation, QLatin1Char(' ') ) );
557   setTextCursor( aCursor );
558   
559   return 1;
560 }
561
562 /*!
563   \brief Set text cursor on home position.
564   \param isExtendLine \c true to keep current anchor position
565 */
566 void PyEditor_Editor::handleHome( bool isExtendLine )
567 {
568   QTextCursor aCursor = textCursor();
569   QTextCursor::MoveMode aMode = QTextCursor::MoveAnchor;
570
571   if ( isExtendLine )
572     aMode = QTextCursor::KeepAnchor;
573
574   int anInitPos = aCursor.position();
575   int aBlockPos = aCursor.block().position();
576
577   QChar aCharacter = document()->characterAt( aBlockPos );
578   while ( aCharacter.category() == QChar::Separator_Space )
579   {
580     ++aBlockPos;
581     if ( aBlockPos == anInitPos )
582       break;
583     aCharacter = document()->characterAt( aBlockPos );
584   }
585   
586   if ( aBlockPos == anInitPos )
587     aBlockPos = aCursor.block().position();
588   
589   aCursor.setPosition( aBlockPos, aMode );
590   setTextCursor( aCursor );
591 }
592
593 /*!
594   \brief Update current line highlighting.
595 */
596 void PyEditor_Editor::updateHighlightCurrentLine()
597 {
598   QList<QTextEdit::ExtraSelection> anExtraSelections;
599   if ( !isReadOnly() && mySettings.highlightCurrentLine() )
600   {
601     QTextEdit::ExtraSelection selection;
602     
603     QColor lineColor = QColor( Qt::gray ).lighter( 155 );
604     
605     selection.format.setBackground( lineColor );
606     selection.format.setProperty( QTextFormat::FullWidthSelection, QVariant( true ) );
607     selection.cursor = textCursor();
608     selection.cursor.clearSelection();
609     anExtraSelections.append( selection );
610   }
611   setExtraSelections( anExtraSelections );
612 }
613
614 /*!
615   \brief Draw linne number area.
616   \param event paint event
617 */
618 void PyEditor_Editor::lineNumberAreaPaintEvent( QPaintEvent* event )
619 {
620   QPainter aPainter( myLineNumberArea );
621   aPainter.fillRect( event->rect(), QColor( Qt::lightGray ).lighter( 125 ) );
622
623   QTextBlock aBlock = firstVisibleBlock();
624   int aBlockNumber = aBlock.blockNumber();
625   int aTop = (int)blockBoundingGeometry( aBlock ).translated( contentOffset() ).top();
626   int aBottom = aTop + (int)blockBoundingRect( aBlock ).height();
627   int aCurrentLine = document()->findBlock( textCursor().position() ).blockNumber();
628
629   QFont aFont = aPainter.font();
630   aPainter.setPen( this->palette().color( QPalette::Text ) );
631
632   while ( aBlock.isValid() && aTop <= event->rect().bottom() )
633   {
634     if ( aBlock.isVisible() && aBottom >= event->rect().top() )
635     {
636       if ( aBlockNumber == aCurrentLine )
637       {
638         aPainter.setPen( Qt::darkGray );
639         aFont.setBold( true );
640         aPainter.setFont( aFont );
641       }
642       else
643       {
644         aPainter.setPen( Qt::gray ) ;
645         aFont.setBold( false );
646         aPainter.setFont( aFont );
647       }
648       QString aNumber = QString::number( aBlockNumber + 1 );
649       aPainter.drawText( 0, aTop, myLineNumberArea->width(), fontMetrics().height(), Qt::AlignRight, aNumber );
650     }
651
652     aBlock = aBlock.next();
653     aTop = aBottom;
654     aBottom = aTop + (int)blockBoundingRect( aBlock ).height();
655     ++aBlockNumber;
656   }
657 }
658
659 /*!
660   \brief Get with of line number area.
661   \return width of line number area
662 */
663 int PyEditor_Editor::lineNumberAreaWidth()
664 {
665   int aSpace = 0;
666
667   int aDigits = 1;
668   int aMaximum = qMax( 1, blockCount() );
669   while ( aMaximum >= 10 )
670   {
671     aMaximum /= 10;
672     ++aDigits;
673   }
674
675   if ( mySettings.lineNumberArea() )
676     aSpace += 5 + fontMetrics().width( QLatin1Char( '9' ) ) * aDigits;
677   
678   return aSpace;
679 }
680
681 /*!
682   \brief Update width of the line number area.
683   \param newBlockCount (not used)
684 */
685 void PyEditor_Editor::updateLineNumberAreaWidth( int /*newBlockCount*/ )
686 {
687   setViewportMargins( lineNumberAreaWidth(), 0, 0, 0 );
688 }
689
690 /*!
691   \brief Update line number area (when editor viewport is scrolled).
692   \param rect area being updated
693   \param dy scroll factor
694 */
695 void PyEditor_Editor::updateLineNumberArea( const QRect& rect, int dy )
696 {
697   if ( dy )
698     myLineNumberArea->scroll( 0, dy );
699   else
700     myLineNumberArea->update( 0, rect.y(), myLineNumberArea->width(), rect.height() );
701
702   if ( rect.contains( viewport()->rect() ) )
703     updateLineNumberAreaWidth( 0 );
704 }
705
706 /*!
707   \brief Manage parentheses.
708 */
709 void PyEditor_Editor::matchParentheses()
710 {
711   PyEditor_PyHighlighter::TextBlockData* data =
712     static_cast<PyEditor_PyHighlighter::TextBlockData*>( textCursor().block().userData() );
713
714   if ( data )
715   {
716     QVector<PyEditor_PyHighlighter::ParenthesisInfo*> infoEntries = data->parentheses();
717     
718     int aPos = textCursor().block().position();
719     bool ignore = false;
720     for ( int i = 0; i < infoEntries.size(); ++i )
721     {
722       PyEditor_PyHighlighter::ParenthesisInfo* info = infoEntries.at(i);
723       
724       int currentColumnPosition = textCursor().columnNumber();
725       if ( info->position == currentColumnPosition - 1 && isLeftBrackets( info->character ) )
726       {
727         if ( matchLeftParenthesis( textCursor().block(), i + 1, 0 ) )
728           createParenthesisSelection( aPos + info->position );
729       }
730       else if ( info->position == currentColumnPosition && isLeftBrackets( info->character ) )
731       {
732         if ( !ignore )
733         {
734           if ( matchLeftParenthesis( textCursor().block(), i + 1, 0 ) )
735             createParenthesisSelection( aPos + info->position );
736         }
737       }
738       else if ( info->position == currentColumnPosition - 1 && isRightBrackets( info->character ) )
739       {
740         if ( matchRightParenthesis( textCursor().block(), i - 1, 0 ) )
741           createParenthesisSelection( aPos + info->position );
742         ignore = true;
743       }
744       else if ( info->position == currentColumnPosition && isRightBrackets( info->character ) )
745       {
746         if ( matchRightParenthesis( textCursor().block(), i - 1, 0 ) )
747           createParenthesisSelection( aPos + info->position );
748       }
749     }
750   }
751 }
752
753 /*!
754   \brief Match left brackets.
755   \param currentBlock text block
756   \param idx index
757   \param numLeftParentheses number of left parentheses
758   \return \c true if the left match
759 */
760 bool PyEditor_Editor::matchLeftParenthesis( const QTextBlock& currentBlock,
761                                             int idx, int numLeftParentheses )
762 {
763   PyEditor_PyHighlighter::TextBlockData* data =
764     static_cast<PyEditor_PyHighlighter::TextBlockData*>( currentBlock.userData() );
765   QVector<PyEditor_PyHighlighter::ParenthesisInfo*> infos = data->parentheses();
766
767   int docPos = currentBlock.position();
768   for ( ; idx < infos.size(); ++idx )
769   {
770     PyEditor_PyHighlighter::ParenthesisInfo* info = infos.at( idx );
771
772     if ( isLeftBrackets( info->character ) )
773     {
774       ++numLeftParentheses;
775       continue;
776     }
777
778     if ( isRightBrackets( info->character ) && numLeftParentheses == 0 )
779     {
780       createParenthesisSelection( docPos + info->position );
781       return true;
782     }
783     else
784       --numLeftParentheses;
785   }
786
787   QTextBlock nextBlock = currentBlock.next();
788   if ( nextBlock.isValid() )
789     return matchLeftParenthesis( nextBlock, 0, numLeftParentheses );
790
791   return false;
792 }
793
794 /*!
795   \brief Match right brackets.
796   \param currentBlock text block
797   \param idx index
798   \param numRightParentheses number of right parentheses
799   \return \c true if the right match
800 */
801 bool PyEditor_Editor::matchRightParenthesis( const QTextBlock& currentBlock,
802                                              int idx, int numRightParentheses )
803 {
804   PyEditor_PyHighlighter::TextBlockData* data = static_cast<PyEditor_PyHighlighter::TextBlockData*>( currentBlock.userData() );
805   QVector<PyEditor_PyHighlighter::ParenthesisInfo*> parentheses = data->parentheses();
806
807   int docPos = currentBlock.position();
808   for ( ; idx > -1 && parentheses.size() > 0; --idx )
809   {
810     PyEditor_PyHighlighter::ParenthesisInfo* info = parentheses.at( idx );
811     if ( isRightBrackets( info->character ) )
812     {
813       ++numRightParentheses;
814       continue;
815     }
816     if ( isLeftBrackets( info->character ) && numRightParentheses == 0 )
817     {
818       createParenthesisSelection( docPos + info->position );
819       return true;
820     }
821     else
822       --numRightParentheses;
823   }
824
825   QTextBlock prevBlock = currentBlock.previous();
826   if ( prevBlock.isValid() )
827   {
828     PyEditor_PyHighlighter::TextBlockData* data = static_cast<PyEditor_PyHighlighter::TextBlockData*>( prevBlock.userData() );
829     QVector<PyEditor_PyHighlighter::ParenthesisInfo*> parentheses = data->parentheses();
830     return matchRightParenthesis( prevBlock, parentheses.size() - 1, numRightParentheses );
831   }
832
833   return false;
834 }
835
836 /*!
837   \brief Create brackets selection.
838   \param position cursor position
839 */
840 void PyEditor_Editor::createParenthesisSelection( int position )
841 {
842   QList<QTextEdit::ExtraSelection> selections = extraSelections();
843
844   QTextEdit::ExtraSelection selection;
845
846   QTextCharFormat format = selection.format;
847   format.setForeground( Qt::red );
848   format.setBackground( Qt::white );
849   selection.format = format;
850
851   QTextCursor cursor = textCursor();
852   cursor.setPosition( position );
853   cursor.movePosition( QTextCursor::NextCharacter, QTextCursor::KeepAnchor );
854   selection.cursor = cursor;
855
856   selections.append( selection );
857   setExtraSelections( selections );
858 }
859
860 /*!
861   \brief Check if symbol is a left bracket.
862   \param symbol text symbol
863   \return \c true if symbol is any left bracket
864 */
865 bool PyEditor_Editor::isLeftBrackets( QChar symbol )
866 {
867   return symbol == '(' || symbol == '{' || symbol == '[';
868 }
869
870 /*!
871   \brief Check if symbol is a right bracket.
872   \param symbol text symbol
873   \return \c true if symbol is any right bracket
874 */
875 bool PyEditor_Editor::isRightBrackets( QChar symbol )
876 {
877   return symbol == ')' || symbol == '}' || symbol == ']';
878 }
879
880 /*!
881   \brief Append new paragraph to the end of the editor's text.
882   \param text paragraph text
883 */
884 void PyEditor_Editor::append( const QString& text )
885 {
886   appendPlainText( text );
887 }
888
889 /*!
890   \brief Set text to the editor.
891   \param text new text
892 */
893 void PyEditor_Editor::setText( const QString& text )
894 {
895   setPlainText( text );
896 }
897
898 /*!
899   \brief Get current editor's content.
900   \return current text
901 */
902 QString PyEditor_Editor::text() const
903 {
904   return toPlainText();
905 }
906
907 /*!
908   \brief Get user keywords dictionary.
909   \return keywords dictionary
910 */
911 PyEditor_Keywords* PyEditor_Editor::userKeywords() const
912 {
913   return myUserKeywords;
914 }
915
916 /*!
917   \brief Get standard keywords dictionary.
918   \return keywords dictionary
919 */
920 PyEditor_Keywords* PyEditor_Editor::standardKeywords() const
921 {
922   return myStdKeywords;
923 }
924
925 /*!
926   \brief Move cursor to the given line.
927   \note Line count starts from 1.
928   \param line Line number.
929 */
930 void PyEditor_Editor::setCurrentLine( int line )
931 {
932   QTextCursor cursor( document()->findBlockByLineNumber( line - 1 ) );
933   setTextCursor( cursor );
934   ensureCursorVisible();
935 }