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