scide: refactoring - move LineIndicator to separate source file
[supercollider.git] / editors / sc-ide / widgets / code_editor / editor.cpp
blob9a9293a6d00eaef8ac7a8d33173fd899e85c66b7
1 /*
2 SuperCollider Qt IDE
3 Copyright (c) 2012 Jakob Leben & Tim Blechmann
4 http://www.audiosynth.com
6 This program is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 2 of the License, or
9 (at your option) any later version.
11 This program is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
16 You should have received a copy of the GNU General Public License
17 along with this program; if not, write to the Free Software
18 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
21 #include "autocompleter.hpp"
22 #include "editor.hpp"
23 #include "highlighter.hpp"
24 #include "line_indicator.hpp"
25 #include "../sc_symbol_references.hpp"
26 #include "../../core/doc_manager.hpp"
27 #include "../../core/main.hpp"
28 #include "../../core/settings/manager.hpp"
30 #include <QApplication>
31 #include <QDebug>
32 #include <QKeyEvent>
33 #include <QPainter>
34 #include <QPaintEvent>
35 #include <QScrollBar>
36 #include <QTextBlock>
37 #include <QTextDocumentFragment>
38 #include <QUrl>
40 namespace ScIDE {
42 CodeEditor::CodeEditor( Document *doc, QWidget *parent ) :
43 QPlainTextEdit( parent ),
44 mLineIndicator( new LineIndicator(this) ),
45 mDoc(doc),
46 mSpaceIndent(true),
47 mBlinkDuration(600),
48 mMouseBracketMatch(false),
49 mOverlay( new QGraphicsScene(this) ),
50 mAutoCompleter( new AutoCompleter(this) )
52 Q_ASSERT(mDoc != 0);
54 setFrameShape( QFrame::NoFrame );
56 mLineIndicator->move( contentsRect().topLeft() );
58 connect( mDoc, SIGNAL(defaultFontChanged()), this, SLOT(onDocumentFontChanged()) );
60 connect( this, SIGNAL(blockCountChanged(int)),
61 mLineIndicator, SLOT(setLineCount(int)) );
63 connect( mLineIndicator, SIGNAL( widthChanged() ),
64 this, SLOT( updateLayout() ) );
66 connect( this, SIGNAL(updateRequest(QRect,int)),
67 this, SLOT(updateLineIndicator(QRect,int)) );
69 connect( this, SIGNAL(selectionChanged()),
70 mLineIndicator, SLOT(update()) );
72 connect( this, SIGNAL(cursorPositionChanged()),
73 this, SLOT(matchBrackets()) );
75 connect( mOverlay, SIGNAL(changed(const QList<QRectF>&)),
76 this, SLOT(onOverlayChanged(const QList<QRectF>&)) );
78 connect( Main::instance(), SIGNAL(applySettingsRequest(Settings::Manager*)),
79 this, SLOT(applySettings(Settings::Manager*)) );
81 QTextDocument *tdoc = doc->textDocument();
82 QPlainTextEdit::setDocument(tdoc);
83 mAutoCompleter->documentChanged(tdoc);
84 onDocumentFontChanged();
85 mLineIndicator->setLineCount(blockCount());
87 applySettings(Main::settings());
90 bool CodeEditor::showWhitespace()
92 QTextOption options( textDocument()->defaultTextOption() );
93 return options.flags().testFlag( QTextOption::ShowTabsAndSpaces );
96 void CodeEditor::setShowWhitespace(bool show)
98 QTextDocument *doc = textDocument();
99 QTextOption opt( doc->defaultTextOption() );
100 if( show )
101 opt.setFlags( opt.flags() | QTextOption::ShowTabsAndSpaces );
102 else
103 opt.setFlags( opt.flags() & ~QTextOption::ShowTabsAndSpaces );
104 doc->setDefaultTextOption(opt);
107 static bool findInBlock(QTextDocument *doc, const QTextBlock &block, const QRegExp &expr, int offset,
108 QTextDocument::FindFlags options, QTextCursor &cursor)
110 QString text = block.text();
111 if(options & QTextDocument::FindBackward)
112 text.truncate(offset);
113 text.replace(QChar::Nbsp, QLatin1Char(' '));
115 int idx = -1;
116 while (offset >=0 && offset <= text.length()) {
117 idx = (options & QTextDocument::FindBackward) ?
118 expr.lastIndexIn(text, offset) : expr.indexIn(text, offset);
119 if (idx == -1)
120 return false;
122 if (options & QTextDocument::FindWholeWords) {
123 const int start = idx;
124 const int end = start + expr.matchedLength();
125 if ((start != 0 && text.at(start - 1).isLetterOrNumber())
126 || (end != text.length() && text.at(end).isLetterOrNumber())) {
127 //if this is not a whole word, continue the search in the string
128 offset = (options & QTextDocument::FindBackward) ? idx-1 : end+1;
129 idx = -1;
130 continue;
133 //we have a hit, return the cursor for that.
134 break;
137 if (idx == -1)
138 return false;
140 cursor = QTextCursor(doc);
141 cursor.setPosition(block.position() + idx);
142 cursor.setPosition(cursor.position() + expr.matchedLength(), QTextCursor::KeepAnchor);
143 return true;
146 bool CodeEditor::find( const QRegExp &expr, QTextDocument::FindFlags options )
148 // Although QTextDocument provides a find() method, we implement
149 // our own, because the former one is not adequate.
151 if(expr.isEmpty()) return true;
153 bool backwards = options & QTextDocument::FindBackward;
155 QTextCursor c( textCursor() );
156 int pos;
157 if (c.hasSelection())
159 bool matching = expr.exactMatch(c.selectedText());
161 if( backwards == matching )
162 pos = c.selectionStart();
163 else
164 pos = c.selectionEnd();
166 else
167 pos = c.position();
169 QTextDocument *doc = QPlainTextEdit::document();
170 QTextBlock startBlock = doc->findBlock(pos);
171 int startBlockOffset = pos - startBlock.position();
173 QTextCursor cursor;
175 if (!backwards) {
176 int blockOffset = startBlockOffset;
177 QTextBlock block = startBlock;
178 while (block.isValid()) {
179 if (findInBlock(doc, block, expr, blockOffset, options, cursor))
180 break;
181 blockOffset = 0;
182 block = block.next();
184 if(cursor.isNull())
186 blockOffset = 0;
187 block = doc->begin();
188 while(true) {
189 if (findInBlock(doc, block, expr, blockOffset, options, cursor)
190 || block == startBlock)
191 break;
192 block = block.next();
195 } else {
196 int blockOffset = startBlockOffset;
197 QTextBlock block = startBlock;
198 while (block.isValid()) {
199 if (findInBlock(doc, block, expr, blockOffset, options, cursor))
200 break;
201 block = block.previous();
202 blockOffset = block.length() - 1;
204 if(cursor.isNull())
206 block = doc->end();
207 while(true) {
208 blockOffset = block.length() - 1;
209 if (findInBlock(doc, block, expr, blockOffset, options, cursor)
210 || block == startBlock)
211 break;
212 block = block.previous();
217 if(!cursor.isNull()) {
218 setTextCursor(cursor);
219 return true;
221 else
222 return false;
225 int CodeEditor::findAll( const QRegExp &expr, QTextDocument::FindFlags options )
227 mSearchSelections.clear();
229 if(expr.isEmpty()) {
230 updateExtraSelections();
231 return 0;
234 QTextEdit::ExtraSelection selection;
235 selection.format.setBackground(Qt::darkYellow);
237 QTextDocument *doc = QPlainTextEdit::document();
238 QTextBlock block = doc->begin();
239 QTextCursor cursor;
241 while (block.isValid()) {
242 int blockPos = block.position();
243 int offset = 0;
244 while(findInBlock(doc, block, expr, offset, options, cursor))
246 offset = cursor.selectionEnd() - blockPos;
247 selection.cursor = cursor;
248 mSearchSelections.append(selection);
250 block = block.next();
253 updateExtraSelections();
255 return mSearchSelections.count();
258 //#define CSTR(QSTR) QSTR.toStdString().c_str()
260 static QString resolvedReplacement( const QString &replacement, const QRegExp &expr )
262 //qDebug("START");
263 static const QRegExp rexpr("(\\\\\\\\)|(\\\\[0-9]+)");
264 QString str( replacement );
265 int i=0;
266 while(i < str.size() && ((i = rexpr.indexIn(str, i)) != -1))
268 int len = rexpr.matchedLength();
269 if(rexpr.pos(1) != -1)
271 //qDebug("%i (%s): escape", i, CSTR(rexpr.cap(1)));
272 str.replace(i, len, "\\");
273 i += 1;
275 else if(rexpr.pos(2) != -1)
277 QString num_str = rexpr.cap(2);
278 num_str.remove(0, 1);
279 int num = num_str.toInt();
280 //qDebug("%i (%s): backref = %i", i, CSTR(rexpr.cap(2)), num);
281 if(num <= expr.captureCount())
283 QString cap = expr.cap(num);
284 //qDebug("resolving ref to: %s", CSTR(cap));
285 str.replace(i, len, cap);
286 i += cap.size();
288 else
290 //qDebug("ref out of range", i, num);
291 str.remove(i, len);
294 else
296 //qDebug("%i (%s): unknown match", i, CSTR(rexpr.cap(0)));
297 str.remove(i, len);
299 //qDebug(">> [%s] %i", CSTR(str), i);
301 //qDebug("END");
302 return str;
305 bool CodeEditor::replace( const QRegExp &expr, const QString &replacement, QTextDocument::FindFlags options )
307 if(expr.isEmpty()) return true;
309 QTextCursor cursor = textCursor();
310 if (cursor.hasSelection() && expr.exactMatch(cursor.selectedText()))
312 QString rstr = replacement;
313 if(expr.patternSyntax() != QRegExp::FixedString)
314 rstr = resolvedReplacement(rstr, expr);
315 cursor.insertText(rstr);
318 return find(expr, options);
321 int CodeEditor::replaceAll( const QRegExp &expr, const QString &replacement, QTextDocument::FindFlags options )
323 mSearchSelections.clear();
324 updateExtraSelections();
326 if(expr.isEmpty()) return 0;
328 int replacements = 0;
329 bool caps = expr.patternSyntax() != QRegExp::FixedString;
331 QTextDocument *doc = QPlainTextEdit::document();
332 QTextBlock block = doc->begin();
333 QTextCursor cursor;
335 QTextCursor(doc).beginEditBlock();
337 while (block.isValid())
339 int blockPos = block.position();
340 int offset = 0;
341 while(findInBlock(doc, block, expr, offset, options, cursor))
343 QString rstr = replacement;
344 if(caps)
345 rstr = resolvedReplacement(rstr, expr);
346 cursor.insertText(rstr);
347 ++replacements;
348 offset = cursor.selectionEnd() - blockPos;
350 block = block.next();
353 QTextCursor(doc).endEditBlock();
355 return replacements;
358 QTextCursor CodeEditor::currentRegion()
360 return regionAtCursor(textCursor());
364 QTextCursor CodeEditor::regionAtCursor(QTextCursor c)
366 QTextBlock block(c.block());
368 int positionInBlock = c.position() - block.position();
369 TokenIterator start;
370 TokenIterator end;
371 int topLevel = 0;
372 int level = 0;
374 // search unmatched opening bracket
375 TokenIterator it = TokenIterator::leftOf( block, positionInBlock );
376 while(it.isValid())
378 char chr = it->character;
379 if(chr == '(') {
380 ++level;
381 if(level > topLevel) {
382 topLevel = level;
383 start = it;
386 else if(chr == ')') {
387 --level;
389 --it;
392 if(topLevel < 1)
393 // no unmatched opening bracket
394 return QTextCursor();
396 // match the found opening bracket
397 it = TokenIterator::rightOf( block, positionInBlock );
398 while(it.isValid())
400 char chr = it->character;
401 if(chr == '(')
402 ++topLevel;
403 else if(chr == ')')
405 --topLevel;
406 if(topLevel == 0)
408 end = it;
409 break;
412 ++it;
415 if(start.isValid() && end.isValid())
417 // only care about brackets at beginning of a line
418 if(start->positionInBlock != 0)
419 return QTextCursor();
421 // check whether the bracket makes part of an event
422 it = start.next();
423 if (it.isValid()) {
424 if (it->type == Token::SymbolArg)
425 return QTextCursor();
426 else {
427 ++it;
428 if (it.isValid() && it->character == ':')
429 return QTextCursor();
433 // ok, this is is a real top-level region
434 QTextCursor c(QPlainTextEdit::document());
435 c.setPosition(start.position() + 1);
436 c.setPosition(end.position(), QTextCursor::KeepAnchor);
437 return c;
440 return QTextCursor();
443 QTextCursor CodeEditor::blockAtCursor(QTextCursor cursor)
445 TokenIterator it (cursor.block(), cursor.positionInBlock());
447 if (it.isValid()) {
448 switch (it->type) {
449 case Token::OpeningBracket:
450 case Token::ClosingBracket:
452 BracketMatch match;
453 matchBracket(it, match);
455 if (match.first.isValid()) {
456 QTextCursor selection(textDocument());
457 selection.setPosition(match.first.position());
458 selection.setPosition(match.second.position() + 1, QTextCursor::KeepAnchor);
459 return selection;
461 break;
464 default:
465 break;
469 return QTextCursor();
473 void CodeEditor::showPosition( int pos )
475 if (pos < 0) return;
477 QTextDocument *doc = QPlainTextEdit::document();
478 if (!doc) return;
480 int lineNumber = doc->findBlock(pos).firstLineNumber();
481 verticalScrollBar()->setValue(lineNumber);
483 QTextCursor cursor(doc);
484 cursor.setPosition(pos);
485 setTextCursor(cursor);
488 QString CodeEditor::symbolUnderCursor()
490 QTextCursor cursor = textCursor();
491 if (!cursor.hasSelection())
492 cursor.select(QTextCursor::WordUnderCursor);
493 return cursor.selectedText();
496 void CodeEditor::clearSearchHighlighting()
498 mSearchSelections.clear();
499 updateExtraSelections();
502 void CodeEditor::zoomIn(int steps)
504 zoomFont(steps);
507 void CodeEditor::zoomOut(int steps)
509 zoomFont(-steps);
512 void CodeEditor::resetFontSize()
514 mDoc->resetDefaultFont();
517 void CodeEditor::zoomFont(int steps)
519 QFont currentFont = mDoc->defaultFont();
520 const int newSize = currentFont.pointSize() + steps;
521 if (newSize <= 0)
522 return;
523 currentFont.setPointSize(newSize);
524 mDoc->setDefaultFont(currentFont);
527 void CodeEditor::applySettings( Settings::Manager *settings )
529 settings->beginGroup("IDE/editor");
531 mSpaceIndent = settings->value("spaceIndent").toBool();
533 mBlinkDuration = settings->value("blinkDuration").toInt();
535 QPalette palette;
537 settings->beginGroup("colors");
539 if (settings->contains("text")) {
540 QTextCharFormat format = settings->value("text").value<QTextCharFormat>();
541 QBrush bg = format.background();
542 QBrush fg = format.foreground();
543 if (bg.style() != Qt::NoBrush)
544 palette.setBrush(QPalette::Base, bg);
545 if (fg.style() != Qt::NoBrush)
546 palette.setBrush(QPalette::Text, fg);
549 if (settings->contains("lineNumbers")) {
550 QPalette lineNumPlt;
551 QTextCharFormat format = settings->value("lineNumbers").value<QTextCharFormat>();
552 QBrush bg = format.background();
553 QBrush fg = format.foreground();
554 if (bg.style() != Qt::NoBrush)
555 palette.setBrush(QPalette::Button, bg);
556 if (fg.style() != Qt::NoBrush)
557 palette.setBrush(QPalette::ButtonText, fg);
558 mLineIndicator->setPalette(lineNumPlt);
561 mBracketHighlight = settings->value("matchingBrackets").value<QTextCharFormat>();
563 settings->endGroup(); // colors
565 setPalette(palette);
567 settings->endGroup();
570 void CodeEditor::onDocumentFontChanged()
572 QFont font = mDoc->defaultFont();
573 setFont(font);
576 void CodeEditor::deleteTrailingSpaces()
578 mDoc->deleteTrailingSpaces();
581 bool CodeEditor::event( QEvent *e )
583 switch (e->type())
585 case QEvent::KeyPress:
587 QKeyEvent *ke = static_cast<QKeyEvent*>(e);
588 int key = ke->key();
589 switch (key)
591 case Qt::Key_Tab:
592 indent();
593 e->accept();
594 return true;
595 default:;
597 break;
599 default:;
601 return QPlainTextEdit::event(e);
604 void CodeEditor::keyPressEvent( QKeyEvent *e )
606 hideMouseCursor();
608 switch (e->key()) {
609 case Qt::Key_Enter:
610 case Qt::Key_Return:
611 // override to avoid entering a "soft" new line when certain modifier is held
612 textCursor().insertBlock();
613 ensureCursorVisible();
614 break;
615 case Qt::Key_Home:
617 Qt::KeyboardModifiers mods(e->modifiers());
618 if (mods && mods != Qt::ShiftModifier) {
619 QPlainTextEdit::keyPressEvent(e);
620 break;
623 QTextCursor::MoveMode mode =
624 mods & Qt::ShiftModifier ? QTextCursor::KeepAnchor : QTextCursor::MoveAnchor;
626 QTextCursor c(textCursor());
627 QTextBlock b(c.block());
629 int pos = indentedStartOfLine(b);
630 pos += b.position();
632 if (c.position() == pos)
633 c.movePosition(QTextCursor::StartOfLine, mode);
634 else
635 c.setPosition(pos, mode);
637 setTextCursor(c);
639 return;
642 case Qt::Key_Backtab:
644 QTextCursor cursor = textCursor();
645 cursor.insertText("\t");
646 ensureCursorVisible();
647 return;
650 default:
651 QPlainTextEdit::keyPressEvent(e);
654 switch (e->key()) {
655 case Qt::Key_Enter:
656 case Qt::Key_Return:
657 case Qt::Key_BraceRight:
658 case Qt::Key_BracketRight:
659 case Qt::Key_ParenRight:
660 indent();
661 break;
663 default:;
666 mAutoCompleter->keyPress(e);
669 void CodeEditor::mouseReleaseEvent ( QMouseEvent *e )
671 // Prevent deselection of bracket match:
672 if(!mMouseBracketMatch)
673 QPlainTextEdit::mouseReleaseEvent(e);
675 mMouseBracketMatch = false;
678 void CodeEditor::mouseDoubleClickEvent( QMouseEvent * e )
680 QTextCursor cursor = cursorForPosition(e->pos());
681 QTextCursor selection = blockAtCursor(cursor);
683 if (!selection.isNull()) {
684 setTextCursor(selection);
685 return;
688 QPlainTextEdit::mouseDoubleClickEvent(e);
691 void CodeEditor::mouseMoveEvent( QMouseEvent *e )
693 // Prevent initiating a text drag:
694 if(!mMouseBracketMatch)
695 QPlainTextEdit::mouseMoveEvent(e);
698 void CodeEditor::wheelEvent( QWheelEvent * e )
700 if (e->modifiers() == Qt::ControlModifier) {
701 if (e->delta() > 0)
702 zoomIn();
703 else
704 zoomOut();
705 return;
708 QPlainTextEdit::wheelEvent(e);
711 void CodeEditor::onOverlayChanged ( const QList<QRectF> & region )
713 foreach(QRectF r, region)
715 viewport()->update(r.toRect());
719 void CodeEditor::paintEvent( QPaintEvent *e )
721 QPlainTextEdit::paintEvent(e);
723 QPainter p(viewport());
724 mOverlay->render(&p, e->rect(), e->rect());
727 void CodeEditor::dragEnterEvent( QDragEnterEvent * event )
729 foreach (QUrl url, event->mimeData()->urls()) {
730 if (url.scheme() == QString("file")) { // LATER: use isLocalFile
731 // LATER: check mime type ?
732 event->acceptProposedAction();
733 return;
738 void CodeEditor::dropEvent( QDropEvent * event )
740 foreach (QUrl url, event->mimeData()->urls()) {
741 if (url.scheme() == QString("file")) // LATER: use isLocalFile
742 Main::documentManager()->open(url.toLocalFile());
746 void CodeEditor::updateLayout()
748 setViewportMargins( mLineIndicator->width(), 0, 0, 0 );
751 void CodeEditor::updateLineIndicator( QRect r, int dy )
753 if (dy)
754 mLineIndicator->scroll(0, dy);
755 else
756 mLineIndicator->update(0, r.y(), mLineIndicator->width(), r.height() );
759 void CodeEditor::matchBrackets()
761 mBracketSelections.clear();
763 QTextCursor cursor(textCursor());
764 QTextBlock block( cursor.block() );
765 int posInBlock = cursor.positionInBlock();
766 TokenIterator it(block);
767 while (it.isValid() && it.block() == block)
769 const Token & token = *it;
770 if (token.positionInBlock > posInBlock) {
771 it = TokenIterator();
772 break;
773 } else if (
774 (token.positionInBlock == posInBlock && token.type == Token::OpeningBracket) ||
775 (token.positionInBlock == posInBlock - 1 && token.type == Token::ClosingBracket)
777 break;
778 ++it;
781 BracketMatch match;
782 if( it.isValid() && it.block() == block)
783 matchBracket( it, match );
785 if( match.first.isValid() && match.second.isValid() )
787 const Token & tok1 = *match.first;
788 const Token & tok2 = *match.second;
790 if (
791 (tok1.character == '(' && tok2.character == ')')
792 || (tok1.character == '[' && tok2.character == ']')
793 || (tok1.character == '{' && tok2.character == '}')
795 QTextEdit::ExtraSelection selection;
796 selection.format = mBracketHighlight;
798 cursor.setPosition(match.first.position());
799 cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor);
800 selection.cursor = cursor;
801 mBracketSelections.append(selection);
803 cursor.setPosition(match.second.position());
804 cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor);
805 selection.cursor = cursor;
806 mBracketSelections.append(selection);
808 else {
809 QTextEdit::ExtraSelection selection;
810 selection.format.setBackground(Qt::red);
811 cursor.setPosition(match.first.position());
812 cursor.setPosition(match.second.position()+1, QTextCursor::KeepAnchor);
813 selection.cursor = cursor;
814 mBracketSelections.append(selection);
818 updateExtraSelections();
821 void CodeEditor::matchBracket( const TokenIterator & bracket , BracketMatch & match )
823 TokenIterator it(bracket);
825 if(it->type == Token::OpeningBracket)
827 match.first = it;
828 int level = 1;
829 while((++it).isValid())
831 Token::Type type = it->type;
832 if(type == Token::ClosingBracket)
833 --level;
834 else if(type == Token::OpeningBracket)
835 ++level;
836 if(level == 0) {
837 match.second = it;
838 return;
842 else if(it->type == Token::ClosingBracket)
844 match.second = it;
845 int level = 1;
846 while((--it).isValid())
848 Token::Type type = it->type;
849 if(type == Token::OpeningBracket)
850 --level;
851 else if(type == Token::ClosingBracket)
852 ++level;
853 if(level == 0) {
854 match.first = it;
855 return;
861 int CodeEditor::indentedStartOfLine( const QTextBlock &b )
863 QString t(b.text());
864 int n = t.size();
865 int i = 0;
866 while (i < n) {
867 QChar c(t[i]);
868 if (c != ' ' && c != '\t')
869 break;
870 ++i;
873 return i;
876 void CodeEditor::updateExtraSelections()
878 QList<QTextEdit::ExtraSelection> selections;
879 selections.append(mBracketSelections);
880 selections.append(mSearchSelections);
881 setExtraSelections(selections);
884 void CodeEditor::resizeEvent( QResizeEvent *e )
886 QPlainTextEdit::resizeEvent( e );
888 QRect cr = contentsRect();
889 mLineIndicator->resize( mLineIndicator->width(), cr.height() );
892 void CodeEditor::paintLineIndicator( QPaintEvent *e )
894 QPalette plt( mLineIndicator->palette() );
895 QRect r( e->rect() );
896 QPainter p( mLineIndicator );
898 p.fillRect( r, plt.color( QPalette::Button ) );
899 p.setPen( plt.color(QPalette::ButtonText) );
900 p.drawLine( r.topRight(), r.bottomRight() );
902 QTextDocument *doc = QPlainTextEdit::document();
903 QTextCursor cursor(textCursor());
904 int selStartBlock, selEndBlock;
905 if (cursor.hasSelection()) {
906 selStartBlock = doc->findBlock(cursor.selectionStart()).blockNumber();
907 selEndBlock = doc->findBlock(cursor.selectionEnd()).blockNumber();
909 else
910 selStartBlock = selEndBlock = -1;
912 QTextBlock block = firstVisibleBlock();
913 int blockNumber = block.blockNumber();
914 int top = (int) blockBoundingGeometry(block).translated(contentOffset()).top();
915 int bottom = top + (int) blockBoundingRect(block).height();
917 while (block.isValid() && top <= e->rect().bottom()) {
918 if (block.isVisible() && bottom >= e->rect().top()) {
919 p.save();
921 QRect numRect( 0, top, mLineIndicator->width() - 1, bottom - top );
923 int num = blockNumber;
924 if (num >= selStartBlock && num <= selEndBlock) {
925 num -= selStartBlock;
926 p.setPen(Qt::NoPen);
927 p.setBrush(plt.color(QPalette::Highlight));
928 p.drawRect(numRect);
929 p.setPen(plt.color(QPalette::HighlightedText));
932 QString number = QString::number(num + 1);
933 p.drawText(0, top, mLineIndicator->width() - 4, bottom - top,
934 Qt::AlignRight, number);
936 p.restore();
939 block = block.next();
940 top = bottom;
941 bottom = top + (int) blockBoundingRect(block).height();
942 ++blockNumber;
946 void CodeEditor::indent()
948 indent(textCursor());
951 void CodeEditor::indent( const QTextCursor & selection )
953 QTextCursor cursor(selection);
955 cursor.beginEditBlock();
957 QTextDocument *doc = QPlainTextEdit::document();
958 int startBlockNum = doc->findBlock(cursor.selectionStart()).blockNumber();
959 int endBlockNum = cursor.hasSelection() ?
960 doc->findBlock(cursor.selectionEnd()).blockNumber() : startBlockNum;
962 QStack<int> stack;
963 int level = 0;
964 int blockNum = 0;
965 QTextBlock block = QPlainTextEdit::document()->begin();
966 while (block.isValid())
968 if (level > 0) {
969 stack.push(level);
970 level = 0;
973 int initialStackSize = stack.size();
975 TextBlockData *data = static_cast<TextBlockData*>(block.userData());
976 if (data)
978 int count = data->tokens.size();
979 for (int idx = 0; idx < count; ++idx)
981 const Token & token = data->tokens[idx];
982 switch (token.type)
984 case Token::OpeningBracket:
985 if (token.character != '(' || stack.size() || token.positionInBlock)
986 level += 1;
987 break;
989 case Token::ClosingBracket:
990 if (level)
991 level -= 1;
992 else if(!stack.isEmpty()) {
993 stack.top() -= 1;
994 if (stack.top() <= 0)
995 stack.pop();
997 break;
999 default:
1005 if(blockNum >= startBlockNum) {
1006 int indentLevel;
1007 if (data && data->tokens.size() && data->tokens[0].type == Token::ClosingBracket)
1008 indentLevel = stack.size();
1009 else
1010 indentLevel = initialStackSize;
1011 block = indent(block, indentLevel);
1014 if(blockNum == endBlockNum)
1015 break;
1017 block = block.next();
1018 ++blockNum;
1021 cursor.endEditBlock();
1024 QString CodeEditor::makeIndentationString(int level)
1026 if (level <= 0)
1027 return QString();
1029 if ( mSpaceIndent ) {
1030 const int spaces = mDoc->indentWidth() * level;
1031 QString indentationString (spaces, QChar(' '));
1032 return indentationString;
1033 } else {
1034 const int tabs = level;
1035 QString indentationString (tabs, QChar('\t'));
1036 return indentationString;
1040 QTextBlock CodeEditor::indent( const QTextBlock & block, int level )
1042 QTextCursor cursor(block);
1043 cursor.movePosition(QTextCursor::StartOfBlock);
1044 cursor.setPosition(cursor.position() + indentedStartOfLine(block), QTextCursor::KeepAnchor);
1046 cursor.insertText(makeIndentationString(level));
1048 // modification has invalidated the block, so return a new one
1049 return cursor.block();
1052 int CodeEditor::indentationLevel(const QTextCursor & cursor)
1054 QTextDocument *doc = QPlainTextEdit::document();
1055 int startBlockNum = doc->findBlock(cursor.selectionStart()).blockNumber();
1057 QStack<int> stack;
1058 int level = 0;
1059 int blockNum = 0;
1060 QTextBlock block = QPlainTextEdit::document()->begin();
1061 while (block.isValid()) {
1062 if (level > 0) {
1063 stack.push(level);
1064 level = 0;
1067 TextBlockData *data = static_cast<TextBlockData*>(block.userData());
1068 if (data) {
1069 int count = data->tokens.size();
1070 for (int idx = 0; idx < count; ++idx) {
1071 const Token & token = data->tokens[idx];
1072 switch (token.type) {
1073 case Token::OpeningBracket:
1074 if (token.character != '(' || stack.size() || token.positionInBlock)
1075 level += 1;
1076 break;
1078 case Token::ClosingBracket:
1079 if (level)
1080 level -= 1;
1081 else if(!stack.isEmpty()) {
1082 stack.top() -= 1;
1083 if (stack.top() <= 0)
1084 stack.pop();
1086 break;
1088 default:
1094 if (blockNum == startBlockNum)
1095 return stack.size();
1097 block = block.next();
1098 ++blockNum;
1101 return -1;
1104 void CodeEditor::triggerAutoCompletion()
1106 mAutoCompleter->triggerCompletion();
1109 void CodeEditor::triggerMethodCallAid()
1111 mAutoCompleter->triggerMethodCallAid();
1114 static bool isSingleLineComment(QTextBlock const & block)
1116 static QRegExp commentRegex("^\\s*//.*");
1117 return commentRegex.exactMatch(block.text());
1120 static bool isSelectionComment(QString const & text)
1122 QString trimmed = text.trimmed();
1123 if ( trimmed.startsWith(QString("/*")) && trimmed.endsWith(QString("*/")) )
1124 return true;
1125 else
1126 return false;
1129 void CodeEditor::toggleComment()
1131 QTextCursor cursor = textCursor();
1133 if (cursor.hasSelection())
1134 toggleCommentSelection();
1135 else
1136 toggleCommentSingleLine();
1139 void CodeEditor::toggleCommentSingleLine()
1141 QTextCursor cursor = textCursor();
1142 cursor.beginEditBlock();
1144 toggleCommentSingleLine( cursor );
1146 cursor.endEditBlock();
1149 void CodeEditor::addSingleLineComment(QTextCursor cursor, int indentation)
1151 QTextBlock currentBlock(cursor.block());
1152 int blockIndentationLevel = indentationLevel(cursor);
1154 cursor.movePosition(QTextCursor::StartOfBlock);
1155 cursor.setPosition(cursor.position() + indentedStartOfLine(currentBlock), QTextCursor::KeepAnchor);
1157 QString commentString = makeIndentationString(indentation) + QString("// ")
1158 + makeIndentationString(blockIndentationLevel - indentation);
1160 cursor.insertText(commentString);
1162 cursor.movePosition(QTextCursor::StartOfBlock);
1165 void CodeEditor::removeSingleLineComment(QTextCursor cursor)
1167 QTextBlock currentBlock(cursor.block());
1168 cursor.movePosition(QTextCursor::StartOfBlock);
1169 cursor.setPosition(cursor.position() + indentedStartOfLine(currentBlock) + 2, QTextCursor::KeepAnchor);
1171 if (!cursor.selectedText().endsWith(QString("//")))
1172 cursor.setPosition(cursor.anchor() + indentedStartOfLine(currentBlock), QTextCursor::KeepAnchor);
1174 cursor.insertText("");
1177 void CodeEditor::toggleCommentSingleLine(QTextCursor cursor)
1179 QTextBlock currentBlock(cursor.block());
1181 cursor.beginEditBlock();
1183 if (!isSingleLineComment(currentBlock)) {
1184 int blockIndentation = indentationLevel(cursor);
1185 addSingleLineComment(cursor, blockIndentation);
1186 } else
1187 removeSingleLineComment(cursor);
1189 cursor.endEditBlock();
1190 indent(cursor);
1193 static bool isBlockOnlySelection(QTextCursor)
1195 return true;
1198 void CodeEditor::toggleCommentSelection()
1200 QTextCursor cursor = textCursor();
1201 cursor.beginEditBlock();
1203 if (isBlockOnlySelection(cursor)) {
1204 QTextCursor selectionCursor(cursor);
1205 selectionCursor.setPosition(cursor.selectionStart());
1207 QTextBlock currentBlock = selectionCursor.block();
1208 bool isComment = isSingleLineComment(currentBlock);
1209 int firstBlockIndentation = isComment ? 0
1210 : indentationLevel(selectionCursor);
1212 do {
1213 QTextCursor blockCursor(currentBlock);
1214 if (!isComment)
1215 addSingleLineComment(blockCursor, firstBlockIndentation);
1216 else
1217 removeSingleLineComment(blockCursor);
1218 currentBlock = currentBlock.next();
1219 } while (currentBlock.isValid() && currentBlock.position() < cursor.selectionEnd());
1220 } else {
1221 QString selectionText = cursor.selectedText();
1222 QTextCursor selectionCursor(cursor);
1224 if (isSelectionComment(selectionText)) {
1225 selectionText = selectionText.trimmed().remove(2);
1226 selectionText.chop(2);
1227 selectionCursor.insertText(selectionText);
1228 } else {
1229 selectionText = QString("/* ") + selectionText + QString(" */");
1230 selectionCursor.insertText(selectionText);
1234 cursor.endEditBlock();
1235 cursor.beginEditBlock();
1236 indent(currentRegion());
1237 cursor.endEditBlock();
1240 void CodeEditor::copyUpDown(bool up)
1242 // directly taken from qtcreator
1243 // Copyright (c) 2012 Nokia Corporation and/or its subsidiary(-ies).
1244 // GNU Lesser General Public License
1245 QTextCursor cursor = textCursor();
1246 QTextCursor move = cursor;
1247 move.beginEditBlock();
1249 bool hasSelection = cursor.hasSelection();
1251 if (hasSelection) {
1252 move.setPosition(cursor.selectionStart());
1253 move.movePosition(QTextCursor::StartOfBlock);
1254 move.setPosition(cursor.selectionEnd(), QTextCursor::KeepAnchor);
1255 move.movePosition(move.atBlockStart() ? QTextCursor::Left: QTextCursor::EndOfBlock,
1256 QTextCursor::KeepAnchor);
1257 } else {
1258 move.movePosition(QTextCursor::StartOfBlock);
1259 move.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
1262 QString text = move.selectedText();
1264 if (up) {
1265 move.setPosition(cursor.selectionStart());
1266 move.movePosition(QTextCursor::StartOfBlock);
1267 move.insertBlock();
1268 move.movePosition(QTextCursor::Left);
1269 } else {
1270 move.movePosition(QTextCursor::EndOfBlock);
1271 if (move.atBlockStart()) {
1272 move.movePosition(QTextCursor::NextBlock);
1273 move.insertBlock();
1274 move.movePosition(QTextCursor::Left);
1275 } else {
1276 move.insertBlock();
1280 int start = move.position();
1281 move.clearSelection();
1282 move.insertText(text);
1283 int end = move.position();
1285 move.setPosition(start);
1286 move.setPosition(end, QTextCursor::KeepAnchor);
1288 move.endEditBlock();
1289 indent(currentRegion());
1291 setTextCursor(move);
1295 void CodeEditor::toggleOverwriteMode()
1297 setOverwriteMode(!overwriteMode());
1301 void CodeEditor::copyLineDown()
1303 copyUpDown(false);
1306 void CodeEditor::copyLineUp()
1308 copyUpDown(true);
1311 void CodeEditor::moveLineUpDown(bool up)
1313 // directly taken from qtcreator
1314 // Copyright (c) 2012 Nokia Corporation and/or its subsidiary(-ies).
1315 // GNU Lesser General Public License
1316 QTextCursor cursor = textCursor();
1317 QTextCursor move = cursor;
1319 move.setVisualNavigation(false); // this opens folded items instead of destroying them
1321 move.beginEditBlock();
1323 bool hasSelection = cursor.hasSelection();
1325 if (cursor.hasSelection()) {
1326 move.setPosition(cursor.selectionStart());
1327 move.movePosition(QTextCursor::StartOfBlock);
1328 move.setPosition(cursor.selectionEnd(), QTextCursor::KeepAnchor);
1329 move.movePosition(move.atBlockStart() ? QTextCursor::Left: QTextCursor::EndOfBlock,
1330 QTextCursor::KeepAnchor);
1331 } else {
1332 move.movePosition(QTextCursor::StartOfBlock);
1333 move.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
1335 QString text = move.selectedText();
1337 move.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor);
1338 move.removeSelectedText();
1340 if (up) {
1341 move.movePosition(QTextCursor::PreviousBlock);
1342 move.insertBlock();
1343 move.movePosition(QTextCursor::Left);
1344 } else {
1345 move.movePosition(QTextCursor::EndOfBlock);
1346 if (move.atBlockStart()) { // empty block
1347 move.movePosition(QTextCursor::NextBlock);
1348 move.insertBlock();
1349 move.movePosition(QTextCursor::Left);
1350 } else {
1351 move.insertBlock();
1355 int start = move.position();
1356 move.clearSelection();
1357 move.insertText(text);
1358 int end = move.position();
1360 if (hasSelection) {
1361 move.setPosition(start);
1362 move.setPosition(end, QTextCursor::KeepAnchor);
1365 move.endEditBlock();
1366 indent(currentRegion());
1368 setTextCursor(move);
1371 void CodeEditor::moveLineUp()
1373 moveLineUpDown(true);
1376 void CodeEditor::moveLineDown()
1378 moveLineUpDown(false);
1381 // taking nested brackets into account
1382 static TokenIterator previousOpeningBracket(TokenIterator it)
1384 int level = 0;
1385 while (it.isValid()) {
1386 switch (it->type) {
1387 case Token::OpeningBracket:
1388 if (level == 0)
1389 return it;
1390 --level;
1391 break;
1393 case Token::ClosingBracket:
1394 ++level;
1396 default:
1397 break;
1399 --it;
1401 return it;
1404 // taking nested brackets into account
1405 static TokenIterator nextClosingBracket(TokenIterator it)
1407 int level = 0;
1408 while (it.isValid()) {
1409 switch (it->type) {
1410 case Token::ClosingBracket:
1411 if (level == 0)
1412 return it;
1413 --level;
1414 break;
1416 case Token::OpeningBracket:
1417 ++level;
1419 default:
1420 break;
1422 ++it;
1424 return it;
1428 void CodeEditor::gotoNextBlock()
1430 QTextCursor cursor = textCursor();
1432 TokenIterator startIt (cursor.block(), cursor.positionInBlock());
1433 TokenIterator previousBracket;
1435 if (startIt.type() != Token::OpeningBracket) {
1436 startIt = TokenIterator::leftOf(cursor.block(), cursor.positionInBlock());
1437 previousBracket = previousOpeningBracket(startIt);
1438 } else
1439 previousBracket = startIt;
1441 if (previousBracket.isValid()) {
1442 TokenIterator nextBracket = nextClosingBracket(previousBracket.next());
1444 if (nextBracket.isValid()) {
1445 setTextCursor(cursorAt(nextBracket, 1));
1446 return;
1450 gotoNextRegion();
1453 void CodeEditor::gotoPreviousBlock()
1455 QTextCursor cursor = textCursor();
1457 TokenIterator startIt;
1458 if (cursor.positionInBlock())
1459 startIt = TokenIterator(cursor.block(), cursor.positionInBlock() - 1);
1460 else {
1461 QTextBlock previousBlock = cursor.block().previous();
1462 startIt = TokenIterator(previousBlock, previousBlock.length() - 1);
1465 TokenIterator nextBracket;
1467 if (startIt.type() != Token::ClosingBracket) {
1468 startIt = TokenIterator::rightOf(cursor.block(), cursor.positionInBlock());
1469 nextBracket = nextClosingBracket(startIt);
1470 } else
1471 nextBracket = startIt;
1473 if (nextBracket.isValid()) {
1474 TokenIterator previousBracket = previousOpeningBracket(nextBracket.previous());
1476 if (previousBracket.isValid()) {
1477 setTextCursor(cursorAt(previousBracket));
1478 return;
1482 gotoPreviousRegion();
1485 void CodeEditor::gotoPreviousEmptyLine()
1487 gotoEmptyLineUpDown(true);
1490 void CodeEditor::gotoNextEmptyLine()
1492 gotoEmptyLineUpDown(false);
1495 void CodeEditor::gotoEmptyLineUpDown(bool up)
1497 static const QRegExp whiteSpaceLine("^\\s*$");
1499 const QTextCursor::MoveOperation direction = up ? QTextCursor::PreviousBlock
1500 : QTextCursor::NextBlock;
1502 QTextCursor cursor = textCursor();
1503 cursor.beginEditBlock();
1505 bool cursorMoved = false;
1507 // find first non-whitespace line
1508 while ( cursor.movePosition(direction) ) {
1509 if ( !whiteSpaceLine.exactMatch(cursor.block().text()) )
1510 break;
1513 // find first whitespace line
1514 while ( cursor.movePosition(direction) ) {
1515 if ( whiteSpaceLine.exactMatch(cursor.block().text()) ) {
1516 setTextCursor(cursor);
1517 cursorMoved = true;
1518 break;
1522 if (!cursorMoved) {
1523 const QTextCursor::MoveOperation startOrEnd = up ? QTextCursor::Start
1524 : QTextCursor::End;
1526 cursor.movePosition(startOrEnd);
1527 setTextCursor(cursor);
1530 cursor.endEditBlock();
1534 void CodeEditor::selectCurrentRegion()
1536 QTextCursor selectedRegionCursor = currentRegion();
1537 if (!selectedRegionCursor.isNull() && selectedRegionCursor.hasSelection())
1538 setTextCursor(selectedRegionCursor);
1541 void CodeEditor::gotoNextRegion()
1543 QTextCursor cursor = textCursor();
1544 cursor.movePosition(QTextCursor::NextCharacter);
1546 QTextCursor regionCursor = regionAtCursor(cursor);
1548 QTextCursor cursorBehindRegion;
1549 if (!regionCursor.isNull() && regionCursor.hasSelection()) {
1550 cursorBehindRegion = regionCursor;
1551 cursorBehindRegion.movePosition(QTextCursor::NextCharacter);
1552 } else {
1553 cursorBehindRegion = textCursor();
1554 cursorBehindRegion.movePosition(QTextCursor::NextCharacter);
1557 TokenIterator it = TokenIterator(cursorBehindRegion.block(), cursorBehindRegion.positionInBlock());
1558 if (!it.isValid() || it->type == Token::OpeningBracket)
1559 it = TokenIterator::rightOf(cursorBehindRegion.block(), cursorBehindRegion.positionInBlock());
1561 while (it.isValid()) {
1562 if ( (it->type == Token::OpeningBracket) && (it->character == '(') &&
1563 (it->positionInBlock == 0) ) {
1564 setTextCursor( cursorAt(it) );
1565 return;
1567 ++it;
1570 cursor = textCursor();
1571 cursor.movePosition(QTextCursor::End);
1572 setTextCursor(cursor);
1575 void CodeEditor::gotoPreviousRegion()
1577 QTextCursor cursor = textCursor();
1579 QTextCursor regionCursor = regionAtCursor(cursor);
1581 QTextCursor cursorBeforeRegion;
1582 if (!regionCursor.isNull() && regionCursor.hasSelection()) {
1583 cursorBeforeRegion = regionCursor;
1584 cursorBeforeRegion.setPosition(cursorBeforeRegion.anchor());
1585 cursorBeforeRegion.movePosition(QTextCursor::PreviousCharacter);
1586 } else {
1587 cursorBeforeRegion = textCursor();
1588 cursorBeforeRegion.movePosition(QTextCursor::PreviousCharacter);
1591 TokenIterator it = TokenIterator(cursorBeforeRegion.block(), cursorBeforeRegion.positionInBlock());
1592 if (!it.isValid() || it->type == Token::ClosingBracket)
1593 it = TokenIterator::leftOf(cursorBeforeRegion.block(), cursorBeforeRegion.positionInBlock());
1595 while (it.isValid()) {
1596 if ( (it->type == Token::ClosingBracket) && (it->character == ')') &&
1597 (it->positionInBlock == 0) ) {
1598 setTextCursor( cursorAt(it) );
1599 return;
1601 --it;
1604 cursor = textCursor();
1605 cursor.movePosition(QTextCursor::Start);
1606 setTextCursor(cursor);
1609 bool CodeEditor::openDocumentation()
1611 return Main::openDocumentation(symbolUnderCursor());
1614 void CodeEditor::openDefinition()
1616 Main::openDefinition(symbolUnderCursor(), this);
1619 void CodeEditor::lookupReferences()
1621 QString symbol = symbolUnderCursor();
1622 if (symbol.size() != 0)
1623 new SymbolReferenceRequest(symbol, Main::scProcess(), this);
1626 void CodeEditor::hideMouseCursor()
1628 QCursor * overrideCursor = QApplication::overrideCursor();
1629 if (!overrideCursor || overrideCursor->shape() != Qt::BlankCursor)
1630 QApplication::setOverrideCursor( Qt::BlankCursor );
1633 QTextCursor CodeEditor::cursorAt(const TokenIterator it, int offset)
1635 Q_ASSERT(it.isValid());
1637 QTextCursor textCursor(textDocument());
1638 textCursor.setPosition(it.position() + offset);
1640 return textCursor;
1643 } // namespace ScIDE