1 /*
2   This file is part of the Ofi Labs X2 project.
3 
4   Copyright (C) 2011 Ariya Hidayat <ariya.hidayat@gmail.com>
5   Copyright (C) 2010 Ariya Hidayat <ariya.hidayat@gmail.com>
6 
7   Redistribution and use in source and binary forms, with or without
8   modification, are permitted provided that the following conditions are met:
9 
10     * Redistributions of source code must retain the above copyright
11       notice, this list of conditions and the following disclaimer.
12     * Redistributions in binary form must reproduce the above copyright
13       notice, this list of conditions and the following disclaimer in the
14       documentation and/or other materials provided with the distribution.
15     * Neither the name of the <organization> nor the
16       names of its contributors may be used to endorse or promote products
17       derived from this software without specific prior written permission.
18 
19   THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20   AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21   IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22   ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
23   DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
24   (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25   LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
26   ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27   (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
28   THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 */
30 
31 #include "jsedit.h"
32 
33 #include <QtGui>
34 
35 class JSBlockData: public QTextBlockUserData
36 {
37 public:
38     QList<int> bracketPositions;
39 };
40 
41 class JSHighlighter : public QSyntaxHighlighter
42 {
43 public:
44     JSHighlighter(QTextDocument *parent = 0);
45     void setColor(JSEdit::ColorComponent component, const QColor &color);
46     void mark(const QString &str, Qt::CaseSensitivity caseSensitivity);
47 
48     QStringList keywords() const;
49     void setKeywords(const QStringList &keywords);
50 
51 protected:
52     void highlightBlock(const QString &text);
53 
54 private:
55     QSet<QString> m_keywords;
56     QSet<QString> m_knownIds;
57     QHash<JSEdit::ColorComponent, QColor> m_colors;
58     QString m_markString;
59     Qt::CaseSensitivity m_markCaseSensitivity;
60 };
61 
JSHighlighter(QTextDocument * parent)62 JSHighlighter::JSHighlighter(QTextDocument *parent)
63     : QSyntaxHighlighter(parent)
64     , m_markCaseSensitivity(Qt::CaseInsensitive)
65 {
66     // default color scheme
67     m_colors[JSEdit::Normal]     = QColor("#000000");
68     m_colors[JSEdit::Comment]    = QColor("#808080");
69     m_colors[JSEdit::Number]     = QColor("#008000");
70     m_colors[JSEdit::String]     = QColor("#800000");
71     m_colors[JSEdit::Operator]   = QColor("#808000");
72     m_colors[JSEdit::Identifier] = QColor("#000020");
73     m_colors[JSEdit::Keyword]    = QColor("#000080");
74     m_colors[JSEdit::BuiltIn]    = QColor("#008080");
75     m_colors[JSEdit::Marker]     = QColor("#ffff00");
76 
77     // https://developer.mozilla.org/en/JavaScript/Reference/Reserved_Words
78     m_keywords << "break";
79     m_keywords << "case";
80     m_keywords << "catch";
81     m_keywords << "continue";
82     m_keywords << "default";
83     m_keywords << "delete";
84     m_keywords << "do";
85     m_keywords << "else";
86     m_keywords << "finally";
87     m_keywords << "for";
88     m_keywords << "function";
89     m_keywords << "if";
90     m_keywords << "in";
91     m_keywords << "instanceof";
92     m_keywords << "new";
93     m_keywords << "return";
94     m_keywords << "switch";
95     m_keywords << "this";
96     m_keywords << "throw";
97     m_keywords << "try";
98     m_keywords << "typeof";
99     m_keywords << "var";
100     m_keywords << "void";
101     m_keywords << "while";
102     m_keywords << "with";
103 
104     m_keywords << "true";
105     m_keywords << "false";
106     m_keywords << "null";
107 
108     // built-in and other popular objects + properties
109     m_knownIds << "Object";
110     m_knownIds << "prototype";
111     m_knownIds << "create";
112     m_knownIds << "defineProperty";
113     m_knownIds << "defineProperties";
114     m_knownIds << "getOwnPropertyDescriptor";
115     m_knownIds << "keys";
116     m_knownIds << "getOwnPropertyNames";
117     m_knownIds << "constructor";
118     m_knownIds << "__parent__";
119     m_knownIds << "__proto__";
120     m_knownIds << "__defineGetter__";
121     m_knownIds << "__defineSetter__";
122     m_knownIds << "eval";
123     m_knownIds << "hasOwnProperty";
124     m_knownIds << "isPrototypeOf";
125     m_knownIds << "__lookupGetter__";
126     m_knownIds << "__lookupSetter__";
127     m_knownIds << "__noSuchMethod__";
128     m_knownIds << "propertyIsEnumerable";
129     m_knownIds << "toSource";
130     m_knownIds << "toLocaleString";
131     m_knownIds << "toString";
132     m_knownIds << "unwatch";
133     m_knownIds << "valueOf";
134     m_knownIds << "watch";
135 
136     m_knownIds << "Function";
137     m_knownIds << "arguments";
138     m_knownIds << "arity";
139     m_knownIds << "caller";
140     m_knownIds << "constructor";
141     m_knownIds << "length";
142     m_knownIds << "name";
143     m_knownIds << "apply";
144     m_knownIds << "bind";
145     m_knownIds << "call";
146 
147     m_knownIds << "String";
148     m_knownIds << "fromCharCode";
149     m_knownIds << "length";
150     m_knownIds << "charAt";
151     m_knownIds << "charCodeAt";
152     m_knownIds << "concat";
153     m_knownIds << "indexOf";
154     m_knownIds << "lastIndexOf";
155     m_knownIds << "localCompare";
156     m_knownIds << "match";
157     m_knownIds << "quote";
158     m_knownIds << "replace";
159     m_knownIds << "search";
160     m_knownIds << "slice";
161     m_knownIds << "split";
162     m_knownIds << "substr";
163     m_knownIds << "substring";
164     m_knownIds << "toLocaleLowerCase";
165     m_knownIds << "toLocaleUpperCase";
166     m_knownIds << "toLowerCase";
167     m_knownIds << "toUpperCase";
168     m_knownIds << "trim";
169     m_knownIds << "trimLeft";
170     m_knownIds << "trimRight";
171 
172     m_knownIds << "Array";
173     m_knownIds << "isArray";
174     m_knownIds << "index";
175     m_knownIds << "input";
176     m_knownIds << "pop";
177     m_knownIds << "push";
178     m_knownIds << "reverse";
179     m_knownIds << "shift";
180     m_knownIds << "sort";
181     m_knownIds << "splice";
182     m_knownIds << "unshift";
183     m_knownIds << "concat";
184     m_knownIds << "join";
185     m_knownIds << "filter";
186     m_knownIds << "forEach";
187     m_knownIds << "every";
188     m_knownIds << "map";
189     m_knownIds << "some";
190     m_knownIds << "reduce";
191     m_knownIds << "reduceRight";
192 
193     m_knownIds << "RegExp";
194     m_knownIds << "global";
195     m_knownIds << "ignoreCase";
196     m_knownIds << "lastIndex";
197     m_knownIds << "multiline";
198     m_knownIds << "source";
199     m_knownIds << "exec";
200     m_knownIds << "test";
201 
202     m_knownIds << "JSON";
203     m_knownIds << "parse";
204     m_knownIds << "stringify";
205 
206     m_knownIds << "decodeURI";
207     m_knownIds << "decodeURIComponent";
208     m_knownIds << "encodeURI";
209     m_knownIds << "encodeURIComponent";
210     m_knownIds << "eval";
211     m_knownIds << "isFinite";
212     m_knownIds << "isNaN";
213     m_knownIds << "parseFloat";
214     m_knownIds << "parseInt";
215     m_knownIds << "Infinity";
216     m_knownIds << "NaN";
217     m_knownIds << "undefined";
218 
219     m_knownIds << "Math";
220     m_knownIds << "E";
221     m_knownIds << "LN2";
222     m_knownIds << "LN10";
223     m_knownIds << "LOG2E";
224     m_knownIds << "LOG10E";
225     m_knownIds << "PI";
226     m_knownIds << "SQRT1_2";
227     m_knownIds << "SQRT2";
228     m_knownIds << "abs";
229     m_knownIds << "acos";
230     m_knownIds << "asin";
231     m_knownIds << "atan";
232     m_knownIds << "atan2";
233     m_knownIds << "ceil";
234     m_knownIds << "cos";
235     m_knownIds << "exp";
236     m_knownIds << "floor";
237     m_knownIds << "log";
238     m_knownIds << "max";
239     m_knownIds << "min";
240     m_knownIds << "pow";
241     m_knownIds << "random";
242     m_knownIds << "round";
243     m_knownIds << "sin";
244     m_knownIds << "sqrt";
245     m_knownIds << "tan";
246 
247     m_knownIds << "document";
248     m_knownIds << "window";
249     m_knownIds << "navigator";
250     m_knownIds << "userAgent";
251 }
252 
setColor(JSEdit::ColorComponent component,const QColor & color)253 void JSHighlighter::setColor(JSEdit::ColorComponent component, const QColor &color)
254 {
255     m_colors[component] = color;
256     rehighlight();
257 }
258 
highlightBlock(const QString & text)259 void JSHighlighter::highlightBlock(const QString &text)
260 {
261     // parsing state
262     enum {
263         Start = 0,
264         Number = 1,
265         Identifier = 2,
266         String = 3,
267         Comment = 4,
268         Regex = 5
269     };
270 
271     QList<int> bracketPositions;
272 
273     int blockState = previousBlockState();
274     int bracketLevel = blockState >> 4;
275     int state = blockState & 15;
276     if (blockState < 0) {
277         bracketLevel = 0;
278         state = Start;
279     }
280 
281     int start = 0;
282     int i = 0;
283     while (i <= text.length()) {
284         QChar ch = (i < text.length()) ? text.at(i) : QChar();
285         QChar next = (i < text.length() - 1) ? text.at(i + 1) : QChar();
286 
287         switch (state) {
288 
289         case Start:
290             start = i;
291             if (ch.isSpace()) {
292                 ++i;
293             } else if (ch.isDigit()) {
294                 ++i;
295                 state = Number;
296             } else if (ch.isLetter() || ch == '_') {
297                 ++i;
298                 state = Identifier;
299             } else if (ch == '\'' || ch == '\"') {
300                 ++i;
301                 state = String;
302             } else if (ch == '/' && next == '*') {
303                 ++i;
304                 ++i;
305                 state = Comment;
306             } else if (ch == '/' && next == '/') {
307                 i = text.length();
308                 setFormat(start, text.length(), m_colors[JSEdit::Comment]);
309             } else if (ch == '/' && next != '*') {
310                 ++i;
311                 state = Regex;
312             } else {
313                 if (!QString("(){}[]").contains(ch))
314                     setFormat(start, 1, m_colors[JSEdit::Operator]);
315                 if (ch =='{' || ch == '}') {
316                     bracketPositions += i;
317                     if (ch == '{')
318                         bracketLevel++;
319                     else
320                         bracketLevel--;
321                 }
322                 ++i;
323                 state = Start;
324             }
325             break;
326 
327         case Number:
328             if (ch.isSpace() || !ch.isDigit()) {
329                 setFormat(start, i - start, m_colors[JSEdit::Number]);
330                 state = Start;
331             } else {
332                 ++i;
333             }
334             break;
335 
336         case Identifier:
337             if (ch.isSpace() || !(ch.isDigit() || ch.isLetter() || ch == '_')) {
338                 QString token = text.mid(start, i - start).trimmed();
339                 if (m_keywords.contains(token))
340                     setFormat(start, i - start, m_colors[JSEdit::Keyword]);
341                 else if (m_knownIds.contains(token))
342                     setFormat(start, i - start, m_colors[JSEdit::BuiltIn]);
343                 state = Start;
344             } else {
345                 ++i;
346             }
347             break;
348 
349         case String:
350             if (ch == text.at(start)) {
351                 QChar prev = (i > 0) ? text.at(i - 1) : QChar();
352                 if (prev != '\\') {
353                     ++i;
354                     setFormat(start, i - start, m_colors[JSEdit::String]);
355                     state = Start;
356                 } else {
357                     ++i;
358                 }
359             } else {
360                 ++i;
361             }
362             break;
363 
364         case Comment:
365             if (ch == '*' && next == '/') {
366                 ++i;
367                 ++i;
368                 setFormat(start, i - start, m_colors[JSEdit::Comment]);
369                 state = Start;
370             } else {
371                 ++i;
372             }
373             break;
374 
375         case Regex:
376             if (ch == '/') {
377                 QChar prev = (i > 0) ? text.at(i - 1) : QChar();
378                 if (prev != '\\') {
379                     ++i;
380                     setFormat(start, i - start, m_colors[JSEdit::String]);
381                     state = Start;
382                 } else {
383                     ++i;
384                 }
385             } else {
386                 ++i;
387             }
388             break;
389 
390         default:
391             state = Start;
392             break;
393         }
394     }
395 
396     if (state == Comment)
397         setFormat(start, text.length(), m_colors[JSEdit::Comment]);
398     else
399         state = Start;
400 
401     if (!m_markString.isEmpty()) {
402         int pos = 0;
403         int len = m_markString.length();
404         QTextCharFormat markerFormat;
405         markerFormat.setBackground(m_colors[JSEdit::Marker]);
406         markerFormat.setForeground(m_colors[JSEdit::Normal]);
407         for (;;) {
408             pos = text.indexOf(m_markString, pos, m_markCaseSensitivity);
409             if (pos < 0)
410                 break;
411             setFormat(pos, len, markerFormat);
412             ++pos;
413         }
414     }
415 
416     if (!bracketPositions.isEmpty()) {
417         JSBlockData *blockData = reinterpret_cast<JSBlockData*>(currentBlock().userData());
418         if (!blockData) {
419             blockData = new JSBlockData;
420             currentBlock().setUserData(blockData);
421         }
422         blockData->bracketPositions = bracketPositions;
423     }
424 
425     blockState = (state & 15) | (bracketLevel << 4);
426     setCurrentBlockState(blockState);
427 }
428 
mark(const QString & str,Qt::CaseSensitivity caseSensitivity)429 void JSHighlighter::mark(const QString &str, Qt::CaseSensitivity caseSensitivity)
430 {
431     m_markString = str;
432     m_markCaseSensitivity = caseSensitivity;
433     rehighlight();
434 }
435 
keywords() const436 QStringList JSHighlighter::keywords() const
437 {
438     return m_keywords.toList();
439 }
440 
setKeywords(const QStringList & keywords)441 void JSHighlighter::setKeywords(const QStringList &keywords)
442 {
443     m_keywords = QSet<QString>::fromList(keywords);
444     rehighlight();
445 }
446 
447 struct BlockInfo {
448     int position;
449     int number;
450     bool foldable: 1;
451     bool folded : 1;
452 };
453 
454 Q_DECLARE_TYPEINFO(BlockInfo, Q_PRIMITIVE_TYPE);
455 
456 class SidebarWidget : public QWidget
457 {
458 public:
459     SidebarWidget(JSEdit *editor);
460     QVector<BlockInfo> lineNumbers;
461     QColor backgroundColor;
462     QColor lineNumberColor;
463     QColor indicatorColor;
464     QColor foldIndicatorColor;
465     QFont font;
466     int foldIndicatorWidth;
467     QPixmap rightArrowIcon;
468     QPixmap downArrowIcon;
469 protected:
470     void mousePressEvent(QMouseEvent *event);
471     void paintEvent(QPaintEvent *event);
472 };
473 
SidebarWidget(JSEdit * editor)474 SidebarWidget::SidebarWidget(JSEdit *editor)
475     : QWidget(editor)
476     , foldIndicatorWidth(0)
477 {
478     backgroundColor = Qt::lightGray;
479     lineNumberColor = Qt::black;
480     indicatorColor = Qt::white;
481     foldIndicatorColor = Qt::lightGray;
482 }
483 
mousePressEvent(QMouseEvent * event)484 void SidebarWidget::mousePressEvent(QMouseEvent *event)
485 {
486     if (foldIndicatorWidth > 0) {
487         int xofs = width() - foldIndicatorWidth;
488         int lineNo = -1;
489         int fh = fontMetrics().lineSpacing();
490         int ys = event->pos().y();
491         if (event->pos().x() > xofs) {
492             foreach (BlockInfo ln, lineNumbers)
493                 if (ln.position < ys && (ln.position + fh) > ys) {
494                     if (ln.foldable)
495                         lineNo = ln.number;
496                     break;
497                 }
498         }
499         if (lineNo >= 0) {
500             JSEdit *editor = qobject_cast<JSEdit*>(parent());
501             if (editor)
502                 editor->toggleFold(lineNo);
503         }
504     }
505 }
506 
paintEvent(QPaintEvent * event)507 void SidebarWidget::paintEvent(QPaintEvent *event)
508 {
509     QPainter p(this);
510     p.fillRect(event->rect(), backgroundColor);
511     p.setPen(lineNumberColor);
512     p.setFont(font);
513     int fh = QFontMetrics(font).height();
514     foreach (BlockInfo ln, lineNumbers)
515         p.drawText(0, ln.position, width() - 4 - foldIndicatorWidth, fh, Qt::AlignRight, QString::number(ln.number));
516 
517     if (foldIndicatorWidth > 0) {
518         int xofs = width() - foldIndicatorWidth;
519         p.fillRect(xofs, 0, foldIndicatorWidth, height(), indicatorColor);
520 
521         // initialize (or recreate) the arrow icons whenever necessary
522         if (foldIndicatorWidth != rightArrowIcon.width()) {
523             QPainter iconPainter;
524             QPolygonF polygon;
525 
526             int dim = foldIndicatorWidth;
527             rightArrowIcon = QPixmap(dim, dim);
528             rightArrowIcon.fill(Qt::transparent);
529             downArrowIcon = rightArrowIcon;
530 
531             polygon << QPointF(dim * 0.4, dim * 0.25);
532             polygon << QPointF(dim * 0.4, dim * 0.75);
533             polygon << QPointF(dim * 0.8, dim * 0.5);
534             iconPainter.begin(&rightArrowIcon);
535             iconPainter.setRenderHint(QPainter::Antialiasing);
536             iconPainter.setPen(Qt::NoPen);
537             iconPainter.setBrush(foldIndicatorColor);
538             iconPainter.drawPolygon(polygon);
539             iconPainter.end();
540 
541             polygon.clear();
542             polygon << QPointF(dim * 0.25, dim * 0.4);
543             polygon << QPointF(dim * 0.75, dim * 0.4);
544             polygon << QPointF(dim * 0.5, dim * 0.8);
545             iconPainter.begin(&downArrowIcon);
546             iconPainter.setRenderHint(QPainter::Antialiasing);
547             iconPainter.setPen(Qt::NoPen);
548             iconPainter.setBrush(foldIndicatorColor);
549             iconPainter.drawPolygon(polygon);
550             iconPainter.end();
551         }
552 
553         foreach (BlockInfo ln, lineNumbers)
554             if (ln.foldable) {
555                 if (ln.folded)
556                     p.drawPixmap(xofs, ln.position, rightArrowIcon);
557                 else
558                     p.drawPixmap(xofs, ln.position, downArrowIcon);
559             }
560     }
561 }
562 
findClosingMatch(const QTextDocument * doc,int cursorPosition)563 static int findClosingMatch(const QTextDocument *doc, int cursorPosition)
564 {
565     QTextBlock block = doc->findBlock(cursorPosition);
566     JSBlockData *blockData = reinterpret_cast<JSBlockData*>(block.userData());
567     if (!blockData->bracketPositions.isEmpty()) {
568         int depth = 1;
569         while (block.isValid()) {
570             blockData = reinterpret_cast<JSBlockData*>(block.userData());
571             if (blockData && !blockData->bracketPositions.isEmpty()) {
572                 for (int c = 0; c < blockData->bracketPositions.count(); ++c) {
573                     int absPos = block.position() + blockData->bracketPositions.at(c);
574                     if (absPos <= cursorPosition)
575                         continue;
576                     if (doc->characterAt(absPos) == '{')
577                         depth++;
578                     else
579                         depth--;
580                     if (depth == 0)
581                         return absPos;
582                 }
583             }
584             block = block.next();
585         }
586     }
587     return -1;
588 }
589 
findOpeningMatch(const QTextDocument * doc,int cursorPosition)590 static int findOpeningMatch(const QTextDocument *doc, int cursorPosition)
591 {
592     QTextBlock block = doc->findBlock(cursorPosition);
593     JSBlockData *blockData = reinterpret_cast<JSBlockData*>(block.userData());
594     if (!blockData->bracketPositions.isEmpty()) {
595         int depth = 1;
596         while (block.isValid()) {
597             blockData = reinterpret_cast<JSBlockData*>(block.userData());
598             if (blockData && !blockData->bracketPositions.isEmpty()) {
599                 for (int c = blockData->bracketPositions.count() - 1; c >= 0; --c) {
600                     int absPos = block.position() + blockData->bracketPositions.at(c);
601                     if (absPos >= cursorPosition - 1)
602                         continue;
603                     if (doc->characterAt(absPos) == '}')
604                         depth++;
605                     else
606                         depth--;
607                     if (depth == 0)
608                         return absPos;
609                 }
610             }
611             block = block.previous();
612         }
613     }
614     return -1;
615 }
616 
617 class JSDocLayout: public QPlainTextDocumentLayout
618 {
619 public:
620     JSDocLayout(QTextDocument *doc);
621     void forceUpdate();
622 };
623 
JSDocLayout(QTextDocument * doc)624 JSDocLayout::JSDocLayout(QTextDocument *doc)
625     : QPlainTextDocumentLayout(doc)
626 {
627 }
628 
forceUpdate()629 void JSDocLayout::forceUpdate()
630 {
631     emit documentSizeChanged(documentSize());
632 }
633 
634 class JSEditPrivate
635 {
636 public:
637     JSEdit *editor;
638     JSDocLayout *layout;
639     JSHighlighter *highlighter;
640     SidebarWidget *sidebar;
641     bool showLineNumbers;
642     bool textWrap;
643     QColor cursorColor;
644     bool bracketsMatching;
645     QList<int> matchPositions;
646     QColor bracketMatchColor;
647     QList<int> errorPositions;
648     QColor bracketErrorColor;
649     bool codeFolding : 1;
650 };
651 
JSEdit(QWidget * parent)652 JSEdit::JSEdit(QWidget *parent)
653     : QPlainTextEdit(parent)
654     , d_ptr(new JSEditPrivate)
655 {
656     d_ptr->editor = this;
657     d_ptr->layout = new JSDocLayout(document());
658     d_ptr->highlighter = new JSHighlighter(document());
659     d_ptr->sidebar = new SidebarWidget(this);
660     d_ptr->showLineNumbers = true;
661     d_ptr->textWrap = true;
662     d_ptr->bracketsMatching = true;
663     d_ptr->cursorColor = QColor(255, 255, 192);
664     d_ptr->bracketMatchColor = QColor(180, 238, 180);
665     d_ptr->bracketErrorColor = QColor(224, 128, 128);
666     d_ptr->codeFolding = true;
667 
668     document()->setDocumentLayout(d_ptr->layout);
669 
670     connect(this, SIGNAL(cursorPositionChanged()), this, SLOT(updateCursor()));
671     connect(this, SIGNAL(blockCountChanged(int)), this, SLOT(updateSidebar()));
672     connect(this, SIGNAL(updateRequest(QRect, int)), this, SLOT(updateSidebar(QRect, int)));
673 
674 #if defined(Q_OS_MAC)
675     QFont textFont = font();
676     textFont.setPointSize(12);
677     textFont.setFamily("Monaco");
678     setFont(textFont);
679 #elif defined(Q_OS_UNIX)
680     QFont textFont = font();
681     textFont.setFamily("Monospace");
682     setFont(textFont);
683 #endif
684 }
685 
~JSEdit()686 JSEdit::~JSEdit()
687 {
688     delete d_ptr->layout;
689 }
690 
setColor(ColorComponent component,const QColor & color)691 void JSEdit::setColor(ColorComponent component, const QColor &color)
692 {
693     Q_D(JSEdit);
694 
695     if (component == Background) {
696         QPalette pal = palette();
697         pal.setColor(QPalette::Base, color);
698         setPalette(pal);
699         d->sidebar->indicatorColor = color;
700         updateSidebar();
701     } else if (component == Normal) {
702         QPalette pal = palette();
703         pal.setColor(QPalette::Text, color);
704         setPalette(pal);
705     } else if (component == Sidebar) {
706         d->sidebar->backgroundColor = color;
707         updateSidebar();
708     } else if (component == LineNumber) {
709         d->sidebar->lineNumberColor = color;
710         updateSidebar();
711     } else if (component == Cursor) {
712         d->cursorColor = color;
713         updateCursor();
714     } else if (component == BracketMatch) {
715         d->bracketMatchColor = color;
716         updateCursor();
717     } else if (component == BracketError) {
718         d->bracketErrorColor = color;
719         updateCursor();
720     } else if (component == FoldIndicator) {
721         d->sidebar->foldIndicatorColor = color;
722         updateSidebar();
723     } else {
724         d->highlighter->setColor(component, color);
725         updateCursor();
726     }
727 }
728 
keywords() const729 QStringList JSEdit::keywords() const
730 {
731     return d_ptr->highlighter->keywords();
732 }
733 
setKeywords(const QStringList & keywords)734 void JSEdit::setKeywords(const QStringList &keywords)
735 {
736     d_ptr->highlighter->setKeywords(keywords);
737 }
738 
isLineNumbersVisible() const739 bool JSEdit::isLineNumbersVisible() const
740 {
741     return d_ptr->showLineNumbers;
742 }
743 
setLineNumbersVisible(bool visible)744 void JSEdit::setLineNumbersVisible(bool visible)
745 {
746     d_ptr->showLineNumbers = visible;
747     updateSidebar();
748 }
749 
isTextWrapEnabled() const750 bool JSEdit::isTextWrapEnabled() const
751 {
752     return d_ptr->textWrap;
753 }
754 
setTextWrapEnabled(bool enable)755 void JSEdit::setTextWrapEnabled(bool enable)
756 {
757     d_ptr->textWrap = enable;
758     setLineWrapMode(enable ? WidgetWidth : NoWrap);
759 }
760 
isBracketsMatchingEnabled() const761 bool JSEdit::isBracketsMatchingEnabled() const
762 {
763     return d_ptr->bracketsMatching;
764 }
765 
setBracketsMatchingEnabled(bool enable)766 void JSEdit::setBracketsMatchingEnabled(bool enable)
767 {
768     d_ptr->bracketsMatching = enable;
769     updateCursor();
770 }
771 
isCodeFoldingEnabled() const772 bool JSEdit::isCodeFoldingEnabled() const
773 {
774     return d_ptr->codeFolding;
775 }
776 
setCodeFoldingEnabled(bool enable)777 void JSEdit::setCodeFoldingEnabled(bool enable)
778 {
779     d_ptr->codeFolding = enable;
780     updateSidebar();
781 }
782 
findClosingConstruct(const QTextBlock & block)783 static int findClosingConstruct(const QTextBlock &block)
784 {
785     if (!block.isValid())
786         return -1;
787     JSBlockData *blockData = reinterpret_cast<JSBlockData*>(block.userData());
788     if (!blockData)
789         return -1;
790     if (blockData->bracketPositions.isEmpty())
791         return -1;
792     const QTextDocument *doc = block.document();
793     int offset = block.position();
794     foreach (int pos, blockData->bracketPositions) {
795         int absPos = offset + pos;
796         if (doc->characterAt(absPos) == '{') {
797             int matchPos = findClosingMatch(doc, absPos);
798             if (matchPos >= 0)
799                 return matchPos;
800         }
801     }
802     return -1;
803 }
804 
isFoldable(int line) const805 bool JSEdit::isFoldable(int line) const
806 {
807     int matchPos = findClosingConstruct(document()->findBlockByNumber(line - 1));
808     if (matchPos >= 0) {
809         QTextBlock matchBlock = document()->findBlock(matchPos);
810         if (matchBlock.isValid() && matchBlock.blockNumber() > line)
811             return true;
812     }
813     return false;
814 }
815 
isFolded(int line) const816 bool JSEdit::isFolded(int line) const
817 {
818     QTextBlock block = document()->findBlockByNumber(line - 1);
819     if (!block.isValid())
820         return false;
821     block = block.next();
822     if (!block.isValid())
823         return false;
824     return !block.isVisible();
825 }
826 
fold(int line)827 void JSEdit::fold(int line)
828 {
829     QTextBlock startBlock = document()->findBlockByNumber(line - 1);
830     int endPos = findClosingConstruct(startBlock);
831     if (endPos < 0)
832         return;
833     QTextBlock endBlock = document()->findBlock(endPos);
834 
835     QTextBlock block = startBlock.next();
836     while (block.isValid() && block != endBlock) {
837         block.setVisible(false);
838         block.setLineCount(0);
839         block = block.next();
840     }
841 
842     document()->markContentsDirty(startBlock.position(), endPos - startBlock.position() + 1);
843     updateSidebar();
844     update();
845 
846     JSDocLayout *layout = reinterpret_cast<JSDocLayout*>(document()->documentLayout());
847     layout->forceUpdate();
848 }
849 
unfold(int line)850 void JSEdit::unfold(int line)
851 {
852     QTextBlock startBlock = document()->findBlockByNumber(line - 1);
853     int endPos = findClosingConstruct(startBlock);
854 
855     QTextBlock block = startBlock.next();
856     while (block.isValid() && !block.isVisible()) {
857         block.setVisible(true);
858         block.setLineCount(block.layout()->lineCount());
859         endPos = block.position() + block.length();
860         block = block.next();
861     }
862 
863     document()->markContentsDirty(startBlock.position(), endPos - startBlock.position() + 1);
864     updateSidebar();
865     update();
866 
867     JSDocLayout *layout = reinterpret_cast<JSDocLayout*>(document()->documentLayout());
868     layout->forceUpdate();
869 }
870 
toggleFold(int line)871 void JSEdit::toggleFold(int line)
872 {
873     if (isFolded(line))
874         unfold(line);
875     else
876         fold(line);
877 }
878 
resizeEvent(QResizeEvent * e)879 void JSEdit::resizeEvent(QResizeEvent *e)
880 {
881     QPlainTextEdit::resizeEvent(e);
882     updateSidebar();
883 }
884 
wheelEvent(QWheelEvent * e)885 void JSEdit::wheelEvent(QWheelEvent *e)
886 {
887     if (e->modifiers() == Qt::ControlModifier) {
888         int steps = e->delta() / 20;
889         steps = qBound(-3, steps, 3);
890         QFont textFont = font();
891         int pointSize = textFont.pointSize() + steps;
892         pointSize = qBound(10, pointSize, 40);
893         textFont.setPointSize(pointSize);
894         setFont(textFont);
895         updateSidebar();
896         e->accept();
897         return;
898     }
899     QPlainTextEdit::wheelEvent(e);
900 }
901 
902 
updateCursor()903 void JSEdit::updateCursor()
904 {
905     Q_D(JSEdit);
906 
907     if (isReadOnly()) {
908         setExtraSelections(QList<QTextEdit::ExtraSelection>());
909     } else {
910 
911         d->matchPositions.clear();
912         d->errorPositions.clear();
913 
914         if (d->bracketsMatching && textCursor().block().userData()) {
915             QTextCursor cursor = textCursor();
916             int cursorPosition = cursor.position();
917 
918             if (document()->characterAt(cursorPosition) == '{') {
919                 int matchPos = findClosingMatch(document(), cursorPosition);
920                 if (matchPos < 0) {
921                     d->errorPositions += cursorPosition;
922                 } else {
923                     d->matchPositions += cursorPosition;
924                     d->matchPositions += matchPos;
925                 }
926             }
927 
928             if (document()->characterAt(cursorPosition - 1) == '}') {
929                 int matchPos = findOpeningMatch(document(), cursorPosition);
930                 if (matchPos < 0) {
931                     d->errorPositions += cursorPosition - 1;
932                 } else {
933                     d->matchPositions += cursorPosition - 1;
934                     d->matchPositions += matchPos;
935                 }
936             }
937         }
938 
939         QTextEdit::ExtraSelection highlight;
940         highlight.format.setBackground(d->cursorColor);
941         highlight.format.setProperty(QTextFormat::FullWidthSelection, true);
942         highlight.cursor = textCursor();
943         highlight.cursor.clearSelection();
944 
945         QList<QTextEdit::ExtraSelection> extraSelections;
946         extraSelections.append(highlight);
947 
948         for (int i = 0; i < d->matchPositions.count(); ++i) {
949             int pos = d->matchPositions.at(i);
950             QTextEdit::ExtraSelection matchHighlight;
951             matchHighlight.format.setBackground(d->bracketMatchColor);
952             matchHighlight.cursor = textCursor();
953             matchHighlight.cursor.setPosition(pos);
954             matchHighlight.cursor.setPosition(pos + 1, QTextCursor::KeepAnchor);
955             extraSelections.append(matchHighlight);
956         }
957 
958         for (int i = 0; i < d->errorPositions.count(); ++i) {
959             int pos = d->errorPositions.at(i);
960             QTextEdit::ExtraSelection errorHighlight;
961             errorHighlight.format.setBackground(d->bracketErrorColor);
962             errorHighlight.cursor = textCursor();
963             errorHighlight.cursor.setPosition(pos);
964             errorHighlight.cursor.setPosition(pos + 1, QTextCursor::KeepAnchor);
965             extraSelections.append(errorHighlight);
966         }
967 
968         setExtraSelections(extraSelections);
969     }
970 }
971 
updateSidebar(const QRect & rect,int d)972 void JSEdit::updateSidebar(const QRect &rect, int d)
973 {
974     Q_UNUSED(rect)
975     if (d != 0)
976         updateSidebar();
977 }
978 
updateSidebar()979 void JSEdit::updateSidebar()
980 {
981     Q_D(JSEdit);
982 
983     if (!d->showLineNumbers && !d->codeFolding) {
984         d->sidebar->hide();
985         setViewportMargins(0, 0, 0, 0);
986         d->sidebar->setGeometry(3, 0, 0, height());
987         return;
988     }
989 
990     d->sidebar->foldIndicatorWidth = 0;
991     d->sidebar->font = this->font();
992     d->sidebar->show();
993 
994     int sw = 0;
995     if (d->showLineNumbers) {
996         int digits = 2;
997         int maxLines = blockCount();
998         for (int number = 10; number < maxLines; number *= 10)
999             ++digits;
1000         sw += fontMetrics().horizontalAdvance('w') * digits;
1001     }
1002     if (d->codeFolding) {
1003         int fh = fontMetrics().lineSpacing();
1004         int fw = fontMetrics().horizontalAdvance('w');
1005         d->sidebar->foldIndicatorWidth = qMax(fw, fh);
1006         sw += d->sidebar->foldIndicatorWidth;
1007     }
1008     setViewportMargins(sw, 0, 0, 0);
1009 
1010     d->sidebar->setGeometry(0, 0, sw, height());
1011     QRectF sidebarRect(0, 0, sw, height());
1012 
1013     QTextBlock block = firstVisibleBlock();
1014     int index = 0;
1015     while (block.isValid()) {
1016         if (block.isVisible()) {
1017             QRectF rect = blockBoundingGeometry(block).translated(contentOffset());
1018             if (sidebarRect.intersects(rect)) {
1019                 if (d->sidebar->lineNumbers.count() >= index)
1020                     d->sidebar->lineNumbers.resize(index + 1);
1021                 d->sidebar->lineNumbers[index].position = rect.top();
1022                 d->sidebar->lineNumbers[index].number = block.blockNumber() + 1;
1023                 d->sidebar->lineNumbers[index].foldable = d->codeFolding ? isFoldable(block.blockNumber() + 1) : false;
1024                 d->sidebar->lineNumbers[index].folded = d->codeFolding ? isFolded(block.blockNumber() + 1) : false;
1025                 ++index;
1026             }
1027             if (rect.top() > sidebarRect.bottom())
1028                 break;
1029         }
1030         block = block.next();
1031     }
1032     d->sidebar->lineNumbers.resize(index);
1033     d->sidebar->update();
1034 }
1035 
mark(const QString & str,Qt::CaseSensitivity sens)1036 void JSEdit::mark(const QString &str, Qt::CaseSensitivity sens)
1037 {
1038     d_ptr->highlighter->mark(str, sens);
1039 }
1040