diff options
author | Christian Kamm <christian.d.kamm@nokia.com> | 2010-07-07 11:45:18 +0200 |
---|---|---|
committer | Christian Kamm <christian.d.kamm@nokia.com> | 2010-08-10 14:27:08 +0200 |
commit | 822de6c17ab52002ae59a94c3e231bf0d5e3e438 (patch) | |
tree | a683fcd312b3f5840cd326ae2106f377fcd36505 /src/libs/qmljs/qmljscodeformatter.cpp | |
parent | f6232260c2c5ba93e85d725e05c2d2c9b86e293c (diff) | |
download | qt-creator-822de6c17ab52002ae59a94c3e231bf0d5e3e438.tar.gz |
QmlJS: Introduce a new indenter that works similarly to the new C++ one.
Done-with: Thomas Hartmann
Diffstat (limited to 'src/libs/qmljs/qmljscodeformatter.cpp')
-rw-r--r-- | src/libs/qmljs/qmljscodeformatter.cpp | 910 |
1 files changed, 910 insertions, 0 deletions
diff --git a/src/libs/qmljs/qmljscodeformatter.cpp b/src/libs/qmljs/qmljscodeformatter.cpp new file mode 100644 index 0000000000..468d112cbc --- /dev/null +++ b/src/libs/qmljs/qmljscodeformatter.cpp @@ -0,0 +1,910 @@ +/************************************************************************** +** +** This file is part of Qt Creator +** +** Copyright (c) 2010 Nokia Corporation and/or its subsidiary(-ies). +** +** Contact: Nokia Corporation (qt-info@nokia.com) +** +** Commercial Usage +** +** Licensees holding valid Qt Commercial licenses may use this file in +** accordance with the Qt Commercial License Agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Nokia. +** +** 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. +** +** If you are unsure which license is appropriate for your use, please +** contact the sales department at http://qt.nokia.com/contact. +** +**************************************************************************/ + +#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); + //dump(); + //qDebug() << "Token" << m_currentLine.mid(m_currentToken.begin(), m_currentToken.length) << m_tokenIndex << "in line" << block.blockNumber() + 1; + + 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 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; +} + +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 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 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: + 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; +} |