From 7d842169a4f58d940b3637cb5bf4d141e9e8edc6 Mon Sep 17 00:00:00 2001 From: Andrey Paskal Date: Wed, 23 Dec 2009 09:54:00 +0300 Subject: [PATCH] First import of qtscript editor from svn://labs.trolltech.com/svn/qtscript/qtscriptdebug --- src/editor/editor.pri | 11 + src/editor/scriptedit.cpp | 43 ++ src/editor/scriptedit.h | 46 ++ src/editor/scripthighlighter.cpp | 704 ++++++++++++++++++ src/editor/scripthighlighter.h | 80 ++ src/editor/tabsettings.cpp | 187 +++++ src/editor/tabsettings.h | 75 ++ src/editor/textedit.cpp | 1476 ++++++++++++++++++++++++++++++++++++++ src/editor/textedit.h | 264 +++++++ 9 files changed, 2886 insertions(+), 0 deletions(-) create mode 100644 src/editor/editor.pri create mode 100644 src/editor/scriptedit.cpp create mode 100644 src/editor/scriptedit.h create mode 100644 src/editor/scripthighlighter.cpp create mode 100644 src/editor/scripthighlighter.h create mode 100644 src/editor/tabsettings.cpp create mode 100644 src/editor/tabsettings.h create mode 100644 src/editor/textedit.cpp create mode 100644 src/editor/textedit.h diff --git a/src/editor/editor.pri b/src/editor/editor.pri new file mode 100644 index 0000000..d49b206 --- /dev/null +++ b/src/editor/editor.pri @@ -0,0 +1,11 @@ +SOURCES += \ + $$PWD/scripthighlighter.cpp \ + $$PWD/textedit.cpp \ + $$PWD/tabsettings.cpp \ + $$PWD/scriptedit.cpp + +HEADERS += \ + $$PWD/scripthighlighter.h \ + $$PWD/textedit.h \ + $$PWD/tabsettings.h \ + $$PWD/scriptedit.h diff --git a/src/editor/scriptedit.cpp b/src/editor/scriptedit.cpp new file mode 100644 index 0000000..62003ed --- /dev/null +++ b/src/editor/scriptedit.cpp @@ -0,0 +1,43 @@ +/**************************************************************************** +** +** Copyright (C) 2007-2008 Trolltech ASA. All rights reserved. +** +** This file is part of the Qt Script Debug project on Trolltech Labs. +** +** This file may be used under the terms of the GNU General Public +** License version 2.0 as published by the Free Software Foundation +** and appearing in the file LICENSE.GPL included in the packaging of +** this file. Please review the following information to ensure GNU +** General Public Licensing requirements will be met: +** http://www.trolltech.com/products/qt/opensource.html +** +** If you are unsure which license is appropriate for your use, please +** review the following information: +** http://www.trolltech.com/products/qt/licensing.html or contact the +** sales department at sales@trolltech.com. +** +** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +** +****************************************************************************/ + +#include "scriptedit.h" + +#include "scripthighlighter.h" + +#include +#include +#include +#include + + +ScriptEdit::ScriptEdit(QWidget *parent) + : TextEdit(parent) +{ + highlighter = new ScriptHighlighter(this); +} + +ScriptEdit::~ScriptEdit() +{ +} + diff --git a/src/editor/scriptedit.h b/src/editor/scriptedit.h new file mode 100644 index 0000000..ae9fd24 --- /dev/null +++ b/src/editor/scriptedit.h @@ -0,0 +1,46 @@ +/**************************************************************************** +** +** Copyright (C) 2007-2008 Trolltech ASA. All rights reserved. +** +** This file is part of the Qt Script Debug project on Trolltech Labs. +** +** This file may be used under the terms of the GNU General Public +** License version 2.0 as published by the Free Software Foundation +** and appearing in the file LICENSE.GPL included in the packaging of +** this file. Please review the following information to ensure GNU +** General Public Licensing requirements will be met: +** http://www.trolltech.com/products/qt/opensource.html +** +** If you are unsure which license is appropriate for your use, please +** review the following information: +** http://www.trolltech.com/products/qt/licensing.html or contact the +** sales department at sales@trolltech.com. +** +** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +** +****************************************************************************/ + +#ifndef SCRIPTRUNNER_SCRIPTEDIT_H +#define SCRIPTRUNNER_SCRIPTEDIT_H + +#include "textedit.h" + +class ScriptHighlighter; + + +class ScriptEdit : public TextEdit +{ + Q_OBJECT + +public: + explicit ScriptEdit(QWidget *parent = 0); + ~ScriptEdit(); + +private: + ScriptHighlighter *highlighter; + int m_executingLineNumber; +}; + + +#endif // SCRIPTRUNNER_SCRIPTEDIT_H diff --git a/src/editor/scripthighlighter.cpp b/src/editor/scripthighlighter.cpp new file mode 100644 index 0000000..12b849e --- /dev/null +++ b/src/editor/scripthighlighter.cpp @@ -0,0 +1,704 @@ +/**************************************************************************** +** +** Copyright (C) 2007-2008 Trolltech ASA. All rights reserved. +** +** This file is part of the Qt Script Debug project on Trolltech Labs. +** +** This file may be used under the terms of the GNU General Public +** License version 2.0 as published by the Free Software Foundation +** and appearing in the file LICENSE.GPL included in the packaging of +** this file. Please review the following information to ensure GNU +** General Public Licensing requirements will be met: +** http://www.trolltech.com/products/qt/opensource.html +** +** If you are unsure which license is appropriate for your use, please +** review the following information: +** http://www.trolltech.com/products/qt/licensing.html or contact the +** sales department at sales@trolltech.com. +** +** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +** +****************************************************************************/ + + +#include "scripthighlighter.h" + +enum ScriptIds { + Comment = 1, + Number, + String, + Type, + Keyword, + PreProcessor, + Label +}; + +#define MAX_KEYWORD 63 +static const char *const keywords[MAX_KEYWORD] = { + "Infinity", + "NaN", + "abstract", + "boolean", + "break", + "byte", + "case", + "catch", + "char", + "class", + "const", + "constructor", + "continue", + "debugger", + "default", + "delete", + "do", + "double", + "else", + "enum", + "export", + "extends", + "false", + "final", + "finally", + "float", + "for", + "function", + "goto", + "if", + "implements", + "import", + "in", + "instanceof", + "int", + "interface", + "long", + "native", + "new", + "package", + "private", + "protected", + "public", + "return", + "short", + "static", + "super", + "switch", + "synchronized", + "this", + "throw", + "throws", + "transient", + "true", + "try", + "typeof", + "undefined", + "var", + "void", + "volatile", + "while", + "with", // end of array + 0 +}; + +struct KeywordHelper +{ + inline KeywordHelper(const QString &word) : needle(word) {} + const QString needle; +}; + +static bool operator<(const KeywordHelper &helper, const char *kw) +{ + return helper.needle < QLatin1String(kw); +} + +static bool operator<(const char *kw, const KeywordHelper &helper) +{ + return QLatin1String(kw) < helper.needle; +} + +static bool isKeyword(const QString &word) +{ + const char * const *start = &keywords[0]; + const char * const *end = &keywords[MAX_KEYWORD - 1]; + const char * const *kw = qBinaryFind(start, end, KeywordHelper(word)); + + return kw != end; +} + +ScriptHighlighter::ScriptHighlighter(TextEdit *editor) : + QSyntaxHighlighter(editor) +{ + setDocument(editor->document()); + textEdit = editor; + + m_formats[ScriptNumberFormat].setForeground(Qt::darkBlue); + m_formats[ScriptStringFormat].setForeground(Qt::darkGreen); + m_formats[ScriptTypeFormat].setForeground(Qt::darkMagenta); + m_formats[ScriptKeywordFormat].setForeground(Qt::darkYellow); + m_formats[ScriptPreprocessorFormat].setForeground(Qt::darkBlue); + m_formats[ScriptLabelFormat].setForeground(Qt::darkRed); + m_formats[ScriptCommentFormat].setForeground(Qt::darkGreen); + m_formats[ScriptCommentFormat].setFontItalic(true); + + // parentheses matcher + m_formatRange = true; + m_matchFormat.setForeground(Qt::red); + m_rangeFormat.setBackground(QColor(0xb4, 0xee, 0xb4)); + m_mismatchFormat.setBackground(Qt::magenta); + +} + +void ScriptHighlighter::highlightBlock(const QString &text) +{ + + // states + enum States { StateStandard, StateCommentStart1, StateCCommentStart2, + StateScriptCommentStart2, StateCComment, StateScriptComment, StateCCommentEnd1, + StateCCommentEnd2, StateStringStart, StateString, StateStringEnd, + StateString2Start, StateString2, StateString2End, + StateNumber, StatePreProcessor, NumStates }; + + // tokens + enum Tokens { InputAlpha, InputNumber, InputAsterix, InputSlash, InputParen, + InputSpace, InputHash, InputQuotation, InputApostrophe, InputSep, NumTokens }; + + static uchar table[NumStates][NumTokens] = { + { StateStandard, StateNumber, StateStandard, StateCommentStart1, StateStandard, StateStandard, StatePreProcessor, StateStringStart, StateString2Start, StateStandard }, // StateStandard + { StateStandard, StateNumber, StateCCommentStart2, StateScriptCommentStart2, StateStandard, StateStandard, StatePreProcessor, StateStringStart, StateString2Start, StateStandard }, // StateCommentStart1 + { StateCComment, StateCComment, StateCCommentEnd1, StateCComment, StateCComment, StateCComment, StateCComment, StateCComment, StateCComment, StateCComment }, // StateCCommentStart2 + { StateScriptComment, StateScriptComment, StateScriptComment, StateScriptComment, StateScriptComment, StateScriptComment, StateScriptComment, StateScriptComment, StateScriptComment, StateScriptComment }, // ScriptCommentStart2 + { StateCComment, StateCComment, StateCCommentEnd1, StateCComment, StateCComment, StateCComment, StateCComment, StateCComment, StateCComment, StateCComment }, // StateCComment + { StateScriptComment, StateScriptComment, StateScriptComment, StateScriptComment, StateScriptComment, StateScriptComment, StateScriptComment, StateScriptComment, StateScriptComment, StateScriptComment }, // StateScriptComment + { StateCComment, StateCComment, StateCCommentEnd1, StateCCommentEnd2, StateCComment, StateCComment, StateCComment, StateCComment, StateCComment, StateCComment }, // StateCCommentEnd1 + { StateStandard, StateNumber, StateStandard, StateCommentStart1, StateStandard, StateStandard, StatePreProcessor, StateStringStart, StateString2Start, StateStandard }, // StateCCommentEnd2 + { StateString, StateString, StateString, StateString, StateString, StateString, StateString, StateStringEnd, StateString, StateString }, // StateStringStart + { StateString, StateString, StateString, StateString, StateString, StateString, StateString, StateStringEnd, StateString, StateString }, // StateString + { StateStandard, StateStandard, StateStandard, StateCommentStart1, StateStandard, StateStandard, StatePreProcessor, StateStringStart, StateString2Start, StateStandard }, // StateStringEnd + { StateString2, StateString2, StateString2, StateString2, StateString2, StateString2, StateString2, StateString2, StateString2End, StateString2 }, // StateString2Start + { StateString2, StateString2, StateString2, StateString2, StateString2, StateString2, StateString2, StateString2, StateString2End, StateString2 }, // StateString2 + { StateStandard, StateStandard, StateStandard, StateCommentStart1, StateStandard, StateStandard, StatePreProcessor, StateStringStart, StateString2Start, StateStandard }, // StateString2End + { StateNumber, StateNumber, StateStandard, StateCommentStart1, StateStandard, StateStandard, StatePreProcessor, StateStringStart, StateString2Start, StateStandard }, // StateNumber + { StatePreProcessor, StateStandard, StateStandard, StateCommentStart1, StateStandard, StateStandard, StatePreProcessor, StateStringStart, StateString2Start, StateStandard } // StatePreProcessor + }; + + QString buffer; + buffer.reserve(text.length()); + + QTextCharFormat emptyFormat; + + int state = StateStandard; + int braceDepth = 0; + const int previousState = previousBlockState(); + if (previousState != -1) { + state = previousState & 0xff; + braceDepth = previousState >> 8; + } + + if (text.isEmpty()) { + setCurrentBlockState(previousState); + TextEditDocumentLayout::clearParentheses(currentBlock()); + return; + } + Parentheses parentheses; + parentheses.reserve(20); // assume wizard level ;-) + int input = -1; + int i = 0; + bool lastWasBackSlash = false; + bool makeLastStandard = false; + + static const QString alphabeth = QLatin1String("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"); + static const QString mathChars = QLatin1String("xXeE"); + static const QString numbers = QLatin1String("0123456789"); + bool questionMark = false; + QChar lastChar; + + int firstNonSpace = -1; + int lastNonSpace = -1; + + for (;;) { + const QChar c = text.at(i); + + if (lastWasBackSlash) { + input = InputSep; + } else { + switch (c.toAscii()) { + case '*': + input = InputAsterix; + break; + case '/': + input = InputSlash; + break; + case '{': + braceDepth++; + // fall through + case '(': case '[': + input = InputParen; + switch (state) { + case StateStandard: + case StateNumber: + case StatePreProcessor: + case StateCCommentEnd2: + case StateCCommentEnd1: + case StateString2End: + case StateStringEnd: + parentheses.push_back(Parenthesis(Parenthesis::Opened, c, i)); + break; + default: + break; + } + break; + case '}': + if (--braceDepth < 0) + braceDepth = 0; + // fall through + case ')': case ']': + input = InputParen; + switch (state) { + case StateStandard: + case StateNumber: + case StatePreProcessor: + case StateCCommentEnd2: + case StateCCommentEnd1: + case StateString2End: + case StateStringEnd: + parentheses.push_back(Parenthesis(Parenthesis::Closed, c, i)); + break; + default: + break; + } + break; + case '#': + input = InputHash; + break; + case '"': + input = InputQuotation; + break; + case '\'': + input = InputApostrophe; + break; + case ' ': + input = InputSpace; + break; + case '1': case '2': case '3': case '4': case '5': + case '6': case '7': case '8': case '9': case '0': + if (alphabeth.contains(lastChar) + && (!mathChars.contains(lastChar) || !numbers.contains(text.at(i - 1))) + ) { + input = InputAlpha; + } else { + if (input == InputAlpha && numbers.contains(lastChar)) + input = InputAlpha; + else + input = InputNumber; + } + break; + case ':': { + input = InputAlpha; + const QChar colon = QLatin1Char(':'); + if (state == StateStandard && !questionMark && lastChar != colon) { + const QChar nextChar = i < text.length() - 1 ? text.at(i + 1) : QLatin1Char(' '); + if (nextChar != colon) + for (int j = 0; j < i; ++j) { + if (format(j) == emptyFormat ) + setFormat(j, 1, m_formats[ScriptLabelFormat]); + } + } + } break; + default: + if (!questionMark && c == QLatin1Char('?')) + questionMark = true; + if (c.isLetter() || c == QLatin1Char('_')) + input = InputAlpha; + else + input = InputSep; + break; + } + } + + if (input != InputSpace) { + if (firstNonSpace < 0) + firstNonSpace = i; + lastNonSpace = i; + } + + lastWasBackSlash = !lastWasBackSlash && c == QLatin1Char('\\'); + + if (input == InputAlpha) + buffer += c; + + state = table[state][input]; + + switch (state) { + case StateStandard: { + setFormat(i, 1, emptyFormat); + if (makeLastStandard) + setFormat(i - 1, 1, emptyFormat); + makeLastStandard = false; + if (input != InputAlpha) { + highlightWord(i, buffer); + buffer = QString::null; + } + } break; + case StateCommentStart1: + if (makeLastStandard) + setFormat(i - 1, 1, emptyFormat); + makeLastStandard = true; + buffer = QString::null; + break; + case StateCCommentStart2: + setFormat(i - 1, 2, m_formats[ScriptCommentFormat]); + makeLastStandard = false; + parentheses.push_back(Parenthesis(Parenthesis::Opened, QLatin1Char('/'), i-1)); + buffer = QString::null; + break; + case StateScriptCommentStart2: + setFormat(i - 1, 2, m_formats[ScriptCommentFormat]); + makeLastStandard = false; + buffer = QString::null; + break; + case StateCComment: + if (makeLastStandard) + setFormat(i - 1, 1, emptyFormat); + makeLastStandard = false; + setFormat(i, 1, m_formats[ScriptCommentFormat]); + buffer = QString::null; + break; + case StateScriptComment: + if (makeLastStandard) + setFormat(i - 1, 1, emptyFormat); + makeLastStandard = false; + setFormat(i, 1, m_formats[ScriptCommentFormat]); + buffer = QString::null; + break; + case StateCCommentEnd1: + if (makeLastStandard) + setFormat(i - 1, 1, emptyFormat); + makeLastStandard = false; + setFormat(i, 1, m_formats[ScriptCommentFormat]); + buffer = QString::null; + break; + case StateCCommentEnd2: + if (makeLastStandard) + setFormat(i - 1, 1, emptyFormat); + makeLastStandard = false; + setFormat(i, 1, m_formats[ScriptCommentFormat]); + parentheses.push_back(Parenthesis(Parenthesis::Closed, QLatin1Char('/'), i)); + buffer = QString::null; + break; + case StateStringStart: + if (makeLastStandard) + setFormat(i - 1, 1, emptyFormat); + makeLastStandard = false; + setFormat(i, 1, emptyFormat); + buffer = QString::null; + break; + case StateString: + if (makeLastStandard) + setFormat(i - 1, 1, emptyFormat); + makeLastStandard = false; + setFormat(i, 1, m_formats[ScriptStringFormat]); + buffer = QString::null; + break; + case StateStringEnd: + if (makeLastStandard) + setFormat(i - 1, 1, emptyFormat); + makeLastStandard = false; + setFormat(i, 1, emptyFormat); + buffer = QString::null; + break; + case StateString2Start: + if (makeLastStandard) + setFormat(i - 1, 1, emptyFormat); + makeLastStandard = false; + setFormat(i, 1, emptyFormat); + buffer = QString::null; + break; + case StateString2: + if (makeLastStandard) + setFormat(i - 1, 1, emptyFormat); + makeLastStandard = false; + setFormat(i, 1, m_formats[ScriptStringFormat]); + buffer = QString::null; + break; + case StateString2End: + if (makeLastStandard) + setFormat(i - 1, 1, emptyFormat); + makeLastStandard = false; + setFormat(i, 1, emptyFormat); + buffer = QString::null; + break; + case StateNumber: + if (makeLastStandard) + setFormat(i - 1, 1, emptyFormat); + makeLastStandard = false; + setFormat(i, 1, m_formats[ScriptNumberFormat]); + buffer = QString::null; + break; + case StatePreProcessor: + if (makeLastStandard) + setFormat(i - 1, 1, emptyFormat); + makeLastStandard = false; + setFormat(i, 1, m_formats[ScriptPreprocessorFormat]); + buffer = QString::null; + break; + } + + lastChar = c; + i++; + if (i >= text.length()) { + int collapse = Parenthesis::collapseAtPos(parentheses); + if (collapse >= 0) { + if (collapse == firstNonSpace) + TextEditDocumentLayout::userData(currentBlock())->setCollapse(TextBlockUserData::CollapseThis); + else + TextEditDocumentLayout::userData(currentBlock())->setCollapse(TextBlockUserData::CollapseAfter); + } else if (TextBlockUserData *userData = TextEditDocumentLayout::testUserData(currentBlock())) { + userData->setCollapse(TextBlockUserData::NoCollapse); + } + break; + } + } + + highlightWord(text.length(), buffer); + + switch (state) { + case StateCComment: + case StateCCommentEnd1: + case StateCCommentStart2: + state = StateCComment; + break; + case StateString: + // quotes cannot span multiple lines, so if somebody starts + // typing a quoted string we don't need to look for the ending + // quote in another line (or highlight until the end of the + // document) and therefore slow down editing. + state = StateStandard; + break; + case StateString2: + state = StateStandard; + break; + default: + state = StateStandard; + break; + } + + TextEditDocumentLayout::setParentheses(currentBlock(), parentheses); + + setCurrentBlockState((braceDepth << 8) | state); +} + +void ScriptHighlighter::highlightWord(int currentPos, const QString &buffer) +{ + if (buffer.isEmpty()) + return; + + // try to highlight Qt 'identifiers' like QObject and Q_PROPERTY + // but don't highlight words like 'Query' + if (buffer.length() > 1 + && buffer.at(0) == QLatin1Char('Q') + && (buffer.at(1).isUpper() + || buffer.at(1) == QLatin1Char('_') + || buffer.at(1) == QLatin1Char('t'))) { + setFormat(currentPos - buffer.length(), buffer.length(), m_formats[ScriptTypeFormat]); + } else { + if (isKeyword(buffer)) + setFormat(currentPos - buffer.length(), buffer.length(), m_formats[ScriptKeywordFormat]); + } +} + + +ScriptHighlighter::MatchType ScriptHighlighter::checkOpenParenthesis(QTextCursor *cursor, QChar c) +{ + if (!TextEditDocumentLayout::hasParentheses(cursor->block())) + return NoMatch; + + Parentheses parenList = TextEditDocumentLayout::parentheses(cursor->block()); + Parenthesis openParen, closedParen; + QTextBlock closedParenParag = cursor->block(); + + const int cursorPos = cursor->position() - closedParenParag.position(); + int i = 0; + int ignore = 0; + bool foundOpen = false; + for (;;) { + if (!foundOpen) { + if (i >= parenList.count()) + return NoMatch; + openParen = parenList.at(i); + if (openParen.pos != cursorPos) { + ++i; + continue; + } else { + foundOpen = true; + ++i; + } + } + + if (i >= parenList.count()) { + for (;;) { + closedParenParag = closedParenParag.next(); + if (!closedParenParag.isValid()) + return NoMatch; + if (TextEditDocumentLayout::hasParentheses(closedParenParag)) { + parenList = TextEditDocumentLayout::parentheses(closedParenParag); + break; + } + } + i = 0; + } + + closedParen = parenList.at(i); + if (closedParen.type == Parenthesis::Opened) { + ignore++; + ++i; + continue; + } else { + if (ignore > 0) { + ignore--; + ++i; + continue; + } + + cursor->clearSelection(); + cursor->setPosition(closedParenParag.position() + closedParen.pos + 1, QTextCursor::KeepAnchor); + + if (c == QLatin1Char('{') && closedParen.chr != QLatin1Char('}') + || c == QLatin1Char('(') && closedParen.chr != QLatin1Char(')') + || c == QLatin1Char('[') && closedParen.chr != QLatin1Char(']') + ) + return Mismatch; + + return Match; + } + } +} + +ScriptHighlighter::MatchType ScriptHighlighter::checkClosedParenthesis(QTextCursor *cursor, QChar c) +{ + + if (!TextEditDocumentLayout::hasParentheses(cursor->block())) + return NoMatch; + + Parentheses parenList = TextEditDocumentLayout::parentheses(cursor->block()); + Parenthesis openParen, closedParen; + QTextBlock openParenParag = cursor->block(); + + const int cursorPos = cursor->position() - openParenParag.position(); + int i = parenList.count() - 1; + int ignore = 0; + bool foundClosed = false; + for (;;) { + if (!foundClosed) { + if (i < 0) + return NoMatch; + closedParen = parenList.at(i); + if (closedParen.pos != cursorPos - 1) { + --i; + continue; + } else { + foundClosed = true; + --i; + } + } + + if (i < 0) { + for (;;) { + openParenParag = openParenParag.previous(); + if (!openParenParag.isValid()) + return NoMatch; + + if (TextEditDocumentLayout::hasParentheses(openParenParag)) { + parenList = TextEditDocumentLayout::parentheses(openParenParag); + break; + } + } + i = parenList.count() - 1; + } + + openParen = parenList.at(i); + if (openParen.type == Parenthesis::Closed) { + ignore++; + --i; + continue; + } else { + if (ignore > 0) { + ignore--; + --i; + continue; + } + + cursor->clearSelection(); + cursor->setPosition(openParenParag.position() + openParen.pos, QTextCursor::KeepAnchor); + + if ( c == '}' && openParen.chr != '{' || + c == ')' && openParen.chr != '(' || + c == ']' && openParen.chr != '[' ) + return Mismatch; + + return Match; + } + } +} +ScriptHighlighter::MatchType ScriptHighlighter::matchCursor(QTextCursor *cursor) +{ + cursor->clearSelection(); + + const QTextBlock block = cursor->block(); + + + if (!TextEditDocumentLayout::hasParentheses(block)) + return NoMatch; + + const int relPos = cursor->position() - block.position(); + + Parentheses parentheses = TextEditDocumentLayout::parentheses(block); + const Parentheses::const_iterator cend = parentheses.constEnd(); + for (Parentheses::const_iterator it = parentheses.constBegin();it != cend; ++it) { + const Parenthesis &paren = *it; + if (paren.pos == relPos + && paren.type == Parenthesis::Opened) { + return checkOpenParenthesis(cursor, paren.chr); + } else if (paren.pos == relPos - 1 + && paren.type == Parenthesis::Closed) { + return checkClosedParenthesis(cursor, paren.chr); + } + } + return NoMatch; +} + +void ScriptHighlighter::matchParentheses() +{ + if (textEdit->isReadOnly()) + return; + + QTextCursor currentMatch = textEdit->textCursor(); + const MatchType matchType = matchCursor(¤tMatch); + if (currentMatch.hasSelection()) { + QList extraSelections = textEdit->extraSelections(); + QTextEdit::ExtraSelection sel; + if (matchType == Mismatch) { + sel.cursor = currentMatch; + sel.format = m_mismatchFormat; + } else { + + if (m_formatRange) { + sel.cursor = currentMatch; + sel.format = m_rangeFormat; + extraSelections.append(sel); + } + + sel.cursor = currentMatch; + sel.format = m_matchFormat; + + sel.cursor.setPosition(currentMatch.selectionStart()); + sel.cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor); + extraSelections.append(sel); + + sel.cursor.setPosition(currentMatch.selectionEnd()); + sel.cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor); + } + extraSelections.append(sel); + textEdit->setExtraSelections(extraSelections); + } +} diff --git a/src/editor/scripthighlighter.h b/src/editor/scripthighlighter.h new file mode 100644 index 0000000..f6dfec4 --- /dev/null +++ b/src/editor/scripthighlighter.h @@ -0,0 +1,80 @@ +/**************************************************************************** +** +** Copyright (C) 2007-2008 Trolltech ASA. All rights reserved. +** +** This file is part of the Qt Script Debug project on Trolltech Labs. +** +** This file may be used under the terms of the GNU General Public +** License version 2.0 as published by the Free Software Foundation +** and appearing in the file LICENSE.GPL included in the packaging of +** this file. Please review the following information to ensure GNU +** General Public Licensing requirements will be met: +** http://www.trolltech.com/products/qt/opensource.html +** +** If you are unsure which license is appropriate for your use, please +** review the following information: +** http://www.trolltech.com/products/qt/licensing.html or contact the +** sales department at sales@trolltech.com. +** +** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +** +****************************************************************************/ + +#ifndef SCRIPTRUNNER_SCRIPTHIGHLIGHTER_H +#define SCRIPTRUNNER_SCRIPTHIGHLIGHTER_H + +#include "textedit.h" + +#include +#include +#include + + +class ScriptHighlighter : public QSyntaxHighlighter +{ + Q_OBJECT + +public: + ScriptHighlighter(TextEdit *editor); + + virtual void highlightBlock(const QString &text); + +// // Set formats from a sequence of type QTextCharFormat +// template +// void setFormats(InputIterator begin, InputIterator end) { +// qCopy(begin, end, m_formats); +// } + + enum ScriptFormats { + ScriptTextFormat, ScriptNumberFormat, + ScriptStringFormat, ScriptTypeFormat, + ScriptKeywordFormat, ScriptPreprocessorFormat, + ScriptLabelFormat, ScriptCommentFormat, + NumScriptFormats + }; + +public Q_SLOTS: + void matchParentheses(); + +private: + enum MatchType { NoMatch, Match, Mismatch }; + static MatchType checkOpenParenthesis(QTextCursor *cursor, QChar c); + static MatchType checkClosedParenthesis(QTextCursor *cursor, QChar c); + static MatchType matchCursor(QTextCursor *cursor); + + void highlightWord(int currentPos, const QString &buffer); + + QTextCharFormat m_formats[NumScriptFormats]; + + // parentheses matcher + bool m_formatRange; + QTextCharFormat m_matchFormat; + QTextCharFormat m_mismatchFormat; + QTextCharFormat m_rangeFormat; + + TextEdit *textEdit; +}; + + +#endif // SCRIPTRUNNER_SCRIPTEDIT_H diff --git a/src/editor/tabsettings.cpp b/src/editor/tabsettings.cpp new file mode 100644 index 0000000..a7093fa --- /dev/null +++ b/src/editor/tabsettings.cpp @@ -0,0 +1,187 @@ +/**************************************************************************** +** +** Copyright (C) 2007-2008 Trolltech ASA. All rights reserved. +** +** This file is part of the Qt Script Debug project on Trolltech Labs. +** +** This file may be used under the terms of the GNU General Public +** License version 2.0 as published by the Free Software Foundation +** and appearing in the file LICENSE.GPL included in the packaging of +** this file. Please review the following information to ensure GNU +** General Public Licensing requirements will be met: +** http://www.trolltech.com/products/qt/opensource.html +** +** If you are unsure which license is appropriate for your use, please +** review the following information: +** http://www.trolltech.com/products/qt/licensing.html or contact the +** sales department at sales@trolltech.com. +** +** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +** +****************************************************************************/ + +#include "tabsettings.h" +#include +#include +#include +#include + +static const char* spacesForTabsKey="SpacesForTabs"; +static const char* autoIndentKey="AutoIndent"; +static const char* indentSelectionKey="IndentSelection"; +static const char* indentSizeKey="IndentSize"; +static const char* groupPostfix ="TabSettings"; + + +TabSettings::TabSettings() : + m_spacesForTabs(true), + m_autoIndent(true), + m_indentSelection(true), + m_indentSize(4) +{ +} + +void TabSettings::toSettings(const QString &category, QSettings *s) const +{ + QString group = QLatin1String(groupPostfix); + if (!category.isEmpty()) + group.insert(0, category); + s->beginGroup(group); + s->setValue(QLatin1String(spacesForTabsKey), m_spacesForTabs); + s->setValue(QLatin1String(autoIndentKey), m_autoIndent); + s->setValue(QLatin1String(indentSelectionKey), m_indentSelection); + s->setValue(QLatin1String(indentSizeKey), m_indentSize); + s->endGroup(); +} + +void TabSettings::fromSettings(const QString &category, const QSettings *s) +{ + QString group = QLatin1String(groupPostfix); + if (!category.isEmpty()) + group.insert(0, category); + group += QLatin1Char('/'); + + *this = TabSettings(); + + m_spacesForTabs = s->value(group + QLatin1String(spacesForTabsKey), m_spacesForTabs).toBool(); + m_autoIndent = s->value(group + QLatin1String(autoIndentKey), m_autoIndent).toBool(); + m_indentSelection = s->value(group + QLatin1String(indentSelectionKey), m_indentSelection).toBool(); + m_indentSize = s->value(group + QLatin1String(indentSizeKey), m_indentSize).toInt(); +} + +void TabSettings::tabify(QString &s) const +{ + const QChar tab = QLatin1Char('\t'); + const QChar blank = QLatin1Char(' '); + const QChar newLine = QLatin1Char('\n'); + + if (m_spacesForTabs) { + s.replace(tab, QString(m_indentSize, blank)); + } else { + int i = 0; + forever { + for (int j = i; j < s.length(); ++j) { + if (s.at(j) != blank && s.at(j) != tab) { + if (j > i) { + const QString t = s.mid(i, j - i); + int spaces = 0; + for (int k = 0; k < t.length(); ++k) + spaces += (t.at(k) == blank ? 1 : m_indentSize); + s.remove(i, t.length()); + const int tabs = spaces / m_indentSize; + spaces = spaces - (m_indentSize * tabs); + if (spaces > 0) + s.insert(i, QString(spaces, blank)); + if (tabs > 0) + s.insert(i, QString(tabs, tab)); + } + break; + } + } + i = s.indexOf(newLine, i); + if (i == -1) + break; + ++i; + } + } +} + +QString TabSettings::indentationString(int indent) const +{ + if (indent == 0) + return QString(); + QString indentString(indent, QLatin1Char(' ')); + indentString.append(QLatin1Char('a')); + tabify(indentString); + indentString.truncate(indentString.length() - 1); + return indentString; +} + +TabSettings::IndentationNumCharsPair TabSettings::indentation(const QString &s, IndentationCountMode icm) const +{ + IndentationNumCharsPair rc = IndentationNumCharsPair(0, 0); + const int size = s.size(); + if (size == 0) + return rc; + + const QChar tab = QLatin1Char('\t'); + const QChar blank = QLatin1Char(' '); + + for ( ; rc.second < size; rc.second++) { + const QChar c = s.at(rc.second); + if (c == tab) + rc.first += m_indentSize; + else { + if (c == blank || (icm == CountNonAlphaNumerical && !c.isLetterOrNumber())) + rc.first++; + else + break; + } + } + return rc; +} + +void TabSettings::indentLine(QTextBlock block, int newIndent) const +{ + const QString blockText = block.text(); + const int oldBlockLength = blockText.size(); + + // Quickly check whether indenting is required. + if (oldBlockLength == 0 && newIndent == 0) + return; + + const QString indentString = indentationString(newIndent); + newIndent = indentString.length(); + + if (oldBlockLength == indentString.length() && blockText == indentString) + return; + + if (oldBlockLength > indentString.length() && + blockText.startsWith(indentString) && + !blockText.at(indentString.length()).isSpace()) { + return; + } + + QTextCursor cursor(block); + cursor.movePosition(QTextCursor::StartOfBlock); + + const IndentationNumCharsPair oldIndentation = indentation(blockText); + if (oldIndentation.second > 0) { + cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, oldIndentation.second); + cursor.removeSelectedText(); + } + + if (!indentString.isEmpty()) { + cursor.insertText(indentString); + cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, indentString.length()); + } +} + +bool TabSettings::equals(const TabSettings &ts) const +{ + return m_spacesForTabs == ts.m_spacesForTabs && + m_autoIndent == ts.m_autoIndent && + m_indentSelection == ts.m_indentSelection && + m_indentSize == ts.m_indentSize; +} diff --git a/src/editor/tabsettings.h b/src/editor/tabsettings.h new file mode 100644 index 0000000..b706247 --- /dev/null +++ b/src/editor/tabsettings.h @@ -0,0 +1,75 @@ +/**************************************************************************** +** +** Copyright (C) 2007-2008 Trolltech ASA. All rights reserved. +** +** This file is part of the Qt Script Debug project on Trolltech Labs. +** +** This file may be used under the terms of the GNU General Public +** License version 2.0 as published by the Free Software Foundation +** and appearing in the file LICENSE.GPL included in the packaging of +** this file. Please review the following information to ensure GNU +** General Public Licensing requirements will be met: +** http://www.trolltech.com/products/qt/opensource.html +** +** If you are unsure which license is appropriate for your use, please +** review the following information: +** http://www.trolltech.com/products/qt/licensing.html or contact the +** sales department at sales@trolltech.com. +** +** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +** +****************************************************************************/ + +#ifndef TABSETTING_H +#define TABSETTING_H +#include +#include + +class QSettings; +class QString; +class QTextDocument; +class QTextCursor; + +// Tab settings: Data type the TabSettingsPage acts on +// with some convenience functions for formatting. +struct TabSettings { + TabSettings(); + + void toSettings(const QString &category, QSettings *s) const; + void fromSettings(const QString &category, const QSettings *s); + + void tabify(QString &s) const; + void indentLine(QTextBlock block, int newIndent) const; + + // Determine indentation string + QString indentationString(int indent) const; + + // A pair of / IndentationNumCharsPair; + + // How to determine the indentation + enum IndentationCountMode { + // Count space characters only. + CountSpaceCharacters, + // Consider non-alphanumerical characters to be blanks. + // This can be used for plain text editors for writing bulleted lists + // with items that span several lines. + CountNonAlphaNumerical }; + + // Determine indentation + IndentationNumCharsPair indentation(const QString &, IndentationCountMode icm = CountSpaceCharacters) const; + + bool m_spacesForTabs; + bool m_autoIndent; + bool m_indentSelection; + int m_indentSize; + + bool equals(const TabSettings &ts) const; +}; + +inline bool operator==(const TabSettings &t1, const TabSettings &t2) { return t1.equals(t2); } +inline bool operator!=(const TabSettings &t1, const TabSettings &t2) { return !t1.equals(t2); } + + +#endif diff --git a/src/editor/textedit.cpp b/src/editor/textedit.cpp new file mode 100644 index 0000000..827207d --- /dev/null +++ b/src/editor/textedit.cpp @@ -0,0 +1,1476 @@ +/**************************************************************************** +** +** Copyright (C) 2007-2008 Trolltech ASA. All rights reserved. +** +** This file is part of the Qt Script Debug project on Trolltech Labs. +** +** This file may be used under the terms of the GNU General Public +** License version 2.0 as published by the Free Software Foundation +** and appearing in the file LICENSE.GPL included in the packaging of +** this file. Please review the following information to ensure GNU +** General Public Licensing requirements will be met: +** http://www.trolltech.com/products/qt/opensource.html +** +** If you are unsure which license is appropriate for your use, please +** review the following information: +** http://www.trolltech.com/products/qt/licensing.html or contact the +** sales department at sales@trolltech.com. +** +** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +** +****************************************************************************/ + +#include "textedit.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + + +int Parenthesis::collapseAtPos(const Parentheses &parentheses) +{ + int result = -1; + + int depth = 0; + for (int i = 0; i < parentheses.size(); ++i) { + const Parenthesis &p = parentheses.at(i); + if (p.chr == QLatin1Char('{')) { + if (depth == 0) + result = p.pos; + ++depth; + } else if (p.chr == QLatin1Char('}')) { + if (--depth < 0) + depth = 0; + result = -1; + } + } + return result; +} + + +bool TextBlockUserData::hasClosingCollapse() const +{ + int depth = 0; + for (int i = 0; i < m_parentheses.size(); ++i) { + const Parenthesis &p = m_parentheses.at(i); + if (p.chr == QLatin1Char('{')) { + ++depth; + } else if (p.chr == QLatin1Char('}')) { + if (--depth < 0) + return true; + } + } + return false; +} + +int TextBlockUserData::collapseAtPos() const +{ + return Parenthesis::collapseAtPos(m_parentheses); +} + + +void TextEditDocumentLayout::setParentheses(const QTextBlock &block, const Parentheses &parentheses) +{ + if (parentheses.isEmpty()) { + if (TextBlockUserData *userData = testUserData(block)) + userData->clearParentheses(); + } else { + userData(block)->setParentheses(parentheses); + } +} + +Parentheses TextEditDocumentLayout::parentheses(const QTextBlock &block) +{ + if (TextBlockUserData *userData = testUserData(block)) + return userData->parentheses(); + return Parentheses(); +} + +bool TextEditDocumentLayout::hasParentheses(const QTextBlock &block) +{ + if (TextBlockUserData *userData = testUserData(block)) + return userData->hasParentheses(); + return false; +} + + + + + +QRectF TextEditDocumentLayout::blockBoundingRect(const QTextBlock &block) const +{ + QRectF r = QPlainTextDocumentLayout::blockBoundingRect(block); + return r; +} + +class TextEditExtraArea : public QWidget { + TextEdit *textEdit; +public: + TextEditExtraArea(TextEdit *edit):QWidget(edit) { + textEdit = edit; + setAutoFillBackground(true); + } +public: + + QSize sizeHint() const { + return QSize(textEdit->extraAreaWidth(), 0); + } +protected: + void paintEvent(QPaintEvent *event){ + textEdit->extraAreaPaintEvent(event); + } + void mousePressEvent(QMouseEvent *event){ + textEdit->extraAreaMouseEvent(event); + } + void mouseMoveEvent(QMouseEvent *event){ + textEdit->extraAreaMouseEvent(event); + } + void mouseReleaseEvent(QMouseEvent *event){ + textEdit->extraAreaMouseEvent(event); + } + + void wheelEvent(QWheelEvent *event) { + QCoreApplication::sendEvent(textEdit->viewport(), event); + } +}; + + + +TextEdit::TextEdit(QWidget *parent) : QPlainTextEdit(parent) +{ + viewport()->setMouseTracking(true); + extraAreaSelectionAnchorBlockNumber = extraAreaToggleBreakpointBlockNumber = -1; + QTextDocument *doc = document(); + doc->setDocumentLayout(new TextEditDocumentLayout(doc)); + + connect(this, SIGNAL(blockCountChanged(int)), this, SLOT(slotUpdateExtraAreaWidth())); + connect(this, SIGNAL(modificationChanged(bool)), this, SLOT(slotModificationChanged(bool))); + connect(this, SIGNAL(cursorPositionChanged()), this, SLOT(slotCursorPositionChanged())); + m_extraArea = new TextEditExtraArea(this); + m_extraArea->setMouseTracking(true); + connect(this, SIGNAL(updateRequest(QRect, int)), this, SLOT(slotUpdateRequest(QRect, int))); + + baseLine = 1; + + executionIcon = QIcon(":/images/location.svg"); + bookmarkIcon = QIcon(":/images/bookmark.svg"); + breakpointIcon = QIcon(":/images/breakpoint.svg"); + breakpointPendingIcon = QIcon(":/images/pending.svg"); + + // parentheses matcher + m_formatRange = true; + m_matchFormat.setForeground(Qt::red); + m_rangeFormat.setBackground(QColor(0xb4, 0xee, 0xb4)); + m_mismatchFormat.setBackground(Qt::magenta); + m_parenthesesMatchingTimer = new QTimer(this); + m_parenthesesMatchingTimer->setSingleShot(true); + connect(m_parenthesesMatchingTimer, SIGNAL(timeout()), this, SLOT(matchParentheses())); + + slotUpdateExtraAreaWidth(); + slotCursorPositionChanged(); +} + +TextEdit::~TextEdit() +{ +} + + +bool TextEdit::viewportEvent(QEvent *event) +{ + if (event->type() == QEvent::ToolTip) { + QString text; + QRect box; + if (collapsedBlockAt(static_cast(event)->pos(), &text, &box).isValid()) { + QToolTip::showText(static_cast(event)->globalPos(), text, viewport(), box); + } + return true; + } + return QPlainTextEdit::viewportEvent(event); +} + + +void TextEdit::resizeEvent(QResizeEvent *e) +{ + QPlainTextEdit::resizeEvent(e); + QRect cr = contentsRect(); + m_extraArea->setGeometry( + QStyle::visualRect(layoutDirection(), cr, + QRect(cr.left(), cr.top(), extraAreaWidth(), cr.height()))); +} + +QRect TextEdit::collapseBox(const QTextBlock &block) +{ + QRectF br = blockBoundingGeometry(block).translated(contentOffset()); + int collapseBoxWidth = fontMetrics().lineSpacing(); + return QRect(m_extraArea->width() - collapseBoxWidth + collapseBoxWidth/4, + int(br.top()) + collapseBoxWidth/4, + 2 * (collapseBoxWidth/4) + 1, 2 * (collapseBoxWidth/4) + 1); + +} + +QTextBlock TextEdit::collapsedBlockAt(const QPoint &pos, QString *text, QRect *box) const { + QPointF offset(contentOffset()); + QTextBlock block = firstVisibleBlock(); + int top = (int)blockBoundingGeometry(block).translated(offset).top(); + int bottom = top + (int)blockBoundingRect(block).height(); + + int viewportHeight = viewport()->height(); + + while (block.isValid() && top <= viewportHeight) { + QTextBlock nextBlock = block.next(); + if (block.isVisible() && bottom >= 0) { + if (nextBlock.isValid() && !nextBlock.isVisible()) { + QTextLayout *layout = block.layout(); + QTextLine line = layout->lineAt(layout->lineCount()-1); + QRectF lineRect = line.naturalTextRect().translated(offset.x(), top); + lineRect.adjust(0, 0, -1, -1); + + QRectF collapseRect(lineRect.right() + 8, lineRect.top(), + fontMetrics().width(QLatin1String(" {...}; ")), + lineRect.height()); + if (collapseRect.contains(pos)) { + QTextBlock result = block; + if (box) + *box = collapseRect.toAlignedRect(); + if (text) { + *text = QString(); + block = nextBlock; + int lineCount = 15; + while (nextBlock.isValid() && !nextBlock.isVisible()) { + block = nextBlock; + if (lineCount) { + --lineCount; + if (!text->isEmpty()) + *text += QLatin1Char('\n'); + *text += block.text(); + } + nextBlock = block.next(); + } + if (lineCount == 0) + *text += QLatin1String("\n..."); + } + return result; + } else { + block = nextBlock; + while (nextBlock.isValid() && !nextBlock.isVisible()) { + block = nextBlock; + nextBlock = block.next(); + } + } + } + } + + block = nextBlock; + top = bottom; + bottom = top + (int)blockBoundingRect(block).height(); + } + return QTextBlock(); +} + +void TextEdit::paintEvent(QPaintEvent *e) +{ + QPlainTextEdit::paintEvent(e); + + QPainter painter(viewport()); + QPointF offset(contentOffset()); + QTextBlock block = firstVisibleBlock(); + int top = (int)blockBoundingGeometry(block).translated(offset).top(); + int bottom = top + (int)blockBoundingRect(block).height(); + + while (block.isValid() && top <= e->rect().bottom()) { + QTextBlock nextBlock = block.next(); + if (block.isVisible() && bottom >= e->rect().top()) { + if (nextBlock.isValid() && !nextBlock.isVisible()) { + QTextLayout *layout = block.layout(); + QTextLine line = layout->lineAt(layout->lineCount()-1); + QRectF lineRect = line.naturalTextRect().translated(offset.x(), top); + lineRect.adjust(0, 0, -1, -1); + + QRectF collapseRect(lineRect.right() + 8, lineRect.top(), + fontMetrics().width(QLatin1String(" {...}; ")), + lineRect.height()); + painter.drawRect(collapseRect.adjusted(0, 0, 0, -1)); + + + QString replacement = QLatin1String("..."); + + QTextBlock info = block; + if (block.userData() + && static_cast(block.userData())->collapse() == TextBlockUserData::CollapseAfter) + ; + else if (block.next().userData() + && static_cast(block.next().userData())->collapse() + == TextBlockUserData::CollapseThis) { + replacement.prepend(nextBlock.text().trimmed().left(1)); + info = nextBlock; + } + + block = nextBlock; + while (nextBlock.isValid() && !nextBlock.isVisible()) { + block = nextBlock; + nextBlock = block.next(); + } + if (static_cast(info.userData())->collapseIncludesClosure()) { + QString right = block.text().trimmed(); + if (right.endsWith(QLatin1Char(';'))) { + right.chop(1); + right = right.trimmed(); + replacement.append(right.right(1)); + replacement.append(QLatin1Char(';')); + } else { + replacement.append(right.right(1)); + } + } + painter.drawText(collapseRect, Qt::AlignCenter, replacement); + } + } + + block = nextBlock; + top = bottom; + bottom = top + (int)blockBoundingRect(block).height(); + } + + +} + +void TextEdit::slotUpdateExtraAreaWidth() +{ + if (isLeftToRight()) + setViewportMargins(extraAreaWidth(), 0, 0, 0); + else + setViewportMargins(0, 0, extraAreaWidth(), 0); +} + + +QWidget *TextEdit::extraArea() const { + return m_extraArea; +} + +int TextEdit::extraAreaWidth(int *markWidthPtr) const { + int digits = 1; + int max = qMax(1, blockCount()); + while (max >= 10) { + max /= 10; + ++digits; + } + QFontMetrics fm(fontMetrics()); + int space = fm.width(QLatin1Char('9')) * digits; + TextEditDocumentLayout *documentLayout = qobject_cast(document()->documentLayout()); + Q_ASSERT(documentLayout); + int markWidth = 0; + markWidth += fm.lineSpacing(); + if (documentLayout->doubleMarkCount) + markWidth += fm.lineSpacing() / 3; + + if (markWidthPtr) + *markWidthPtr = markWidth; + + space += markWidth; + + + space += 4; + + space += fm.lineSpacing(); + + return space; +} + +void TextEdit::slotModificationChanged(bool m) +{ + if (!m) { + QTextDocument *doc = document(); + TextEditDocumentLayout *documentLayout = qobject_cast(doc->documentLayout()); + Q_ASSERT(documentLayout); + int oldLastSaveRevision = documentLayout->lastSaveRevision; + documentLayout->lastSaveRevision = doc->revision(); + QTextBlock block = doc->begin(); + while (block.isValid()) { + if (block.revision() < 0 || block.revision() != oldLastSaveRevision) { + block.setRevision(-documentLayout->lastSaveRevision - 1); + } else { + block.setRevision(documentLayout->lastSaveRevision); + } + block = block.next(); + } + m_extraArea->update(); + } +} + +void TextEdit::slotUpdateRequest(const QRect &r, int dy) +{ + if (dy) + m_extraArea->scroll(0, dy); + else + m_extraArea->update(0, r.y(), m_extraArea->width(), r.height()); + + if (r.contains(viewport()->rect())) + slotUpdateExtraAreaWidth(); +} + + +int TextEdit::executionBlockNumber() const +{ + QTextDocument *doc = document(); + TextEditDocumentLayout *documentLayout = qobject_cast(doc->documentLayout()); + Q_ASSERT(documentLayout); + return documentLayout->executionBlockNumber(); +} + +void TextEdit::setExecutionBlockNumber(int blockNumber) +{ + QTextDocument *doc = document(); + TextEditDocumentLayout *documentLayout = qobject_cast(doc->documentLayout()); + Q_ASSERT(documentLayout); + documentLayout->setExecutionBlockNumber(blockNumber); + + if (true) { + QTextBlock block = document()->findBlockByNumber(blockNumber); + if (block.isValid()) { + QTextCursor cursor = textCursor(); + cursor.setPosition(block.position()); + setTextCursor(cursor); + } + } +} + +void TextEdit::extraAreaPaintEvent(QPaintEvent *e) +{ + TextEditDocumentLayout *documentLayout = qobject_cast(document()->documentLayout()); + Q_ASSERT(documentLayout); + + QPalette pal = palette(); + QPainter painter(m_extraArea); + QFontMetrics fm(painter.fontMetrics()); + int fmLineSpacing = fm.lineSpacing(); + + int markWidth = 0; + markWidth += fm.lineSpacing(); + if (documentLayout->doubleMarkCount) + markWidth += fm.lineSpacing() / 3; + + + int collapseBoxWidth = fmLineSpacing; + int extraAreaWidth = m_extraArea->width() - collapseBoxWidth; + + QLinearGradient gradient(QPointF(extraAreaWidth - 10, 0), QPointF(extraAreaWidth, 0)); + gradient.setColorAt(0, pal.color(QPalette::Background)); + gradient.setColorAt(1, pal.color(QPalette::Base)); + painter.fillRect(e->rect(), gradient); + + QLinearGradient gradient2(QPointF(0, 0), QPointF(markWidth, 0)); + gradient2.setColorAt(0, pal.color(QPalette::Dark)); + gradient2.setColorAt(1, pal.color(QPalette::Background)); + painter.fillRect(e->rect().intersected(QRect(0, 0, markWidth, INT_MAX)), gradient2); + + + painter.setPen(QPen(pal.color(QPalette::Background), 2)); + painter.drawLine(extraAreaWidth-1, e->rect().top(), extraAreaWidth-1, e->rect().bottom()); + painter.setRenderHint(QPainter::Antialiasing); + + + QTextBlock block = firstVisibleBlock(); + int blockNumber = block.blockNumber(); + int top = (int)blockBoundingGeometry(block).translated(contentOffset()).top(); + int bottom = top + (int)blockBoundingRect(block).height(); + + int executionBlock = executionBlockNumber(); + + while (block.isValid() && top <= e->rect().bottom()) { + bool collapseThis = false; + bool collapseAfter = false; + bool hasClosingCollapse = false; + QTextBlock nextBlock = block.next(); + if (block.isVisible() && bottom >= e->rect().top()) { + QString number = QString::number(blockNumber + baseLine); + painter.setPen(pal.color(QPalette::Highlight)); + + if (TextBlockUserData *userData = static_cast(block.userData())) { + int x = 0; + int radius = fmLineSpacing - 1; + if (userData->bookmark()) { + QRect r(x, top, radius, radius); + bookmarkIcon.paint(&painter, r); + x += fmLineSpacing / 3; + } + if (userData->breakpoint() || userData->pendingBreakpoint()) { + QRect r(x, top, radius, radius); + breakpointIcon.paint(&painter, r); + if (!userData->breakpoint()) { // pending only + breakpointPendingIcon.paint(&painter, r); + } + } + + collapseAfter = (userData->collapse() == TextBlockUserData::CollapseAfter); + collapseThis = (userData->collapse() == TextBlockUserData::CollapseThis); + hasClosingCollapse = userData->hasClosingCollapse(); + } + + + if (executionBlock == blockNumber) { + QRect br(0, top, markWidth, fmLineSpacing); + executionIcon.paint(&painter, br); + } + + + int previousBraceDepth = block.previous().userState(); + if (previousBraceDepth >= 0) + previousBraceDepth >>= 8; + else + previousBraceDepth = 0; + int braceDepth = block.userState(); + if (!nextBlock.isVisible()) { + QTextBlock b = nextBlock; + while (b.isValid() && !b.isVisible()) + b = b.next(); + braceDepth = b.userState(); + } + if (braceDepth >= 0) + braceDepth >>= 8; + else + braceDepth = 0; + + QRect box(extraAreaWidth + collapseBoxWidth/4, top + collapseBoxWidth/4, + 2 * (collapseBoxWidth/4) + 1, 2 * (collapseBoxWidth/4) + 1); + QPoint boxCenter = box.center(); + painter.save(); + painter.setPen(pal.text().color()); + painter.setRenderHint(QPainter::Antialiasing, false); + + bool collapseNext = (nextBlock.userData() && static_cast(nextBlock.userData())->collapse() == TextBlockUserData::CollapseThis); + + if (previousBraceDepth || collapseThis) + painter.drawLine(boxCenter.x(), top, boxCenter.x(), boxCenter.y()); + + if (braceDepth || (collapseNext && nextBlock.isVisible())) + painter.drawLine(boxCenter.x(), boxCenter.y(), boxCenter.x(), bottom-1); + + if (collapseAfter || collapseNext) { + painter.setBrush(pal.background()); + painter.drawRect(box.adjusted(0, 0, -1, -1)); + if (!nextBlock.isVisible()) + painter.drawLine(boxCenter.x(), box.top() + 2, boxCenter.x(), box.bottom() - 2); + painter.drawLine(box.left() + 2, boxCenter.y(), box.right() - 2, boxCenter.y()); + } else if (hasClosingCollapse) { + painter.drawLine(boxCenter.x(), boxCenter.y(), box.right() - 1, boxCenter.y()); + } + + painter.restore(); + + +// qDebug() << "block" << block.blockNumber() << "revision:" << block.revision() << "lastSaved:" << documentLayout->lastSaveRevision; + if (block.revision() != documentLayout->lastSaveRevision) { + painter.save(); + painter.setRenderHint(QPainter::Antialiasing, false); + if (block.revision() < 0) + painter.setPen(QPen(Qt::darkGreen, 2)); + else + painter.setPen(QPen(Qt::red, 2)); + painter.drawLine(extraAreaWidth-1, top, extraAreaWidth-1, bottom-1); + painter.restore(); + } + + painter.drawText(markWidth, top, extraAreaWidth - markWidth - 4, fm.height(), Qt::AlignRight, number); +// painter.drawText(markWidth + 4, top + fm.ascent() + fm.leading(), number); + + + } + block = nextBlock; + top = bottom; + bottom = top + (int)blockBoundingRect(block).height(); + ++blockNumber; + } + +} + +void TextEdit::timerEvent(QTimerEvent *e) +{ + if (e->timerId() == autoScrollTimer.timerId()) { + const QPoint globalPos = QCursor::pos(); + const QPoint pos = m_extraArea->mapFromGlobal(globalPos); + QRect visible = m_extraArea->rect(); + verticalScrollBar()->triggerAction( pos.y() < visible.center().y() ? + QAbstractSlider::SliderSingleStepSub + : QAbstractSlider::SliderSingleStepAdd); + QMouseEvent ev(QEvent::MouseMove, pos, globalPos, Qt::LeftButton, Qt::LeftButton, Qt::NoModifier); + extraAreaMouseEvent(&ev); + int delta = qMax(pos.y() - visible.top(), visible.bottom() - pos.y()) - visible.height(); + if (delta < 7) + delta = 7; + int timeout = 4900 / (delta * delta); + autoScrollTimer.start(timeout, this); + + } + QPlainTextEdit::timerEvent(e); +} + + +void TextEdit::mouseMoveEvent(QMouseEvent *e) +{ + if (e->buttons() == 0) { + QTextBlock collapsedBlock = collapsedBlockAt(e->pos()); + viewport()->setCursor(collapsedBlock.isValid() ? Qt::PointingHandCursor : Qt::IBeamCursor); + } else { + QPlainTextEdit::mouseMoveEvent(e); + } +} + +void TextEdit::mousePressEvent(QMouseEvent *e) +{ + if (e->button() == Qt::LeftButton) { + QTextBlock collapsedBlock = collapsedBlockAt(e->pos()); + if (collapsedBlock.isValid()) { + toggleBlockVisible(collapsedBlock); + viewport()->setCursor(Qt::IBeamCursor); + } + } + QPlainTextEdit::mousePressEvent(e); +} + +void TextEdit::extraAreaMouseEvent(QMouseEvent *e) +{ + QTextCursor cursor = cursorForPosition(QPoint(0, e->pos().y())); + cursor.setPosition(cursor.block().position()); + + int markWidth; + extraAreaWidth(&markWidth); + + if (e->type() == QEvent::MouseMove && e->buttons() == 0) { // mouse tracking + bool hand = (e->pos().x() <= markWidth + || (TextBlockUserData::canCollapse(cursor.block()) + && collapseBox(cursor.block()).contains(e->pos()))); + if (hand != (m_extraArea->cursor().shape() == Qt::PointingHandCursor)) + m_extraArea->setCursor(hand ? Qt::PointingHandCursor : Qt::ArrowCursor); + return; + } + + + if (e->type() == QEvent::MouseButtonPress || e->type() == QEvent::MouseButtonDblClick) { + if (e->button() == Qt::LeftButton) { + if (TextBlockUserData::canCollapse(cursor.block()) + && collapseBox(cursor.block()).contains(e->pos())) { + setTextCursor(cursor); + toggleBlockVisible(cursor.block()); + } else if (e->pos().x() > markWidth) { + QTextCursor selection = cursor; + extraAreaSelectionAnchorBlockNumber = selection.blockNumber(); + selection.movePosition(QTextCursor::NextBlock, QTextCursor::KeepAnchor); + setTextCursor(selection); + } else { + extraAreaToggleBreakpointBlockNumber = cursor.blockNumber(); + } + } + } else if (extraAreaSelectionAnchorBlockNumber >= 0) { + QTextCursor selection = cursor; + if (e->type() == QEvent::MouseMove) { + QTextBlock anchorBlock = document()->findBlockByNumber(extraAreaSelectionAnchorBlockNumber); + if (extraAreaSelectionAnchorBlockNumber == cursor.blockNumber()) { + selection.movePosition(QTextCursor::NextBlock, QTextCursor::KeepAnchor); + } else { + selection.setPosition(anchorBlock.position() + + (cursor.blockNumber() <= extraAreaSelectionAnchorBlockNumber ? + anchorBlock.length() - (anchorBlock.next().isValid()?0:1) : 0)); + selection.setPosition(cursor.block().position(), QTextCursor::KeepAnchor); + } + + if (e->pos().y() >= 0 && e->pos().y() <= m_extraArea->height()) + autoScrollTimer.stop(); + else if (!autoScrollTimer.isActive()) + autoScrollTimer.start(100, this); + + } else { + autoScrollTimer.stop(); + extraAreaSelectionAnchorBlockNumber = -1; + return; + } + setTextCursor(selection); + } else if (extraAreaToggleBreakpointBlockNumber >= 0) { + if (e->type() == QEvent::MouseButtonRelease && e->button() == Qt::LeftButton) { + if (cursor.blockNumber() == extraAreaToggleBreakpointBlockNumber) { + bool b = false; + if (TextBlockUserData *userData = TextEditDocumentLayout::testUserData(cursor.block())) + b = userData->breakpoint(); + setBreakpoint_helper(extraAreaToggleBreakpointBlockNumber, !b); + } + extraAreaToggleBreakpointBlockNumber = -1; + } + } +} + +void TextEdit::slotCursorPositionChanged() +{ + + QList extraSelections; + + if (true || !isReadOnly()){ // the line selection + + QTextEdit::ExtraSelection sel; + + QTextCursor cursor = textCursor(); + const QColor fg = palette().color(QPalette::Highlight); + const QColor bg = palette().color(QPalette::Base); + QColor col; + const qreal ratio = 0.15; + col.setRedF(fg.redF() * ratio + bg.redF() * (1 - ratio)); + col.setGreenF(fg.greenF() * ratio + bg.greenF() * (1 - ratio)); + col.setBlueF(fg.blueF() * ratio + bg.blueF() * (1 - ratio)); + sel.format.setBackground(col); + sel.format.setProperty(QTextFormat::FullWidthSelection, true); + sel.cursor = cursor; + sel.cursor.clearSelection(); + extraSelections.append(sel); + + m_parenthesesMatchingTimer->start(50); + } + + setExtraSelections(extraSelections); +} + +void TextEdit::slotToggleBookmark() +{ + QTextBlock block = textCursor().block(); + bool b = false; + if (TextBlockUserData *userData = TextEditDocumentLayout::testUserData(block)) + b = userData->bookmark(); + setBookmark_helper(block.blockNumber(), !b); +} + +void TextEdit::slotToggleBreakpoint() +{ + QTextBlock block = textCursor().block(); + bool b = false; + if (TextBlockUserData *userData = TextEditDocumentLayout::testUserData(block)) + b = userData->breakpoint(); + setBreakpoint_helper(block.blockNumber(), !b); +} + +void TextEdit::slotTogglePendingBreakpoint() +{ + QTextBlock block = textCursor().block(); + bool b = false; + if (TextBlockUserData *userData = TextEditDocumentLayout::testUserData(block)) + b = userData->pendingBreakpoint(); + setPendingBreakpoint_helper(block.blockNumber(), !b); +} + +void TextEdit::slotSetExecutionBlock() +{ + setExecutionBlockNumber(textCursor().block().blockNumber()); +} + + +void TextBlockUserData::setCollapse(const QTextBlock& block, bool visible) +{ + QTextBlock info = block; + if (block.userData() && static_cast(block.userData())->collapse() == CollapseAfter) + ; + else if (block.next().userData() + && static_cast(block.next().userData())->collapse() + == TextBlockUserData::CollapseThis) + info = block.next(); + else + return; + int pos = static_cast(info.userData())->collapseAtPos(); + if (pos < 0) + return; + QTextCursor cursor(info); + cursor.setPosition(cursor.position() + pos); + if (matchCursorForward(&cursor) != Match) { + if (visible) { + // no match, at least unfold! + QTextBlock b = block.next(); + while (b.isValid() && !b.isVisible()) { + b.setVisible(true); + b = b.next(); + } + } + return; + } + + QTextBlock b = block.next(); + while (b < cursor.block()) { + b.setVisible(visible); + b = b.next(); + } + + Q_ASSERT(cursor.block() == b); + int relPos = cursor.position() - b.position(); + QString s = b.text().mid(relPos).trimmed(); + bool collapseIncludesClosure = (s.isEmpty() || s == QLatin1String(";")); + if (collapseIncludesClosure) + b.setVisible(visible); + static_cast(info.userData())->setCollapseIncludesClosure(collapseIncludesClosure); +} + +void TextEdit::toggleBlockVisible(const QTextBlock &block) +{ + TextEditDocumentLayout *documentLayout = qobject_cast(document()->documentLayout()); + Q_ASSERT(documentLayout); + + if (!TextBlockUserData::canCollapse(block)) + return; + + bool visible = block.next().isVisible(); + TextBlockUserData::setCollapse(block, !visible); + documentLayout->requestUpdate(); +} + +void TextEdit::slotToggleBlockVisible() +{ + toggleBlockVisible(textCursor().block()); +} + + +QChar TextEdit::charAt(int pos) const +{ + QTextCursor c = textCursor(); + + if (pos < 0) + return QChar(); + + c.movePosition(QTextCursor::End); + if (pos >= c.position()) + return QChar(); + + c.setPosition(pos); + c.setPosition(pos + 1, QTextCursor::KeepAnchor); + + return c.selectedText().at(0); +} + + +void TextEdit::keyPressEvent(QKeyEvent *e) +{ + switch (e->key()) { + case Qt::Key_Enter: + case Qt::Key_Return: + if (m_tabsettings.m_autoIndent) { + QTextCursor cursor = textCursor(); + cursor.insertBlock(); + const QString leftText = cursor.block().text().left(cursor.position() - cursor.block().position()); + if (cursor.hasSelection() || leftText.simplified().isEmpty()) { + indent(document(), cursor, QChar::Null); + } + e->accept(); + setTextCursor(cursor); + return; + } + break; + case Qt::Key_Tab: + if (m_tabsettings.m_spacesForTabs) { + indentSelection(); + e->accept(); + return; + } + break; + case Qt::Key_Backtab: + if (m_tabsettings.m_indentSelection) { + unIndentSelection(); + e->accept(); + return; + } + break; + case Qt::Key_Home: + if (!(e == QKeySequence::MoveToStartOfDocument)) { + handleHomeKey(e->modifiers() == Qt::ShiftModifier); + e->accept(); + return; + } + break; + case Qt::Key_Left: + case Qt::Key_Right: + if ((e->modifiers() == Qt::NoModifier) && textCursor().hasSelection()) { + handleArrowKeys(e->key()); + e->accept(); + return; + } + break; + case Qt::Key_BraceLeft: + case Qt::Key_BraceRight: { + const QString text = e->text(); + if (!text.isEmpty()) { + const QChar c = text.at(0); + if (c == QLatin1Char('}') || c == QLatin1Char('{')) { + QTextCursor cursor = textCursor(); + cursor.insertText(QString(c)); + const QString leftText = cursor.block().text().left(cursor.position() - 1 - cursor.block().position()); + if (cursor.hasSelection() || leftText.simplified().isEmpty()) { + indent(document(), cursor, c); + } + TextEditDocumentLayout *documentLayout = qobject_cast(document()->documentLayout()); + Q_ASSERT(documentLayout); + documentLayout->requestUpdate(); // a bit drastic + e->accept(); + setTextCursor(cursor); + return; + } + } + } break; + + default: + break; + } + QPlainTextEdit::keyPressEvent(e); + if (e->key() == Qt::Key_Delete) + slotCursorPositionChanged(); // parentheses matching +} + +void TextEdit::unIndentSelection() +{ + QTextCursor cursor = textCursor(); + cursor.beginEditBlock(); + const QString indent(m_tabsettings.m_indentSize, QLatin1Char(' ')); + + const int anchor = cursor.anchor(); + const int pos = cursor.position(); + int indents = 0; + const bool reversed = (anchor > pos); + int startPos = (reversed ? pos : anchor); + int endPos = (reversed ? anchor : pos); + const QTextBlock startBlock = document()->findBlock(reversed ? pos : anchor); + QTextBlock lastBlock = document()->findBlock(reversed ? anchor : pos); + if ((lastBlock.position() == pos || lastBlock.position() == anchor) && startBlock != lastBlock) + lastBlock = lastBlock.previous(); + + QTextBlock curBlock = startBlock; + while(curBlock.previous() != lastBlock) { + if (curBlock.text().startsWith(indent)) { + cursor.setPosition(curBlock.position()); + cursor.setPosition(curBlock.position() + m_tabsettings.m_indentSize, QTextCursor::KeepAnchor); + cursor.removeSelectedText(); + ++indents; + } + curBlock = curBlock.next(); + } + + startPos = startBlock.position(); + endPos -= m_tabsettings.m_indentSize * (indents); + if (endPos < startPos) + endPos = startPos; + cursor.setPosition(reversed ? endPos : startPos); + cursor.setPosition(reversed ? startPos : endPos, QTextCursor::KeepAnchor); + + cursor.endEditBlock(); + setTextCursor(cursor); +} + +void TextEdit::indentSelection() +{ + QTextCursor cursor = textCursor(); + cursor.beginEditBlock(); + + const QString indent(m_tabsettings.m_indentSize, QLatin1Char(' ')); + if (cursor.hasSelection() && m_tabsettings.m_indentSelection) { + int anchor = cursor.anchor(); + int pos = cursor.position(); + int indents = 0; + const bool reversed = (anchor > pos); + const QTextBlock startBlock = document()->findBlock(reversed ? pos : anchor); + QTextBlock lastBlock = document()->findBlock(reversed ? anchor : pos); + if ((lastBlock.position() == pos || lastBlock.position() == anchor) && startBlock != lastBlock) + lastBlock = lastBlock.previous(); + + QTextBlock curBlock = startBlock; + while(curBlock.previous() != lastBlock) { + cursor.setPosition(curBlock.position()); + cursor.insertText(indent); + curBlock = curBlock.next(); + ++indents; + } + + if (reversed) { + pos = startBlock.position(); + anchor += m_tabsettings.m_indentSize * (indents); + } else { + anchor = startBlock.position(); + pos += m_tabsettings.m_indentSize * (indents); + } + cursor.setPosition(anchor); + cursor.setPosition(pos, QTextCursor::KeepAnchor); + } else { + cursor.insertText(indent); + } + cursor.endEditBlock(); + setTextCursor(cursor); +} + +void TextEdit::handleHomeKey(bool anchor) +{ + QTextCursor cursor = textCursor(); + QTextCursor::MoveMode mode = QTextCursor::MoveAnchor; + + if (anchor) + mode = QTextCursor::KeepAnchor; + + const int initpos = cursor.position(); + int pos = cursor.block().position(); + + while (charAt(pos).category() == QChar::Separator_Space) { + ++pos; + } + + // Go to the start of the block when we're already at the start of the text + if (pos == initpos) + pos = cursor.block().position(); + + cursor.setPosition(pos, mode); + setTextCursor(cursor); +} + +void TextEdit::handleArrowKeys(int key) +{ + QTextCursor cursor = textCursor(); + if (key == Qt::Key_Left) + cursor.setPosition(cursor.selectionStart()); + else if (key == Qt::Key_Right) + cursor.setPosition(cursor.selectionEnd()); + setTextCursor(cursor); +} + + +void TextEdit::format() +{ + QTextCursor cursor = textCursor(); + if (!cursor.hasSelection()) + return; + + const int start = cursor.selectionStart(); + int end = cursor.selectionEnd(); + + const QTextBlock endblock = document()->findBlock(end - 1); + indent(document(), cursor, QChar::Null); + + cursor.setPosition(start); + end = endblock.position() + endblock.length(); + cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, end-start); + setTextCursor(cursor); +} + +void TextEdit::setTabSettings(const TabSettings &ts) +{ + m_tabsettings = ts; +} + +void TextEdit::indentBlock(QTextDocument *, QTextBlock, QChar) +{ +} + +void TextEdit::indent(QTextDocument *doc, const QTextCursor &cursor, QChar typedChar) +{ + if (cursor.hasSelection()) { + QTextBlock block = doc->findBlock(cursor.selectionStart()); + const QTextBlock end = doc->findBlock(cursor.selectionEnd()).next(); + do { + indentBlock(doc, block, typedChar); + block = block.next(); + } while (block.isValid() && block != end); + } else { + indentBlock(doc, cursor.block(), typedChar); + } +} + + +QList TextEdit::bookmarks() const +{ + QList list; + QTextBlock block = document()->begin(); + int blockNumber = 0; + while (block.isValid()) { + if (TextBlockUserData *userData = TextEditDocumentLayout::testUserData(block)) + if (userData->bookmark()) + list += blockNumber; + block = block.next(); + ++blockNumber; + } + return list; +} + +QList TextEdit::breakpoints() const +{ + QList list; + QTextBlock block = document()->begin(); + int blockNumber = 0; + while (block.isValid()) { + if (TextBlockUserData *userData = TextEditDocumentLayout::testUserData(block)) + if (userData->breakpoint()) + list += blockNumber; + block = block.next(); + ++blockNumber; + } + return list; +} + +void TextEdit::setBookmark_helper(int blockNumber, bool b, bool update) +{ + QTextDocument *doc = document(); + TextEditDocumentLayout *documentLayout = qobject_cast(doc->documentLayout()); + Q_ASSERT(documentLayout); + QTextBlock block = doc->findBlockByNumber(blockNumber); + if (block.isValid()) { + TextBlockUserData *userData = TextEditDocumentLayout::testUserData(block); + bool wasSet = userData && userData->bookmark(); + if (b != wasSet) { + if (!userData) + userData = TextEditDocumentLayout::userData(block); + userData->setBookmark(b); + int diff = b ? 1 : -1; + documentLayout->markCount += diff; + if (userData->breakpoint()) + documentLayout->doubleMarkCount += diff; + } + } + if (update) + documentLayout->requestUpdate(); +} + +void TextEdit::setBreakpoint_helper(int blockNumber, bool b, bool update) +{ + QTextDocument *doc = document(); + TextEditDocumentLayout *documentLayout = qobject_cast(doc->documentLayout()); + Q_ASSERT(documentLayout); + QTextBlock block = doc->findBlockByNumber(blockNumber); + + if (block.isValid()) { + TextBlockUserData *userData = TextEditDocumentLayout::testUserData(block); + bool wasSet = userData && userData->breakpoint(); + if (b != wasSet) { + if (!userData) + userData = TextEditDocumentLayout::userData(block); + setPendingBreakpoint_helper(blockNumber, false, false); + userData->setBreakpoint(b); + int diff = b ? 1 : -1; + documentLayout->markCount += diff; + if (userData->bookmark()) + documentLayout->doubleMarkCount += diff; + emit breakpointToggled(blockNumber + baseLine, b); + } + } + if (update) + documentLayout->requestUpdate(); +} + +void TextEdit::setPendingBreakpoint_helper(int blockNumber, bool b, bool update) +{ + QTextDocument *doc = document(); + TextEditDocumentLayout *documentLayout = qobject_cast(doc->documentLayout()); + Q_ASSERT(documentLayout); + QTextBlock block = doc->findBlockByNumber(blockNumber); + + if (block.isValid()) { + TextBlockUserData *userData = TextEditDocumentLayout::testUserData(block); + bool wasSet = userData && userData->pendingBreakpoint(); + if (b != wasSet) { + if (!userData) + userData = TextEditDocumentLayout::userData(block); + setBreakpoint_helper(blockNumber, false, false); + userData->setPendingBreakpoint(b); + int diff = b ? 1 : -1; + documentLayout->markCount += diff; + if (userData->bookmark()) + documentLayout->doubleMarkCount += diff; + } + } + if (update) + documentLayout->requestUpdate(); +} + +void TextEdit::setBookmarks(const QList &blockNumbers) +{ + Q_ASSERT(qobject_cast(document()->documentLayout())); + QTextBlock block = document()->begin(); + while (block.isValid()) { + if (TextBlockUserData *userData = TextEditDocumentLayout::testUserData(block)) + userData->setBookmark(false); + block = block.next(); + } + foreach (int blockNumber, blockNumbers) { + setBookmark_helper(blockNumber, true, blockNumber == blockNumbers.last()); + } +} + + +void TextEdit::setBreakpoints(const QList &blockNumbers) +{ + Q_ASSERT(qobject_cast(document()->documentLayout())); + QTextBlock block = document()->begin(); + while (block.isValid()) { + if (TextBlockUserData *userData = TextEditDocumentLayout::testUserData(block)) + userData->setBreakpoint(false); + block = block.next(); + } + foreach (int blockNumber, blockNumbers) { + setBreakpoint_helper(blockNumber, true, blockNumber == blockNumbers.last()); + } +} + + +void TextEdit::markBlocksAsChanged(QList blockNumbers) { + QTextBlock block = document()->begin(); + while (block.isValid()) { + if (block.revision() < 0) + block.setRevision(-block.revision() - 1); + block = block.next(); + } + foreach(int blockNumber, blockNumbers) { + QTextBlock block = document()->findBlockByNumber(blockNumber); + if (block.isValid()) + block.setRevision(-block.revision() - 1); + } +} + + + +TextBlockUserData::MatchType TextBlockUserData::checkOpenParenthesis(QTextCursor *cursor, QChar c) +{ + if (!TextEditDocumentLayout::hasParentheses(cursor->block())) + return NoMatch; + + Parentheses parenList = TextEditDocumentLayout::parentheses(cursor->block()); + Parenthesis openParen, closedParen; + QTextBlock closedParenParag = cursor->block(); + + const int cursorPos = cursor->position() - closedParenParag.position(); + int i = 0; + int ignore = 0; + bool foundOpen = false; + for (;;) { + if (!foundOpen) { + if (i >= parenList.count()) + return NoMatch; + openParen = parenList.at(i); + if (openParen.pos != cursorPos) { + ++i; + continue; + } else { + foundOpen = true; + ++i; + } + } + + if (i >= parenList.count()) { + for (;;) { + closedParenParag = closedParenParag.next(); + if (!closedParenParag.isValid()) + return NoMatch; + if (TextEditDocumentLayout::hasParentheses(closedParenParag)) { + parenList = TextEditDocumentLayout::parentheses(closedParenParag); + break; + } + } + i = 0; + } + + closedParen = parenList.at(i); + if (closedParen.type == Parenthesis::Opened) { + ignore++; + ++i; + continue; + } else { + if (ignore > 0) { + ignore--; + ++i; + continue; + } + + cursor->clearSelection(); + cursor->setPosition(closedParenParag.position() + closedParen.pos + 1, QTextCursor::KeepAnchor); + + if (c == QLatin1Char('{') && closedParen.chr != QLatin1Char('}') + || c == QLatin1Char('(') && closedParen.chr != QLatin1Char(')') + || c == QLatin1Char('[') && closedParen.chr != QLatin1Char(']') + ) + return Mismatch; + + return Match; + } + } +} + +TextBlockUserData::MatchType TextBlockUserData::checkClosedParenthesis(QTextCursor *cursor, QChar c) +{ + + if (!TextEditDocumentLayout::hasParentheses(cursor->block())) + return NoMatch; + + Parentheses parenList = TextEditDocumentLayout::parentheses(cursor->block()); + Parenthesis openParen, closedParen; + QTextBlock openParenParag = cursor->block(); + + const int cursorPos = cursor->position() - openParenParag.position(); + int i = parenList.count() - 1; + int ignore = 0; + bool foundClosed = false; + for (;;) { + if (!foundClosed) { + if (i < 0) + return NoMatch; + closedParen = parenList.at(i); + if (closedParen.pos != cursorPos - 1) { + --i; + continue; + } else { + foundClosed = true; + --i; + } + } + + if (i < 0) { + for (;;) { + openParenParag = openParenParag.previous(); + if (!openParenParag.isValid()) + return NoMatch; + + if (TextEditDocumentLayout::hasParentheses(openParenParag)) { + parenList = TextEditDocumentLayout::parentheses(openParenParag); + break; + } + } + i = parenList.count() - 1; + } + + openParen = parenList.at(i); + if (openParen.type == Parenthesis::Closed) { + ignore++; + --i; + continue; + } else { + if (ignore > 0) { + ignore--; + --i; + continue; + } + + cursor->clearSelection(); + cursor->setPosition(openParenParag.position() + openParen.pos, QTextCursor::KeepAnchor); + + if ( c == '}' && openParen.chr != '{' || + c == ')' && openParen.chr != '(' || + c == ']' && openParen.chr != '[' ) + return Mismatch; + + return Match; + } + } +} +TextBlockUserData::MatchType TextBlockUserData::matchCursorBackward(QTextCursor *cursor) +{ + cursor->clearSelection(); + const QTextBlock block = cursor->block(); + + if (!TextEditDocumentLayout::hasParentheses(block)) + return NoMatch; + + const int relPos = cursor->position() - block.position(); + + Parentheses parentheses = TextEditDocumentLayout::parentheses(block); + const Parentheses::const_iterator cend = parentheses.constEnd(); + for (Parentheses::const_iterator it = parentheses.constBegin();it != cend; ++it) { + const Parenthesis &paren = *it; + if (paren.pos == relPos - 1 + && paren.type == Parenthesis::Closed) { + return checkClosedParenthesis(cursor, paren.chr); + } + } + return NoMatch; +} + +TextBlockUserData::MatchType TextBlockUserData::matchCursorForward(QTextCursor *cursor) +{ + cursor->clearSelection(); + const QTextBlock block = cursor->block(); + + if (!TextEditDocumentLayout::hasParentheses(block)) + return NoMatch; + + const int relPos = cursor->position() - block.position(); + + Parentheses parentheses = TextEditDocumentLayout::parentheses(block); + const Parentheses::const_iterator cend = parentheses.constEnd(); + for (Parentheses::const_iterator it = parentheses.constBegin();it != cend; ++it) { + const Parenthesis &paren = *it; + if (paren.pos == relPos + && paren.type == Parenthesis::Opened) { + return checkOpenParenthesis(cursor, paren.chr); + } + } + return NoMatch; +} + +void TextEdit::matchParentheses() +{ + if (isReadOnly()) + return; + + QTextCursor backwardMatch = textCursor(); + QTextCursor forwardMatch = textCursor(); + const TextBlockUserData::MatchType backwardMatchType = TextBlockUserData::matchCursorBackward(&backwardMatch); + const TextBlockUserData::MatchType forwardMatchType = TextBlockUserData::matchCursorForward(&forwardMatch); + + if (backwardMatchType == TextBlockUserData::NoMatch && forwardMatchType == TextBlockUserData::NoMatch) + return; + + QList extraSelections = this->extraSelections(); + + if (backwardMatch.hasSelection()) { + QTextEdit::ExtraSelection sel; + if (backwardMatchType == TextBlockUserData::Mismatch) { + sel.cursor = backwardMatch; + sel.format = m_mismatchFormat; + } else { + + if (m_formatRange) { + sel.cursor = backwardMatch; + sel.format = m_rangeFormat; + extraSelections.append(sel); + } + + sel.cursor = backwardMatch; + sel.format = m_matchFormat; + + sel.cursor.setPosition(backwardMatch.selectionStart()); + sel.cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor); + extraSelections.append(sel); + + sel.cursor.setPosition(backwardMatch.selectionEnd()); + sel.cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor); + } + extraSelections.append(sel); + } + + if (forwardMatch.hasSelection()) { + QTextEdit::ExtraSelection sel; + if (forwardMatchType == TextBlockUserData::Mismatch) { + sel.cursor = forwardMatch; + sel.format = m_mismatchFormat; + } else { + + if (m_formatRange) { + sel.cursor = forwardMatch; + sel.format = m_rangeFormat; + extraSelections.append(sel); + } + + sel.cursor = forwardMatch; + sel.format = m_matchFormat; + + sel.cursor.setPosition(forwardMatch.selectionStart()); + sel.cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor); + extraSelections.append(sel); + + sel.cursor.setPosition(forwardMatch.selectionEnd()); + sel.cursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor); + } + extraSelections.append(sel); + } + setExtraSelections(extraSelections); +} + diff --git a/src/editor/textedit.h b/src/editor/textedit.h new file mode 100644 index 0000000..52fe8ef --- /dev/null +++ b/src/editor/textedit.h @@ -0,0 +1,264 @@ +/**************************************************************************** +** +** Copyright (C) 2007-2008 Trolltech ASA. All rights reserved. +** +** This file is part of the Qt Script Debug project on Trolltech Labs. +** +** This file may be used under the terms of the GNU General Public +** License version 2.0 as published by the Free Software Foundation +** and appearing in the file LICENSE.GPL included in the packaging of +** this file. Please review the following information to ensure GNU +** General Public Licensing requirements will be met: +** http://www.trolltech.com/products/qt/opensource.html +** +** If you are unsure which license is appropriate for your use, please +** review the following information: +** http://www.trolltech.com/products/qt/licensing.html or contact the +** sales department at sales@trolltech.com. +** +** This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE +** WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. +** +****************************************************************************/ + +#ifndef TEXTEDIT_H +#define TEXTEDIT_H + +#include +#include "tabsettings.h" +#include +#include +#include +#include +#include + + +struct Parenthesis; +typedef QVector Parentheses; +struct Parenthesis +{ + enum Type { Opened, Closed }; + + inline Parenthesis() : type(Opened), pos(-1) {} + inline Parenthesis(Type t, QChar c, int position) + : type(t), chr(c), pos(position) {} + Type type; + QChar chr; + int pos; + static int collapseAtPos(const Parentheses &parentheses); +}; + +class TextBlockUserData : public QTextBlockUserData { +public: + + enum Collapse { CollapseThis, CollapseAfter, NoCollapse }; + + inline TextBlockUserData() + :m_breakpoint(false), m_pendingBreakpoint(false), m_bookmark(false), + m_collapseIncludesClosure(false), + m_collapse(NoCollapse) {} + inline bool breakpoint() const { return m_breakpoint; } + inline bool pendingBreakpoint() const { return m_pendingBreakpoint; } + inline bool bookmark() const { return m_bookmark; } + inline Collapse collapse() const { return m_collapse; } + inline bool collapseIncludesClosure() const { return m_collapseIncludesClosure; } + inline void setBookmark(bool b) { m_bookmark = b; } + inline void setBreakpoint(bool b){ m_breakpoint = b; } + inline void setPendingBreakpoint(bool b){ m_pendingBreakpoint = b; } + inline void setCollapse(Collapse c) { m_collapse = c; } + inline void setCollapseIncludesClosure(bool b) { m_collapseIncludesClosure = b; } + + inline void setParentheses(const Parentheses &parentheses) { m_parentheses = parentheses; } + inline void clearParentheses() { m_parentheses.clear(); } + inline const Parentheses &parentheses() const { return m_parentheses; } + + inline bool hasParentheses() const { return !m_parentheses.isEmpty(); } + + inline static bool canCollapse(const QTextBlock& block) { + return (block.userData() + && static_cast(block.userData())->collapse() + == CollapseAfter) + || (block.next().userData() + && static_cast(block.next().userData())->collapse() + == TextBlockUserData::CollapseThis); + } + + static void setCollapse(const QTextBlock& block, bool visible); + + bool hasClosingCollapse() const; + int collapseAtPos() const; + + enum MatchType { NoMatch, Match, Mismatch }; + static MatchType checkOpenParenthesis(QTextCursor *cursor, QChar c); + static MatchType checkClosedParenthesis(QTextCursor *cursor, QChar c); + static MatchType matchCursorBackward(QTextCursor *cursor); + static MatchType matchCursorForward(QTextCursor *cursor); + + + +private: + bool m_breakpoint; + bool m_pendingBreakpoint; + bool m_bookmark; + bool m_collapseIncludesClosure; + Collapse m_collapse; + Parentheses m_parentheses; +}; + +class TextEditDocumentLayout : public QPlainTextDocumentLayout +{ + Q_OBJECT + +public: + + TextEditDocumentLayout(QTextDocument *doc):QPlainTextDocumentLayout(doc) { + lastSaveRevision = markCount = doubleMarkCount = 0; + m_executionBlockNumber = -1; + } + ~TextEditDocumentLayout(){} + + QRectF blockBoundingRect(const QTextBlock &block) const; + + static void setParentheses(const QTextBlock &block, const Parentheses &parentheses); + static void clearParentheses(const QTextBlock &block) { setParentheses(block, Parentheses());} + static Parentheses parentheses(const QTextBlock &block); + static bool hasParentheses(const QTextBlock &block); + + static TextBlockUserData *testUserData(const QTextBlock &block) { + return static_cast(block.userData()); + } + static TextBlockUserData *userData(const QTextBlock &block) { + TextBlockUserData *data = static_cast(block.userData()); + if (!data && block.isValid()) + const_cast(block).setUserData((data = new TextBlockUserData)); + return data; + } + + inline void setExecutionBlockNumber(int blockNumber) { + m_executionBlockNumber = blockNumber; + requestUpdate(); + } + + inline int executionBlockNumber() const { return m_executionBlockNumber; } + + int lastSaveRevision; + int markCount; + int doubleMarkCount; + + int m_executionBlockNumber; +}; + +class TextEdit : public QPlainTextEdit +{ + Q_OBJECT +public: + explicit TextEdit(QWidget *parent = 0); + ~TextEdit(); + + QWidget *extraArea() const; + virtual int extraAreaWidth(int *markWidthPtr = 0) const; + virtual void extraAreaPaintEvent(QPaintEvent *); + virtual void extraAreaMouseEvent(QMouseEvent *); + + + inline const TabSettings &tabSettings() const { return m_tabsettings; } + + QList bookmarks() const; + void setBookmarks(const QList &blockNumbers); + QList breakpoints() const; + void setBreakpoints(const QList &blockNumbers); + QList pendingBreakpoints() const; + void setPendingBreakpoints(const QList &blockNumbers); + + + void markBlocksAsChanged(QList blockNumbers); + + int executionBlockNumber() const; + void setExecutionBlockNumber(int blockNumber); + + int baseLineNumber() const { return baseLine; } + void setBaseLineNumber(int lineNumber) { baseLine = lineNumber; } + int cursorLineNumber() const { return textCursor().blockNumber() + baseLine; } + +public Q_SLOTS: + void setTabSettings(const TabSettings &); + virtual void format(); + +Q_SIGNALS: + void breakpointToggled(int lineNumber, bool set); + +protected: + + bool viewportEvent(QEvent *event); + QChar charAt(int pos) const; + + void resizeEvent(QResizeEvent *); + void paintEvent(QPaintEvent *); + void keyPressEvent(QKeyEvent *); + void timerEvent(QTimerEvent *); + void mouseMoveEvent(QMouseEvent *); + void mousePressEvent(QMouseEvent *); + + // Indent a text block based on previous line. Default does nothing + virtual void indentBlock(QTextDocument *doc, QTextBlock block, QChar typedChar); + // Indent at cursor. Calls indentBlock for selection or current line. + virtual void indent(QTextDocument *doc, const QTextCursor &cursor, QChar typedChar); + + +protected Q_SLOTS: + virtual void slotUpdateExtraAreaWidth(); + virtual void slotModificationChanged(bool); + virtual void slotUpdateRequest(const QRect &r, int dy); + virtual void slotCursorPositionChanged(); + +public slots: + void slotToggleBookmark(); + void slotToggleBreakpoint(); + void slotTogglePendingBreakpoint(); + void slotToggleBlockVisible(); + void slotSetExecutionBlock(); +private: + + void setBookmark_helper(int blockNumber, bool b = true, bool update = true); + void setBreakpoint_helper(int blockNumber, bool b = true, bool update = true); + void setPendingBreakpoint_helper(int blockNumber, bool b = true, bool update = true); + + void indentSelection(); + void handleHomeKey(bool anchor); + void handleArrowKeys(int key); + void unIndentSelection(); + + void toggleBlockVisible(const QTextBlock &block); + QRect collapseBox(const QTextBlock &block); + + QTextBlock collapsedBlockAt(const QPoint &pos, QString *text = 0, QRect *box = 0) const; + + // parentheses matcher +private slots: + void matchParentheses(); +private: + bool m_formatRange; + QTextCharFormat m_matchFormat; + QTextCharFormat m_mismatchFormat; + QTextCharFormat m_rangeFormat; + QTimer *m_parenthesesMatchingTimer; + // end parentheses matcher + + + QWidget *m_extraArea; + TabSettings m_tabsettings; + QIcon executionIcon; + QIcon bookmarkIcon; + QIcon breakpointIcon; + QIcon breakpointPendingIcon; + + int extraAreaSelectionAnchorBlockNumber; + int extraAreaToggleBreakpointBlockNumber; + + QBasicTimer autoScrollTimer; + + int baseLine; +}; + + +#endif // CPPEDIT_H -- 1.7.1