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