Skip to content
Snippets Groups Projects
  • Christian Kamm's avatar
    a7e5f80d
    QmlJS indenter: Only auto-reindent if indent was unchanged. · a7e5f80d
    Christian Kamm authored
    This change in how electric characters are handled has gone into the
    C++ indenter a while ago and works well there. It means Creator is less
    likely to annoyingly change the indent on lines where the
    indentation whas changed manually.
    It is still possible to trigger a reindent manually.
    
    Reviewed-by: Erik Verbruggen
    a7e5f80d
    History
    QmlJS indenter: Only auto-reindent if indent was unchanged.
    Christian Kamm authored
    This change in how electric characters are handled has gone into the
    C++ indenter a while ago and works well there. It means Creator is less
    likely to annoyingly change the indent on lines where the
    indentation whas changed manually.
    It is still possible to trigger a reindent manually.
    
    Reviewed-by: Erik Verbruggen
qmljscodeformatter.cpp 27.00 KiB
/**************************************************************************
**
** This file is part of Qt Creator
**
** Copyright (c) 2010 Nokia Corporation and/or its subsidiary(-ies).
**
** Contact: Nokia Corporation (qt-info@nokia.com)
**
** No Commercial Usage
**
** This file contains pre-release code and may not be distributed.
** You may use this file in accordance with the terms and conditions
** contained in the Technology Preview License Agreement accompanying
** this package.
**
** GNU Lesser General Public License Usage
**
** Alternatively, 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.
**
** In addition, as a special exception, Nokia gives you certain additional
** rights.  These rights are described in the Nokia Qt LGPL Exception
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
**
** If you have questions regarding the use of this file, please contact
** Nokia at qt-info@nokia.com.
**
**************************************************************************/

#include "qmljscodeformatter.h"

#include <QtCore/QDebug>
#include <QtCore/QMetaEnum>
#include <QtGui/QTextDocument>
#include <QtGui/QTextCursor>
#include <QtGui/QTextBlock>

using namespace QmlJS;

CodeFormatter::BlockData::BlockData()
    : m_blockRevision(-1)
{
}

CodeFormatter::CodeFormatter()
    : m_indentDepth(0)
    , m_tabSize(4)
{
}

CodeFormatter::~CodeFormatter()
{
}

void CodeFormatter::setTabSize(int tabSize)
{
    m_tabSize = tabSize;
}

void CodeFormatter::recalculateStateAfter(const QTextBlock &block)
{
    restoreCurrentState(block.previous());

    const int lexerState = tokenizeBlock(block);
    m_tokenIndex = 0;
    m_newStates.clear();

    //qDebug() << "Starting to look at " << block.text() << block.blockNumber() + 1;

    for (; m_tokenIndex < m_tokens.size(); ) {
        m_currentToken = tokenAt(m_tokenIndex);
        const int kind = extendedTokenKind(m_currentToken);

        //qDebug() << "Token" << m_currentLine.mid(m_currentToken.begin(), m_currentToken.length) << m_tokenIndex << "in line" << block.blockNumber() + 1;
        //dump();

        if (kind == Comment
                && state().type != multiline_comment_cont
                && state().type != multiline_comment_start) {
            m_tokenIndex += 1;
            continue;
        }

        switch (m_currentState.top().type) {
        case topmost_intro:
            switch (kind) {
            case Identifier:    enter(objectdefinition_or_js); continue;
            case Import:        enter(top_qml); continue;
            default:            enter(top_js); continue;
            } break;

        case top_qml:
            switch (kind) {
            case Import:        enter(import_start); break;
            case Identifier:    enter(binding_or_objectdefinition); break;
            } break;

        case top_js:
            tryStatement();
            break;

        case objectdefinition_or_js:
            switch (kind) {
            case Dot:           break;
            case Identifier:
                if (!m_currentLine.at(m_currentToken.begin()).isUpper()) {
                    turnInto(top_js);
                    continue;
                }
                break;
            case LeftBrace:     turnInto(binding_or_objectdefinition); continue;
            default:            turnInto(top_js); continue;
            } break;

        case import_start:
            enter(import_maybe_dot_or_version_or_as);
            break;

        case import_maybe_dot_or_version_or_as:
            switch (kind) {
            case Dot:           turnInto(import_dot); break;
            case As:            turnInto(import_as); break;
            case Number:        turnInto(import_maybe_as); break;
            default:            leave(); leave(); continue;
            } break;

        case import_maybe_as:
            switch (kind) {
            case As:            turnInto(import_as); break;
            default:            leave(); leave(); continue;
            } break;

        case import_dot:
            switch (kind) {
            case Identifier:    turnInto(import_maybe_dot_or_version_or_as); break;
            default:            leave(); leave(); continue;
            } break;

        case import_as:
            switch (kind) {
            case Identifier:    leave(); leave(); break;
            } break;

        case binding_or_objectdefinition:
            switch (kind) {
            case Colon:         enter(binding_assignment); break;
            case LeftBrace:     enter(objectdefinition_open); break;
            } break;

        case binding_assignment:
            switch (kind) {
            case Semicolon:     leave(true); break;
            case If:            enter(if_statement); break;
            case LeftBrace:     enter(jsblock_open); break;
            case On:
            case As:
            case List:
            case Import:
            case Signal:
            case Property:
            case Identifier:    enter(expression_or_objectdefinition); break;
            default:            enter(expression); continue;
            } break;

        case objectdefinition_open:
            switch (kind) {
            case RightBrace:    leave(true); break;
            case Default:       enter(default_property_start); break;
            case Property:      enter(property_start); break;
            case Function:      enter(function_start); break;
            case Signal:        enter(signal_start); break;
            case On:
            case As:
            case List:
            case Import:
            case Identifier:    enter(binding_or_objectdefinition); break;
            } break;

        case default_property_start:
            if (kind != Property)
                leave(true);
            else
                turnInto(property_start);
            break;

        case property_start:
            switch (kind) {
            case Colon:         enter(binding_assignment); break; // oops, was a binding
            case Var:
            case Identifier:    enter(property_type); break;
            case List:          enter(property_list_open); break;
            default:            leave(true); continue;
            } break;

        case property_type:
            turnInto(property_maybe_initializer);
            break;

        case property_list_open:
            if (m_currentLine.midRef(m_currentToken.begin(), m_currentToken.length) == QLatin1String(">"))
                turnInto(property_maybe_initializer);
            break;

        case property_maybe_initializer:
            switch (kind) {
            case Colon:         enter(binding_assignment); break;
            default:            leave(true); continue;
            } break;

        case signal_start:
            switch (kind) {
            case Colon:         enter(binding_assignment); break; // oops, was a binding
            default:            enter(signal_maybe_arglist); break;
            } break;

        case signal_maybe_arglist:
            switch (kind) {
            case LeftParenthesis:   turnInto(signal_arglist_open); break;
            default:                leave(true); continue;
            } break;

        case signal_arglist_open:
            switch (kind) {
            case RightParenthesis:  leave(true); break;
            } break;

        case function_start:
            switch (kind) {
            case LeftParenthesis:   enter(function_arglist_open); break;
            } break;

        case function_arglist_open:
            switch (kind) {
            case RightParenthesis:  turnInto(function_arglist_closed); break;
            } break;

        case function_arglist_closed:
            switch (kind) {
            case LeftBrace:         turnInto(jsblock_open); break;
            default:                leave(true); continue; // error recovery
            } break;

        case expression_or_objectdefinition:
            switch (kind) {
            case LeftBrace:     turnInto(objectdefinition_open); break;
            default:            enter(expression); continue; // really? first token already gone!
            } break;

        case expression:
            if (tryInsideExpression())
                break;
            switch (kind) {
            case Comma:
            case Delimiter:         enter(expression_continuation); break;
            case RightBracket:
            case RightParenthesis:  leave(); continue;
            case RightBrace:        leave(true); continue;
            case Semicolon:         leave(true); break;
            } break;

        case expression_continuation:
            leave();
            continue;

        case expression_maybe_continuation:
            switch (kind) {
            case Question:
            case Delimiter:
            case LeftBracket:
            case LeftParenthesis:  leave(); continue;
            default:               leave(true); continue;
            } break;

        case paren_open:
            if (tryInsideExpression())
                break;
            switch (kind) {
            case RightParenthesis:  leave(); break;
            } break;

        case bracket_open:
            if (tryInsideExpression())
                break;
            switch (kind) {
            case Comma:             enter(bracket_element_start); break;
            case RightBracket:      leave(); break;
            } break;

        case objectliteral_open:
            if (tryInsideExpression())
                break;
            switch (kind) {
            case RightBrace:        leave(); break;
            } break;

        case bracket_element_start:
            switch (kind) {
            case Identifier:        turnInto(bracket_element_maybe_objectdefinition); break;
            default:                leave(); continue;
            } break;

        case bracket_element_maybe_objectdefinition:
            switch (kind) {
            case LeftBrace:         turnInto(objectdefinition_open); break;
            default:                leave(); continue;
            } break;

        case ternary_op:
            if (tryInsideExpression())
                break;
            switch (kind) {
            case RightParenthesis:
            case RightBracket:
            case RightBrace:
            case Comma:
            case Semicolon:         leave(); continue;
            case Colon:             enter(expression); break; // entering expression makes maybe_continuation work
            } break;

        case jsblock_open:
        case substatement_open:
            if (tryStatement())
                break;
            switch (kind) {
            case RightBrace:        leave(true); break;
            } break;

        case substatement:
            // prefer substatement_open over block_open
            if (kind != LeftBrace) {
                if (tryStatement())
                    break;
            }
            switch (kind) {
            case LeftBrace:         turnInto(substatement_open); break;
            } break;

        case if_statement:
            switch (kind) {
            case LeftParenthesis:   enter(condition_open); break;
            default:                leave(true); break; // error recovery
            } break;

        case maybe_else:
            if (kind == Else) {
                turnInto(else_clause);
                enter(substatement);
                break;
            } else {
                leave(true);
                continue;
            }

        case else_clause:
            // ### shouldn't happen
            dump();
            Q_ASSERT(false);
            leave(true);
            break;

        case condition_open:
            switch (kind) {
            case RightParenthesis:  turnInto(substatement); break;
            case LeftParenthesis:   enter(condition_paren_open); break;
            } break;

        // paren nesting
        case condition_paren_open:
            switch (kind) {
            case RightParenthesis:  leave(); break;
            case LeftParenthesis:   enter(condition_paren_open); break;
            } break;

        case switch_statement:
        case statement_with_condition:
            switch (kind) {
            case LeftParenthesis:   enter(statement_with_condition_paren_open); break;
            default:                leave(true);
            } break;

        case statement_with_condition_paren_open:
            if (tryInsideExpression())
                break;
            switch (kind) {
            case RightParenthesis:  turnInto(substatement); break;
            } break;

        case statement_with_block:
            switch (kind) {
            case LeftBrace:         enter(jsblock_open); break;
            default:                leave(true); break;
            } break;

        case do_statement:
            switch (kind) {
            case While:             break;
            case LeftParenthesis:   enter(do_statement_while_paren_open); break;
            default:                leave(true); break;
            } break;

        case do_statement_while_paren_open:
            if (tryInsideExpression())
                break;
            switch (kind) {
            case RightParenthesis:  leave(); leave(true); break;
            } break;

            break;

        case case_start:
            switch (kind) {
            case Colon:             turnInto(case_cont); break;
            } break;

        case case_cont:
            if (kind != Case && kind != Default && tryStatement())
                break;
            switch (kind) {
            case RightBrace:        leave(); continue;
            case Default:
            case Case:              leave(); continue;
            } break;

        case multiline_comment_start:
        case multiline_comment_cont:
            if (kind != Comment) {
                leave();
                continue;
            } else if (m_tokenIndex == m_tokens.size() - 1
                       && lexerState == Scanner::Normal) {
                leave();
            } else if (m_tokenIndex == 0) {
                // to allow enter/leave to update the indentDepth
                turnInto(multiline_comment_cont);
            }
            break;

        default:
            qWarning() << "Unhandled state" << m_currentState.top().type;
            break;
        } // end of state switch

        ++m_tokenIndex;
    }

    int topState = m_currentState.top().type;

    if (topState == expression
            || topState == expression_or_objectdefinition) {
        enter(expression_maybe_continuation);
    }
    if (topState != multiline_comment_start
            && topState != multiline_comment_cont
            && lexerState == Scanner::MultiLineComment) {
        enter(multiline_comment_start);
    }

    saveCurrentState(block);
}

int CodeFormatter::indentFor(const QTextBlock &block)
{
//    qDebug() << "indenting for" << block.blockNumber() + 1;

    restoreCurrentState(block.previous());
    correctIndentation(block);
    return m_indentDepth;
}

int CodeFormatter::indentForNewLineAfter(const QTextBlock &block)
{
    restoreCurrentState(block);

    int lexerState = loadLexerState(block);
    m_tokens.clear();
    m_currentLine.clear();
    adjustIndent(m_tokens, lexerState, &m_indentDepth);

    return m_indentDepth;
}

void CodeFormatter::updateStateUntil(const QTextBlock &endBlock)
{
    QStack<State> previousState = initialState();
    QTextBlock it = endBlock.document()->firstBlock();
    // find the first block that needs recalculation
    for (; it.isValid() && it != endBlock; it = it.next()) {
        BlockData blockData;
        if (!loadBlockData(it, &blockData))
            break;
        if (blockData.m_blockRevision != it.revision())
            break;
        if (previousState != blockData.m_beginState)
            break;
        if (loadLexerState(it) == -1)
            break;

        previousState = blockData.m_endState;
    }

    if (it == endBlock)
        return;

    // update everthing until endBlock
    for (; it.isValid() && it != endBlock; it = it.next()) {
        recalculateStateAfter(it);
    }

    // invalidate everything below by marking the state in endBlock as invalid
    if (it.isValid()) {
        BlockData invalidBlockData;
        saveBlockData(&it, invalidBlockData);
    }
}

void CodeFormatter::updateLineStateChange(const QTextBlock &block)
{
    if (!block.isValid())
        return;

    BlockData blockData;
    if (loadBlockData(block, &blockData) && blockData.m_blockRevision == block.revision())
        return;

    recalculateStateAfter(block);

    // invalidate everything below by marking the next block's state as invalid
    QTextBlock next = block.next();
    if (!next.isValid())
        return;

    saveBlockData(&next, BlockData());
}

CodeFormatter::State CodeFormatter::state(int belowTop) const
{
    if (belowTop < m_currentState.size())
        return m_currentState.at(m_currentState.size() - 1 - belowTop);
    else
        return State();
}

const QVector<CodeFormatter::State> &CodeFormatter::newStatesThisLine() const
{
    return m_newStates;
}

int CodeFormatter::tokenIndex() const
{
    return m_tokenIndex;
}

int CodeFormatter::tokenCount() const
{
    return m_tokens.size();
}

const Token &CodeFormatter::currentToken() const
{
    return m_currentToken;
}

void CodeFormatter::invalidateCache(QTextDocument *document)
{
    if (!document)
        return;

    BlockData invalidBlockData;
    QTextBlock it = document->firstBlock();
    for (; it.isValid(); it = it.next()) {
        saveBlockData(&it, invalidBlockData);
    }
}

void CodeFormatter::enter(int newState)
{
    int savedIndentDepth = m_indentDepth;
    onEnter(newState, &m_indentDepth, &savedIndentDepth);
    State s(newState, savedIndentDepth);
    m_currentState.push(s);
    m_newStates.push(s);

    if (newState == bracket_open)
        enter(bracket_element_start);
}

void CodeFormatter::leave(bool statementDone)
{
    Q_ASSERT(m_currentState.size() > 1);
    if (m_currentState.top().type == topmost_intro)
        return;

    if (m_newStates.size() > 0)
        m_newStates.pop();

    // restore indent depth
    State poppedState = m_currentState.pop();
    m_indentDepth = poppedState.savedIndentDepth;

    int topState = m_currentState.top().type;

    // if statement is done, may need to leave recursively
    if (statementDone) {
        if (!isExpressionEndState(topState))
            leave(true);
        if (topState == if_statement) {
            if (poppedState.type != maybe_else)
                enter(maybe_else);
            else
                leave(true);
        } else if (topState == else_clause) {
            // leave the else *and* the surrounding if, to prevent another else
            leave();
            leave(true);
        }
    }
}

void CodeFormatter::correctIndentation(const QTextBlock &block)
{
    const int lexerState = tokenizeBlock(block);
    Q_ASSERT(m_currentState.size() >= 1);

    adjustIndent(m_tokens, lexerState, &m_indentDepth);
}

bool CodeFormatter::tryInsideExpression(bool alsoExpression)
{
    int newState = -1;
    const int kind = extendedTokenKind(m_currentToken);
    switch (kind) {
    case LeftParenthesis:   newState = paren_open; break;
    case LeftBracket:       newState = bracket_open; break;
    case LeftBrace:         newState = objectliteral_open; break;
    case Function:          newState = function_start; break;
    case Question:          newState = ternary_op; break;
    }

    if (newState != -1) {
        if (alsoExpression)
            enter(expression);
        enter(newState);
        return true;
    }

    return false;
}

bool CodeFormatter::tryStatement()
{
    const int kind = extendedTokenKind(m_currentToken);
    switch (kind) {
    case Semicolon:
        enter(empty_statement);
        leave(true);
        return true;
    case Break:
    case Continue:
        enter(breakcontinue_statement);
        leave(true);
        return true;
    case Throw:
        enter(throw_statement);
        enter(expression);
        return true;
    case Return:
        enter(return_statement);
        enter(expression);
        return true;
    case While:
    case For:
    case Catch:
        enter(statement_with_condition);
        return true;
    case Switch:
        enter(switch_statement);
        return true;
    case If:
        enter(if_statement);
        return true;
    case Do:
        enter(do_statement);
        enter(substatement);
        return true;
    case Case:
    case Default:
        enter(case_start);
        return true;
    case Try:
    case Finally:
        enter(statement_with_block);
        return true;
    case LeftBrace:
        enter(jsblock_open);
        return true;
    case Identifier:
    case Delimiter:
    case PlusPlus:
    case MinusMinus:
    case Import:
    case Signal:
    case On:
    case As:
    case List:
    case Property:
    case Function:
    case Number:
    case String:
        enter(expression);
        // look at the token again
        m_tokenIndex -= 1;
        return true;
    }
    return false;
}

bool CodeFormatter::isBracelessState(int type) const
{
    return
            type == if_statement ||
            type == else_clause ||
            type == substatement ||
            type == binding_assignment ||
            type == binding_or_objectdefinition;
}

bool CodeFormatter::isExpressionEndState(int type) const
{
    return
            type == topmost_intro ||
            type == top_js ||
            type == objectdefinition_open ||
            type == if_statement ||
            type == else_clause ||
            type == do_statement ||
            type == jsblock_open ||
            type == substatement_open ||
            type == bracket_open ||
            type == paren_open ||
            type == case_cont;
}

const Token &CodeFormatter::tokenAt(int idx) const
{
    static const Token empty;
    if (idx < 0 || idx >= m_tokens.size())
        return empty;
    else
        return m_tokens.at(idx);
}

int CodeFormatter::column(int index) const
{
    int col = 0;
    if (index > m_currentLine.length())
        index = m_currentLine.length();

    const QChar tab = QLatin1Char('\t');

    for (int i = 0; i < index; i++) {
        if (m_currentLine[i] == tab) {
            col = ((col / m_tabSize) + 1) * m_tabSize;
        } else {
            col++;
        }
    }
    return col;
}

QStringRef CodeFormatter::currentTokenText() const
{
    return m_currentLine.midRef(m_currentToken.begin(), m_currentToken.length);
}

void CodeFormatter::turnInto(int newState)
{
    leave(false);
    enter(newState);
}

void CodeFormatter::saveCurrentState(const QTextBlock &block)
{
    if (!block.isValid())
        return;

    BlockData blockData;
    blockData.m_blockRevision = block.revision();
    blockData.m_beginState = m_beginState;
    blockData.m_endState = m_currentState;
    blockData.m_indentDepth = m_indentDepth;

    QTextBlock saveableBlock(block);
    saveBlockData(&saveableBlock, blockData);
}

void CodeFormatter::restoreCurrentState(const QTextBlock &block)
{
    if (block.isValid()) {
        BlockData blockData;
        if (loadBlockData(block, &blockData)) {
            m_indentDepth = blockData.m_indentDepth;
            m_currentState = blockData.m_endState;
            m_beginState = m_currentState;
            return;
        }
    }

    m_currentState = initialState();
    m_beginState = m_currentState;
    m_indentDepth = 0;
}

QStack<CodeFormatter::State> CodeFormatter::initialState()
{
    static QStack<CodeFormatter::State> initialState;
    if (initialState.isEmpty())
        initialState.push(State(topmost_intro, 0));
    return initialState;
}

int CodeFormatter::tokenizeBlock(const QTextBlock &block)
{
    int startState = loadLexerState(block.previous());
    if (block.blockNumber() == 0)
        startState = 0;
    Q_ASSERT(startState != -1);

    Scanner tokenize;
    tokenize.setScanComments(true);

    m_currentLine = block.text();
    // to determine whether a line was joined, Tokenizer needs a
    // newline character at the end
    m_currentLine.append(QLatin1Char('\n'));
    m_tokens = tokenize(m_currentLine, startState);

    const int lexerState = tokenize.state();
    QTextBlock saveableBlock(block);
    saveLexerState(&saveableBlock, lexerState);
    return lexerState;
}

CodeFormatter::TokenKind CodeFormatter::extendedTokenKind(const QmlJS::Token &token) const
{
    const int kind = token.kind;
    QStringRef text = m_currentLine.midRef(token.begin(), token.length);

    if (kind == Identifier) {
        if (text == "as")
            return As;
        if (text == "import")
            return Import;
        if (text == "signal")
            return Signal;
        if (text == "property")
            return Property;
        if (text == "on")
            return On;
        if (text == "list")
            return On;
    } else if (kind == Keyword) {
        const QChar char1 = text.at(0);
        const QChar char2 = text.at(1);
        const QChar char3 = (text.size() > 2 ? text.at(2) : QChar());
        switch (char1.toLatin1()) {
        case 'v':
            return Var;
        case 'i':
            if (char2 == 'f')
                return If;
            else if (char3 == 's')
                return Instanceof;
            else
                return In;
        case 'f':
            if (char2 == 'o')
                return For;
            else if (char2 == 'u')
                return Function;
            else
                return Finally;
        case 'e':
            return Else;
        case 'n':
            return New;
        case 'r':
            return Return;
        case 's':
            return Switch;
        case 'w':
            if (char2 == 'h')
                return While;
            return With;
        case 'c':
            if (char3 == 's')
                return Case;
            if (char3 == 't')
                return Catch;
            return Continue;
        case 'd':
            if (char3 == 'l')
                return Delete;
            if (char3 == 'f')
                return Default;
            if (char3 == 'b')
                return Debugger;
            return Do;
        case 't':
            if (char3 == 'i')
                return This;
            if (char3 == 'y')
                return Try;
            if (char3 == 'r')
                return Throw;
            return Typeof;
        case 'b':
            return Break;
        }
    } else if (kind == Delimiter) {
        if (text == "?")
            return Question;
        else if (text == "++")
            return PlusPlus;
        else if (text == "--")
            return MinusMinus;
    }

    return static_cast<TokenKind>(kind);
}

void CodeFormatter::dump() const
{
    QMetaEnum metaEnum = staticMetaObject.enumerator(staticMetaObject.indexOfEnumerator("StateType"));

    qDebug() << "Current token index" << m_tokenIndex;
    qDebug() << "Current state:";
    foreach (State s, m_currentState) {
        qDebug() << metaEnum.valueToKey(s.type) << s.savedIndentDepth;
    }
    qDebug() << "Current indent depth:" << m_indentDepth;
}