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