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