qmljshighlighter.cpp 13.4 KB
Newer Older
1
/**************************************************************************
con's avatar
con committed
2 3 4
**
** This file is part of Qt Creator
**
con's avatar
con committed
5
** Copyright (c) 2011 Nokia Corporation and/or its subsidiary(-ies).
con's avatar
con committed
6
**
hjk's avatar
hjk committed
7
** Contact: Nokia Corporation (info@qt.nokia.com)
con's avatar
con committed
8
**
9
**
10
** GNU Lesser General Public License Usage
11
**
hjk's avatar
hjk committed
12 13 14 15 16 17
** This file may be used under the terms of the GNU Lesser General Public
** License version 2.1 as published by the Free Software Foundation and
** appearing in the file LICENSE.LGPL included in the packaging of this file.
** Please review the following information to ensure the GNU Lesser General
** Public License version 2.1 requirements will be met:
** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
18
**
con's avatar
con committed
19
** In addition, as a special exception, Nokia gives you certain additional
hjk's avatar
hjk committed
20
** rights. These rights are described in the Nokia Qt LGPL Exception
con's avatar
con committed
21 22
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
**
hjk's avatar
hjk committed
23 24 25 26 27
** Other Usage
**
** Alternatively, this file may be used in accordance with the terms and
** conditions contained in a signed written agreement between you and Nokia.
**
con's avatar
con committed
28
** If you have questions regarding the use of this file, please contact
Tobias Hunger's avatar
Tobias Hunger committed
29
** Nokia at info@qt.nokia.com.
con's avatar
con committed
30
**
31
**************************************************************************/
hjk's avatar
hjk committed
32

33
#include "qmljshighlighter.h"
con's avatar
con committed
34 35 36

#include <QtCore/QSet>
#include <QtCore/QtAlgorithms>
37
#include <QtCore/QDebug>
con's avatar
con committed
38

39 40 41
#include <utils/qtcassert.h>

using namespace QmlJSEditor;
42
using namespace QmlJS;
con's avatar
con committed
43

44
Highlighter::Highlighter(QTextDocument *parent)
45
    : TextEditor::SyntaxHighlighter(parent),
46 47
      m_qmlEnabled(true),
      m_inMultilineComment(false)
48 49 50
{
    m_currentBlockParentheses.reserve(20);
    m_braceDepth = 0;
mae's avatar
mae committed
51
    m_foldingIndent = 0;
52 53 54
}

Highlighter::~Highlighter()
55 56 57
{
}

58
bool Highlighter::isQmlEnabled() const
59
{
60
    return m_qmlEnabled;
61 62
}

63 64 65 66 67
void Highlighter::setQmlEnabled(bool qmlEnabled)
{
    m_qmlEnabled = qmlEnabled;
}

68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
static bool checkStartOfBinding(const Token &token)
{
    switch (token.kind) {
    case Token::Semicolon:
    case Token::LeftBrace:
    case Token::RightBrace:
    case Token::LeftBracket:
    case Token::RightBracket:
        return true;

    default:
        return false;
    } // end of switch
}

83 84 85 86 87 88
void Highlighter::setFormats(const QVector<QTextCharFormat> &formats)
{
    QTC_ASSERT(formats.size() == NumFormats, return);
    qCopy(formats.begin(), formats.end(), m_formats);
}

89
void Highlighter::highlightBlock(const QString &text)
con's avatar
con committed
90
{
91
    const QList<Token> tokens = m_scanner(text, onBlockStart());
con's avatar
con committed
92

93 94 95
    int index = 0;
    while (index < tokens.size()) {
        const Token &token = tokens.at(index);
96

97
        switch (token.kind) {
98
            case Token::Keyword:
99 100
                setFormat(token.offset, token.length, m_formats[KeywordFormat]);
                break;
con's avatar
con committed
101

Roberto Raggi's avatar
Roberto Raggi committed
102 103 104 105 106
            case Token::String:
                setFormat(token.offset, token.length, m_formats[StringFormat]);
                break;

            case Token::Comment:
107 108 109 110 111 112 113 114 115
                if (m_inMultilineComment && text.midRef(token.end() - 2, 2) == QLatin1String("*/")) {
                    onClosingParenthesis('-', token.end() - 1, index == tokens.size()-1);
                    m_inMultilineComment = false;
                } else if (!m_inMultilineComment
                           && m_scanner.state() == Scanner::MultiLineComment
                           && index == tokens.size() - 1) {
                    onOpeningParenthesis('+', token.offset, index == 0);
                    m_inMultilineComment = true;
                }
Roberto Raggi's avatar
Roberto Raggi committed
116 117 118
                setFormat(token.offset, token.length, m_formats[CommentFormat]);
                break;

119 120 121 122
            case Token::RegExp:
                setFormat(token.offset, token.length, m_formats[StringFormat]);
                break;

123
            case Token::LeftParenthesis:
mae's avatar
mae committed
124
                onOpeningParenthesis('(', token.offset, index == 0);
125
                break;
126

127
            case Token::RightParenthesis:
mae's avatar
mae committed
128
                onClosingParenthesis(')', token.offset, index == tokens.size()-1);
129
                break;
130

131
            case Token::LeftBrace:
mae's avatar
mae committed
132
                onOpeningParenthesis('{', token.offset, index == 0);
133
                break;
134

135
            case Token::RightBrace:
mae's avatar
mae committed
136
                onClosingParenthesis('}', token.offset, index == tokens.size()-1);
137
                break;
138

139
            case Token::LeftBracket:
mae's avatar
mae committed
140
                onOpeningParenthesis('[', token.offset, index == 0);
141
                break;
142

143
            case Token::RightBracket:
mae's avatar
mae committed
144
                onClosingParenthesis(']', token.offset, index == tokens.size()-1);
145
                break;
146

147
            case Token::Identifier: {
148 149 150
                if (!m_qmlEnabled)
                    break;

Roberto Raggi's avatar
Roberto Raggi committed
151 152
                const QStringRef spell = text.midRef(token.offset, token.length);

153
                if (maybeQmlKeyword(spell)) {
154 155 156 157 158 159 160
                    // check the previous token
                    if (index == 0 || tokens.at(index - 1).isNot(Token::Dot)) {
                        if (index + 1 == tokens.size() || tokens.at(index + 1).isNot(Token::Colon)) {
                            setFormat(token.offset, token.length, m_formats[KeywordFormat]);
                            break;
                        }
                    }
161
                } else if (index > 0 && maybeQmlBuiltinType(spell)) {
Roberto Raggi's avatar
Roberto Raggi committed
162 163 164 165 166 167
                    const Token &previousToken = tokens.at(index - 1);
                    if (previousToken.is(Token::Identifier) && text.at(previousToken.offset) == QLatin1Char('p')
                        && text.midRef(previousToken.offset, previousToken.length) == QLatin1String("property")) {
                        setFormat(token.offset, token.length, m_formats[KeywordFormat]);
                        break;
                    }
168 169
                }

170 171
                if (!spell.isEmpty() && spell.at(0).isUpper())
                    setFormat(token.offset, token.length, m_formats[TypeFormat]);
172

173
                if (index + 1 < tokens.size()) {
174 175 176 177 178 179 180 181 182 183
                    bool maybeBinding = (index == 0 || checkStartOfBinding(tokens.at(index - 1)));
                    bool maybeOnBinding = false;
                    if (index > 0) {
                        const Token &previousToken = tokens.at(index - 1);
                        if (text.midRef(previousToken.offset, previousToken.length) == QLatin1String("on")) {
                            maybeOnBinding = true;
                            maybeBinding = false;
                        }
                    }

184
                    if (maybeBinding || maybeOnBinding) {
185 186 187 188
                        Token::Kind expectedTerminator = Token::Colon;
                        if (maybeOnBinding)
                            expectedTerminator = Token::LeftBrace;

189 190
                        const int start = index;

191 192 193 194
                        // put index on last identifier not followed by .identifier
                        while (index + 2 < tokens.size() &&
                               tokens.at(index + 1).is(Token::Dot) &&
                               tokens.at(index + 2).is(Token::Identifier)) {
195 196 197
                            index += 2;
                        }

198
                        if (index + 1 < tokens.size() && tokens.at(index + 1).is(expectedTerminator)) {
199
                            // it's a binding.
200
                            for (int i = start; i <= index; ++i) {
201
                                const Token &tok = tokens.at(i);
202 203 204 205 206 207 208 209
                                if (tok.kind == Token::Dot)
                                    continue;
                                const QStringRef tokSpell = text.midRef(tok.offset, tok.length);
                                if (!tokSpell.isEmpty() && tokSpell.at(0).isUpper()) {
                                    setFormat(tok.offset, tok.length, m_formats[TypeFormat]);
                                } else {
                                    setFormat(tok.offset, tok.length, m_formats[FieldFormat]);
                                }
210
                            }
211
                            break;
212 213
                        } else {
                            index = start;
214 215
                        }
                    }
216
                }
217
            }   break;
218

219
            case Token::Delimiter:
220
                break;
221

222 223
            default:
                break;
224
        } // end swtich
225

226
        ++index;
con's avatar
con committed
227 228
    }

229
    int previousTokenEnd = 0;
230 231
    for (int index = 0; index < tokens.size(); ++index) {
        const Token &token = tokens.at(index);
232
        setFormat(previousTokenEnd, token.begin() - previousTokenEnd, m_formats[VisualWhitespace]);
233 234 235

        switch (token.kind) {
        case Token::Comment:
236 237
        case Token::String:
        case Token::RegExp: {
238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256
            int i = token.begin(), e = token.end();
            while (i < e) {
                const QChar ch = text.at(i);
                if (ch.isSpace()) {
                    const int start = i;
                    do {
                        ++i;
                    } while (i < e && text.at(i).isSpace());
                    setFormat(start, i - start, m_formats[VisualWhitespace]);
                } else {
                    ++i;
                }
            }
        } break;

        default:
            break;
        } // end of switch

257 258
        previousTokenEnd = token.end();
    }
259

Roberto Raggi's avatar
Roberto Raggi committed
260 261
    setFormat(previousTokenEnd, text.length() - previousTokenEnd, m_formats[VisualWhitespace]);

262
    setCurrentBlockState(m_scanner.state());
mae's avatar
mae committed
263
    onBlockEnd(m_scanner.state());
con's avatar
con committed
264 265
}

266
bool Highlighter::maybeQmlKeyword(const QStringRef &text) const
267
{
268 269
    if (text.isEmpty())
        return false;
270

271 272 273 274 275 276 277 278 279 280 281
    const QChar ch = text.at(0);
    if (ch == QLatin1Char('p') && text == QLatin1String("property")) {
        return true;
    } else if (ch == QLatin1Char('a') && text == QLatin1String("alias")) {
        return true;
    } else if (ch == QLatin1Char('s') && text == QLatin1String("signal")) {
        return true;
    } else if (ch == QLatin1Char('p') && text == QLatin1String("property")) {
        return true;
    } else if (ch == QLatin1Char('r') && text == QLatin1String("readonly")) {
        return true;
Roberto Raggi's avatar
Roberto Raggi committed
282 283
    } else if (ch == QLatin1Char('i') && text == QLatin1String("import")) {
        return true;
284 285
    } else if (ch == QLatin1Char('o') && text == QLatin1String("on")) {
        return true;
286 287
    } else {
        return false;
288 289
    }
}
Roberto Raggi's avatar
Roberto Raggi committed
290

291
bool Highlighter::maybeQmlBuiltinType(const QStringRef &text) const
Roberto Raggi's avatar
Roberto Raggi committed
292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321
{
    if (text.isEmpty())
        return false;

    const QChar ch = text.at(0);

    if (ch == QLatin1Char('i') && text == QLatin1String("int")) {
        return true;
    } else if (ch == QLatin1Char('b') && text == QLatin1String("bool")) {
        return true;
    } else if (ch == QLatin1Char('d') && text == QLatin1String("double")) {
        return true;
    } else if (ch == QLatin1Char('r') && text == QLatin1String("real")) {
        return true;
    } else if (ch == QLatin1Char('s') && text == QLatin1String("string")) {
        return true;
    } else if (ch == QLatin1Char('u') && text == QLatin1String("url")) {
        return true;
    } else if (ch == QLatin1Char('c') && text == QLatin1String("color")) {
        return true;
    } else if (ch == QLatin1Char('d') && text == QLatin1String("date")) {
        return true;
    } else if (ch == QLatin1Char('v') && text == QLatin1String("var")) {
        return true;
    } else if (ch == QLatin1Char('v') && text == QLatin1String("variant")) {
        return true;
    } else {
        return false;
    }
}
322 323 324 325 326

int Highlighter::onBlockStart()
{
    m_currentBlockParentheses.clear();
    m_braceDepth = 0;
mae's avatar
mae committed
327
    m_foldingIndent = 0;
328
    m_inMultilineComment = false;
mae's avatar
mae committed
329 330 331 332 333
    if (TextEditor::TextBlockUserData *userData = TextEditor::BaseTextDocumentLayout::testUserData(currentBlock())) {
        userData->setFoldingIndent(0);
        userData->setFoldingStartIncluded(false);
        userData->setFoldingEndIncluded(false);
    }
334 335 336 337 338

    int state = 0;
    int previousState = previousBlockState();
    if (previousState != -1) {
        state = previousState & 0xff;
339
        m_braceDepth = (previousState >> 8);
340
        m_inMultilineComment = (state == Scanner::MultiLineComment);
341
    }
mae's avatar
mae committed
342
    m_foldingIndent = m_braceDepth;
343 344 345 346

    return state;
}

mae's avatar
mae committed
347
void Highlighter::onBlockEnd(int state)
348 349 350
{
    typedef TextEditor::TextBlockUserData TextEditorBlockData;

351
    setCurrentBlockState((m_braceDepth << 8) | state);
mae's avatar
mae committed
352 353
    TextEditor::BaseTextDocumentLayout::setParentheses(currentBlock(), m_currentBlockParentheses);
    TextEditor::BaseTextDocumentLayout::setFoldingIndent(currentBlock(), m_foldingIndent);
354 355
}

mae's avatar
mae committed
356
void Highlighter::onOpeningParenthesis(QChar parenthesis, int pos, bool atStart)
357
{
358
    if (parenthesis == QLatin1Char('{') || parenthesis == QLatin1Char('[') || parenthesis == QLatin1Char('+')) {
359
        ++m_braceDepth;
mae's avatar
mae committed
360 361 362 363 364
        // if a folding block opens at the beginning of a line, treat the entire line
        // as if it were inside the folding block
        if (atStart)
            TextEditor::BaseTextDocumentLayout::userData(currentBlock())->setFoldingStartIncluded(true);
    }
365 366 367
    m_currentBlockParentheses.push_back(Parenthesis(Parenthesis::Opened, parenthesis, pos));
}

mae's avatar
mae committed
368
void Highlighter::onClosingParenthesis(QChar parenthesis, int pos, bool atEnd)
369
{
370
    if (parenthesis == QLatin1Char('}') || parenthesis == QLatin1Char(']') || parenthesis == QLatin1Char('-')) {
371
        --m_braceDepth;
mae's avatar
mae committed
372 373 374 375 376
        if (atEnd)
            TextEditor::BaseTextDocumentLayout::userData(currentBlock())->setFoldingEndIncluded(true);
        else
            m_foldingIndent = qMin(m_braceDepth, m_foldingIndent); // folding indent is the minimum brace depth of a block
    }
377 378 379
    m_currentBlockParentheses.push_back(Parenthesis(Parenthesis::Closed, parenthesis, pos));
}