Commit e07c3401 authored by mae's avatar mae
Browse files

Rework code folding

The new and cleaner foldingIndent in the block user data will
make it easier to support other kinds of indentation for various
other programming languages (like Python).
parent 55b26868
...@@ -65,15 +65,19 @@ void CppHighlighter::highlightBlock(const QString &text) ...@@ -65,15 +65,19 @@ void CppHighlighter::highlightBlock(const QString &text)
const QList<SimpleToken> tokens = tokenize(text, initialState); const QList<SimpleToken> tokens = tokenize(text, initialState);
state = tokenize.state(); // refresh the state state = tokenize.state(); // refresh the state
if (tokens.isEmpty()) { int foldingIndent = initialBraceDepth;
setCurrentBlockState(previousState);
if (TextBlockUserData *userData = BaseTextDocumentLayout::testUserData(currentBlock())) { if (TextBlockUserData *userData = BaseTextDocumentLayout::testUserData(currentBlock())) {
userData->setClosingCollapseMode(TextBlockUserData::NoClosingCollapse); userData->setFoldingIndent(0);
userData->setCollapseMode(TextBlockUserData::NoCollapse); userData->setFoldingStartIncluded(false);
userData->setFoldingEndIncluded(false);
} }
if (tokens.isEmpty()) {
setCurrentBlockState(previousState);
BaseTextDocumentLayout::clearParentheses(currentBlock()); BaseTextDocumentLayout::clearParentheses(currentBlock());
if (text.length()) // the empty line can still contain whitespace if (text.length()) // the empty line can still contain whitespace
setFormat(0, text.length(), m_formats[CppVisualWhitespace]); setFormat(0, text.length(), m_formats[CppVisualWhitespace]);
BaseTextDocumentLayout::setFoldingIndent(currentBlock(), foldingIndent);
return; return;
} }
...@@ -102,13 +106,29 @@ void CppHighlighter::highlightBlock(const QString &text) ...@@ -102,13 +106,29 @@ void CppHighlighter::highlightBlock(const QString &text)
if (tk.is(T_LPAREN) || tk.is(T_LBRACE) || tk.is(T_LBRACKET)) { if (tk.is(T_LPAREN) || tk.is(T_LBRACE) || tk.is(T_LBRACKET)) {
const QChar c(tk.text().at(0)); const QChar c(tk.text().at(0));
parentheses.append(Parenthesis(Parenthesis::Opened, c, tk.position())); parentheses.append(Parenthesis(Parenthesis::Opened, c, tk.position()));
if (tk.is(T_LBRACE)) if (tk.is(T_LBRACE)) {
++braceDepth; ++braceDepth;
// if a folding block opens at the beginning of a line, treat the entire line
// as if it were inside the folding block
if (tk.position() == firstNonSpace) {
++foldingIndent;
BaseTextDocumentLayout::userData(currentBlock())->setFoldingStartIncluded(true);
}
}
} else if (tk.is(T_RPAREN) || tk.is(T_RBRACE) || tk.is(T_RBRACKET)) { } else if (tk.is(T_RPAREN) || tk.is(T_RBRACE) || tk.is(T_RBRACKET)) {
const QChar c(tk.text().at(0)); const QChar c(tk.text().at(0));
parentheses.append(Parenthesis(Parenthesis::Closed, c, tk.position())); parentheses.append(Parenthesis(Parenthesis::Closed, c, tk.position()));
if (tk.is(T_RBRACE)) if (tk.is(T_RBRACE)) {
--braceDepth; --braceDepth;
if (braceDepth < foldingIndent) {
// unless we are at the end of the block, we reduce the folding indent
if (i == tokens.size()-1 || tokens.at(i+1).is(T_SEMICOLON))
BaseTextDocumentLayout::userData(currentBlock())->setFoldingEndIncluded(true);
else
foldingIndent = qMin(braceDepth, foldingIndent);
}
}
} }
bool highlightCurrentWordAsPreprocessor = highlightAsPreprocessor; bool highlightCurrentWordAsPreprocessor = highlightAsPreprocessor;
...@@ -148,7 +168,11 @@ void CppHighlighter::highlightBlock(const QString &text) ...@@ -148,7 +168,11 @@ void CppHighlighter::highlightBlock(const QString &text)
// - is not a continuation line (tokens.size() > 1 || ! state) // - is not a continuation line (tokens.size() > 1 || ! state)
if (initialState && i == 0 && (tokens.size() > 1 || ! state)) { if (initialState && i == 0 && (tokens.size() > 1 || ! state)) {
--braceDepth; --braceDepth;
// unless we are at the end of the block, we reduce the folding indent
if (i == tokens.size()-1)
BaseTextDocumentLayout::userData(currentBlock())->setFoldingEndIncluded(true);
else
foldingIndent = qMin(braceDepth, foldingIndent);
const int tokenEnd = tk.position() + tk.length() - 1; const int tokenEnd = tk.position() + tk.length() - 1;
parentheses.append(Parenthesis(Parenthesis::Closed, QLatin1Char('-'), tokenEnd)); parentheses.append(Parenthesis(Parenthesis::Closed, QLatin1Char('-'), tokenEnd));
...@@ -167,6 +191,7 @@ void CppHighlighter::highlightBlock(const QString &text) ...@@ -167,6 +191,7 @@ void CppHighlighter::highlightBlock(const QString &text)
else if (tk.is(T_IDENTIFIER)) else if (tk.is(T_IDENTIFIER))
highlightWord(tk.text(), tk.position(), tk.length()); highlightWord(tk.text(), tk.position(), tk.length());
} }
// mark the trailing white spaces // mark the trailing white spaces
...@@ -177,43 +202,23 @@ void CppHighlighter::highlightBlock(const QString &text) ...@@ -177,43 +202,23 @@ void CppHighlighter::highlightBlock(const QString &text)
highlightLine(text, lastTokenEnd, text.length() - lastTokenEnd, QTextCharFormat()); highlightLine(text, lastTokenEnd, text.length() - lastTokenEnd, QTextCharFormat());
} }
if (TextBlockUserData *userData = BaseTextDocumentLayout::testUserData(currentBlock())) {
userData->setClosingCollapseMode(TextBlockUserData::NoClosingCollapse);
userData->setCollapseMode(TextBlockUserData::NoCollapse);
}
if (! initialState && state && ! tokens.isEmpty()) { if (! initialState && state && ! tokens.isEmpty()) {
parentheses.append(Parenthesis(Parenthesis::Opened, QLatin1Char('+'), parentheses.append(Parenthesis(Parenthesis::Opened, QLatin1Char('+'),
tokens.last().position())); tokens.last().position()));
++braceDepth; ++braceDepth;
} }
QChar c;
int collapse = Parenthesis::collapseAtPos(parentheses, &c);
if (collapse >= 0) {
TextBlockUserData::CollapseMode collapseMode = TextBlockUserData::CollapseAfter;
if (collapse == firstNonSpace && c != QLatin1Char('+'))
collapseMode = TextBlockUserData::CollapseThis;
BaseTextDocumentLayout::userData(currentBlock())->setCollapseMode(collapseMode);
}
int cc = Parenthesis::closeCollapseAtPos(parentheses);
if (cc >= 0) {
TextBlockUserData *userData = BaseTextDocumentLayout::userData(currentBlock());
userData->setClosingCollapseMode(TextBlockUserData::ClosingCollapse);
QString trailingText = text.mid(cc+1).simplified();
if (trailingText.isEmpty() || trailingText == QLatin1String(";")) {
userData->setClosingCollapseMode(TextBlockUserData::ClosingCollapseAtEnd);
}
}
BaseTextDocumentLayout::setParentheses(currentBlock(), parentheses); BaseTextDocumentLayout::setParentheses(currentBlock(), parentheses);
// if the block is ifdefed out, we only store the parentheses, but // if the block is ifdefed out, we only store the parentheses, but
// do not adjust the brace depth. // do not adjust the brace depth.
if (BaseTextDocumentLayout::ifdefedOut(currentBlock())) if (BaseTextDocumentLayout::ifdefedOut(currentBlock())) {
braceDepth = initialBraceDepth; braceDepth = initialBraceDepth;
foldingIndent = initialBraceDepth;
}
BaseTextDocumentLayout::setFoldingIndent(currentBlock(), foldingIndent);
// optimization: if only the brace depth changes, we adjust subsequent blocks // optimization: if only the brace depth changes, we adjust subsequent blocks
// to have QSyntaxHighlighter stop the rehighlighting // to have QSyntaxHighlighter stop the rehighlighting
...@@ -226,6 +231,7 @@ void CppHighlighter::highlightBlock(const QString &text) ...@@ -226,6 +231,7 @@ void CppHighlighter::highlightBlock(const QString &text)
QTextBlock block = currentBlock().next(); QTextBlock block = currentBlock().next();
while (block.isValid() && block.userState() != -1) { while (block.isValid() && block.userState() != -1) {
BaseTextDocumentLayout::changeBraceDepth(block, delta); BaseTextDocumentLayout::changeBraceDepth(block, delta);
BaseTextDocumentLayout::changeFoldingIndent(block, delta);
block = block.next(); block = block.next();
} }
} }
......
...@@ -44,6 +44,7 @@ Highlighter::Highlighter(QTextDocument *parent) ...@@ -44,6 +44,7 @@ Highlighter::Highlighter(QTextDocument *parent)
{ {
m_currentBlockParentheses.reserve(20); m_currentBlockParentheses.reserve(20);
m_braceDepth = 0; m_braceDepth = 0;
m_foldingIndent = 0;
} }
Highlighter::~Highlighter() Highlighter::~Highlighter()
...@@ -102,27 +103,27 @@ void Highlighter::highlightBlock(const QString &text) ...@@ -102,27 +103,27 @@ void Highlighter::highlightBlock(const QString &text)
break; break;
case Token::LeftParenthesis: case Token::LeftParenthesis:
onOpeningParenthesis('(', token.offset); onOpeningParenthesis('(', token.offset, index == 0);
break; break;
case Token::RightParenthesis: case Token::RightParenthesis:
onClosingParenthesis(')', token.offset); onClosingParenthesis(')', token.offset, index == tokens.size()-1);
break; break;
case Token::LeftBrace: case Token::LeftBrace:
onOpeningParenthesis('{', token.offset); onOpeningParenthesis('{', token.offset, index == 0);
break; break;
case Token::RightBrace: case Token::RightBrace:
onClosingParenthesis('}', token.offset); onClosingParenthesis('}', token.offset, index == tokens.size()-1);
break; break;
case Token::LeftBracket: case Token::LeftBracket:
onOpeningParenthesis('[', token.offset); onOpeningParenthesis('[', token.offset, index == 0);
break; break;
case Token::RightBracket: case Token::RightBracket:
onClosingParenthesis(']', token.offset); onClosingParenthesis(']', token.offset, index == tokens.size()-1);
break; break;
case Token::Identifier: { case Token::Identifier: {
...@@ -232,12 +233,8 @@ void Highlighter::highlightBlock(const QString &text) ...@@ -232,12 +233,8 @@ void Highlighter::highlightBlock(const QString &text)
setFormat(previousTokenEnd, text.length() - previousTokenEnd, m_formats[VisualWhitespace]); setFormat(previousTokenEnd, text.length() - previousTokenEnd, m_formats[VisualWhitespace]);
int firstNonSpace = 0;
if (! tokens.isEmpty())
firstNonSpace = tokens.first().offset;
setCurrentBlockState(m_scanner.state()); setCurrentBlockState(m_scanner.state());
onBlockEnd(m_scanner.state(), firstNonSpace); onBlockEnd(m_scanner.state());
} }
bool Highlighter::maybeQmlKeyword(const QStringRef &text) const bool Highlighter::maybeQmlKeyword(const QStringRef &text) const
...@@ -301,6 +298,12 @@ int Highlighter::onBlockStart() ...@@ -301,6 +298,12 @@ int Highlighter::onBlockStart()
{ {
m_currentBlockParentheses.clear(); m_currentBlockParentheses.clear();
m_braceDepth = 0; m_braceDepth = 0;
m_foldingIndent = 0;
if (TextEditor::TextBlockUserData *userData = TextEditor::BaseTextDocumentLayout::testUserData(currentBlock())) {
userData->setFoldingIndent(0);
userData->setFoldingStartIncluded(false);
userData->setFoldingEndIncluded(false);
}
int state = 0; int state = 0;
int previousState = previousBlockState(); int previousState = previousBlockState();
...@@ -308,56 +311,41 @@ int Highlighter::onBlockStart() ...@@ -308,56 +311,41 @@ int Highlighter::onBlockStart()
state = previousState & 0xff; state = previousState & 0xff;
m_braceDepth = previousState >> 8; m_braceDepth = previousState >> 8;
} }
m_foldingIndent = m_braceDepth;
return state; return state;
} }
void Highlighter::onBlockEnd(int state, int firstNonSpace) void Highlighter::onBlockEnd(int state)
{ {
typedef TextEditor::TextBlockUserData TextEditorBlockData; typedef TextEditor::TextBlockUserData TextEditorBlockData;
setCurrentBlockState((m_braceDepth << 8) | state); setCurrentBlockState((m_braceDepth << 8) | state);
TextEditor::BaseTextDocumentLayout::setParentheses(currentBlock(), m_currentBlockParentheses);
// Set block data parentheses. Force creation of block data unless empty TextEditor::BaseTextDocumentLayout::setFoldingIndent(currentBlock(), m_foldingIndent);
TextEditorBlockData *blockData = 0;
if (QTextBlockUserData *userData = currentBlockUserData())
blockData = static_cast<TextEditorBlockData *>(userData);
if (!blockData && !m_currentBlockParentheses.empty()) {
blockData = new TextEditorBlockData;
setCurrentBlockUserData(blockData);
}
if (blockData) {
blockData->setParentheses(m_currentBlockParentheses);
blockData->setClosingCollapseMode(TextEditor::TextBlockUserData::NoClosingCollapse);
blockData->setCollapseMode(TextEditor::TextBlockUserData::NoCollapse);
}
if (!m_currentBlockParentheses.isEmpty()) {
QTC_ASSERT(blockData, return);
int collapse = Parenthesis::collapseAtPos(m_currentBlockParentheses);
if (collapse >= 0) {
if (collapse == firstNonSpace)
blockData->setCollapseMode(TextEditor::TextBlockUserData::CollapseThis);
else
blockData->setCollapseMode(TextEditor::TextBlockUserData::CollapseAfter);
}
if (Parenthesis::hasClosingCollapse(m_currentBlockParentheses))
blockData->setClosingCollapseMode(TextEditor::TextBlockUserData::NoClosingCollapse);
}
} }
void Highlighter::onOpeningParenthesis(QChar parenthesis, int pos) void Highlighter::onOpeningParenthesis(QChar parenthesis, int pos, bool atStart)
{ {
if (parenthesis == QLatin1Char('{') || parenthesis == QLatin1Char('[')) if (parenthesis == QLatin1Char('{') || parenthesis == QLatin1Char('[')) {
++m_braceDepth; ++m_braceDepth;
// 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);
}
m_currentBlockParentheses.push_back(Parenthesis(Parenthesis::Opened, parenthesis, pos)); m_currentBlockParentheses.push_back(Parenthesis(Parenthesis::Opened, parenthesis, pos));
} }
void Highlighter::onClosingParenthesis(QChar parenthesis, int pos) void Highlighter::onClosingParenthesis(QChar parenthesis, int pos, bool atEnd)
{ {
if (parenthesis == QLatin1Char('}') || parenthesis == QLatin1Char(']')) if (parenthesis == QLatin1Char('}') || parenthesis == QLatin1Char(']')) {
--m_braceDepth; --m_braceDepth;
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
}
m_currentBlockParentheses.push_back(Parenthesis(Parenthesis::Closed, parenthesis, pos)); m_currentBlockParentheses.push_back(Parenthesis(Parenthesis::Closed, parenthesis, pos));
} }
...@@ -78,12 +78,12 @@ protected: ...@@ -78,12 +78,12 @@ protected:
virtual void highlightBlock(const QString &text); virtual void highlightBlock(const QString &text);
int onBlockStart(); int onBlockStart();
void onBlockEnd(int state, int firstNonSpace); void onBlockEnd(int state);
// The functions are notified whenever parentheses are encountered. // The functions are notified whenever parentheses are encountered.
// Custom behaviour can be added, for example storing info for indenting. // Custom behaviour can be added, for example storing info for indenting.
void onOpeningParenthesis(QChar parenthesis, int pos); void onOpeningParenthesis(QChar parenthesis, int pos, bool atStart);
void onClosingParenthesis(QChar parenthesis, int pos); void onClosingParenthesis(QChar parenthesis, int pos, bool atEnd);
bool maybeQmlKeyword(const QStringRef &text) const; bool maybeQmlKeyword(const QStringRef &text) const;
bool maybeQmlBuiltinType(const QStringRef &text) const; bool maybeQmlBuiltinType(const QStringRef &text) const;
...@@ -94,6 +94,7 @@ private: ...@@ -94,6 +94,7 @@ private:
bool m_qmlEnabled; bool m_qmlEnabled;
int m_braceDepth; int m_braceDepth;
int m_foldingIndent;
QmlJS::Scanner m_scanner; QmlJS::Scanner m_scanner;
Parentheses m_currentBlockParentheses; Parentheses m_currentBlockParentheses;
......
...@@ -31,60 +31,6 @@ ...@@ -31,60 +31,6 @@
using namespace TextEditor; using namespace TextEditor;
bool Parenthesis::hasClosingCollapse(const Parentheses &parentheses)
{
return closeCollapseAtPos(parentheses) >= 0;
}
int Parenthesis::closeCollapseAtPos(const Parentheses &parentheses)
{
int depth = 0;
for (int i = 0; i < parentheses.size(); ++i) {
const Parenthesis &p = parentheses.at(i);
if (p.chr == QLatin1Char('{')
|| p.chr == QLatin1Char('+')
|| p.chr == QLatin1Char('[')) {
++depth;
} else if (p.chr == QLatin1Char('}')
|| p.chr == QLatin1Char('-')
|| p.chr == QLatin1Char(']')) {
if (--depth < 0)
return p.pos;
}
}
return -1;
}
int Parenthesis::collapseAtPos(const Parentheses &parentheses, QChar *character)
{
int result = -1;
QChar c;
int depth = 0;
for (int i = 0; i < parentheses.size(); ++i) {
const Parenthesis &p = parentheses.at(i);
if (p.chr == QLatin1Char('{')
|| p.chr == QLatin1Char('+')
|| p.chr == QLatin1Char('[')) {
if (depth == 0) {
result = p.pos;
c = p.chr;
}
++depth;
} else if (p.chr == QLatin1Char('}')
|| p.chr == QLatin1Char('-')
|| p.chr == QLatin1Char(']')) {
if (--depth < 0)
depth = 0;
result = -1;
}
}
if (result >= 0 && character)
*character = c;
return result;
}
TextBlockUserData::~TextBlockUserData() TextBlockUserData::~TextBlockUserData()
{ {
TextMarks marks = m_marks; TextMarks marks = m_marks;
...@@ -94,11 +40,6 @@ TextBlockUserData::~TextBlockUserData() ...@@ -94,11 +40,6 @@ TextBlockUserData::~TextBlockUserData()
} }
} }
int TextBlockUserData::collapseAtPos(QChar *character) const
{
return Parenthesis::collapseAtPos(m_parentheses, character);
}
int TextBlockUserData::braceDepthDelta() const int TextBlockUserData::braceDepthDelta() const
{ {
int delta = 0; int delta = 0;
...@@ -112,96 +53,6 @@ int TextBlockUserData::braceDepthDelta() const ...@@ -112,96 +53,6 @@ int TextBlockUserData::braceDepthDelta() const
return delta; return delta;
} }
QTextBlock TextBlockUserData::testCollapse(const QTextBlock& block)
{
QTextBlock info = block;
if (block.userData() && static_cast<TextBlockUserData*>(block.userData())->collapseMode() == CollapseAfter)
;
else if (block.next().userData()
&& static_cast<TextBlockUserData*>(block.next().userData())->collapseMode()
== TextBlockUserData::CollapseThis)
info = block.next();
else
return QTextBlock();
int pos = static_cast<TextBlockUserData*>(info.userData())->collapseAtPos();
if (pos < 0)
return QTextBlock();
QTextCursor cursor(info);
cursor.setPosition(cursor.position() + pos);
matchCursorForward(&cursor);
return cursor.block();
}
void TextBlockUserData::doCollapse(const QTextBlock& block, bool visible)
{
QTextBlock info = block;
if (block.userData() && static_cast<TextBlockUserData*>(block.userData())->collapseMode() == CollapseAfter)
;
else if (block.next().userData()
&& static_cast<TextBlockUserData*>(block.next().userData())->collapseMode()
== TextBlockUserData::CollapseThis)
info = block.next();
else {
if (visible && !block.next().isVisible()) {
// no match, at least unfold!
QTextBlock b = block.next();
while (b.isValid() && !b.isVisible()) {
b.setVisible(true);
b.setLineCount(visible ? qMax(1, b.layout()->lineCount()) : 0);
b = b.next();
}
}
return;
}
int pos = static_cast<TextBlockUserData*>(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.setLineCount(visible ? qMax(1, b.layout()->lineCount()) : 0);
b = b.next();
}
}
return;
}
QTextBlock b = block.next();
while (b < cursor.block()) {
b.setVisible(visible);
b.setLineCount(visible ? qMax(1, b.layout()->lineCount()) : 0);
if (visible) {
TextBlockUserData *data = canCollapse(b);
if (data && data->collapsed()) {
QTextBlock end = testCollapse(b);
if (data->collapseIncludesClosure())
end = end.next();
if (end.isValid()) {
b = end;
continue;
}
}
}
b = b.next();
}
bool collapseIncludesClosure = hasClosingCollapseAtEnd(b);
if (collapseIncludesClosure) {
b.setVisible(visible);
b.setLineCount(visible ? qMax(1, b.layout()->lineCount()) : 0);
}
static_cast<TextBlockUserData*>(info.userData())->setCollapseIncludesClosure(collapseIncludesClosure);
static_cast<TextBlockUserData*>(info.userData())->setCollapsed(!block.next().isVisible());
}
TextBlockUserData::MatchType TextBlockUserData::checkOpenParenthesis(QTextCursor *cursor, QChar c) TextBlockUserData::MatchType TextBlockUserData::checkOpenParenthesis(QTextCursor *cursor, QChar c)
{ {
QTextBlock block = cursor->block(); QTextBlock block = cursor->block();
...@@ -591,3 +442,76 @@ void BaseTextDocumentLayout::changeBraceDepth(QTextBlock &block, int delta) ...@@ -591,3 +442,76 @@ void BaseTextDocumentLayout::changeBraceDepth(QTextBlock &block, int delta)