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)
const QList<SimpleToken> tokens = tokenize(text, initialState);
state = tokenize.state(); // refresh the state
int foldingIndent = initialBraceDepth;
if (TextBlockUserData *userData = BaseTextDocumentLayout::testUserData(currentBlock())) {
userData->setFoldingIndent(0);
userData->setFoldingStartIncluded(false);
userData->setFoldingEndIncluded(false);
}
if (tokens.isEmpty()) {
setCurrentBlockState(previousState);
if (TextBlockUserData *userData = BaseTextDocumentLayout::testUserData(currentBlock())) {
userData->setClosingCollapseMode(TextBlockUserData::NoClosingCollapse);
userData->setCollapseMode(TextBlockUserData::NoCollapse);
}
BaseTextDocumentLayout::clearParentheses(currentBlock());
if (text.length()) // the empty line can still contain whitespace
setFormat(0, text.length(), m_formats[CppVisualWhitespace]);
BaseTextDocumentLayout::setFoldingIndent(currentBlock(), foldingIndent);
return;
}
......@@ -102,13 +106,29 @@ void CppHighlighter::highlightBlock(const QString &text)
if (tk.is(T_LPAREN) || tk.is(T_LBRACE) || tk.is(T_LBRACKET)) {
const QChar c(tk.text().at(0));
parentheses.append(Parenthesis(Parenthesis::Opened, c, tk.position()));
if (tk.is(T_LBRACE))
if (tk.is(T_LBRACE)) {
++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)) {
const QChar c(tk.text().at(0));
parentheses.append(Parenthesis(Parenthesis::Closed, c, tk.position()));
if (tk.is(T_RBRACE))
if (tk.is(T_RBRACE)) {
--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;
......@@ -148,7 +168,11 @@ void CppHighlighter::highlightBlock(const QString &text)
// - is not a continuation line (tokens.size() > 1 || ! state)
if (initialState && i == 0 && (tokens.size() > 1 || ! state)) {
--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;
parentheses.append(Parenthesis(Parenthesis::Closed, QLatin1Char('-'), tokenEnd));
......@@ -167,6 +191,7 @@ void CppHighlighter::highlightBlock(const QString &text)
else if (tk.is(T_IDENTIFIER))
highlightWord(tk.text(), tk.position(), tk.length());
}
// mark the trailing white spaces
......@@ -177,43 +202,23 @@ void CppHighlighter::highlightBlock(const QString &text)
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()) {
parentheses.append(Parenthesis(Parenthesis::Opened, QLatin1Char('+'),
tokens.last().position()));
++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);
// if the block is ifdefed out, we only store the parentheses, but
// do not adjust the brace depth.
if (BaseTextDocumentLayout::ifdefedOut(currentBlock()))
if (BaseTextDocumentLayout::ifdefedOut(currentBlock())) {
braceDepth = initialBraceDepth;
foldingIndent = initialBraceDepth;
}
BaseTextDocumentLayout::setFoldingIndent(currentBlock(), foldingIndent);
// optimization: if only the brace depth changes, we adjust subsequent blocks
// to have QSyntaxHighlighter stop the rehighlighting
......@@ -226,6 +231,7 @@ void CppHighlighter::highlightBlock(const QString &text)
QTextBlock block = currentBlock().next();
while (block.isValid() && block.userState() != -1) {
BaseTextDocumentLayout::changeBraceDepth(block, delta);
BaseTextDocumentLayout::changeFoldingIndent(block, delta);
block = block.next();
}
}
......
......@@ -44,6 +44,7 @@ Highlighter::Highlighter(QTextDocument *parent)
{
m_currentBlockParentheses.reserve(20);
m_braceDepth = 0;
m_foldingIndent = 0;
}
Highlighter::~Highlighter()
......@@ -102,27 +103,27 @@ void Highlighter::highlightBlock(const QString &text)
break;
case Token::LeftParenthesis:
onOpeningParenthesis('(', token.offset);
onOpeningParenthesis('(', token.offset, index == 0);
break;
case Token::RightParenthesis:
onClosingParenthesis(')', token.offset);
onClosingParenthesis(')', token.offset, index == tokens.size()-1);
break;
case Token::LeftBrace:
onOpeningParenthesis('{', token.offset);
onOpeningParenthesis('{', token.offset, index == 0);
break;
case Token::RightBrace:
onClosingParenthesis('}', token.offset);
onClosingParenthesis('}', token.offset, index == tokens.size()-1);
break;
case Token::LeftBracket:
onOpeningParenthesis('[', token.offset);
onOpeningParenthesis('[', token.offset, index == 0);
break;
case Token::RightBracket:
onClosingParenthesis(']', token.offset);
onClosingParenthesis(']', token.offset, index == tokens.size()-1);
break;
case Token::Identifier: {
......@@ -232,12 +233,8 @@ void Highlighter::highlightBlock(const QString &text)
setFormat(previousTokenEnd, text.length() - previousTokenEnd, m_formats[VisualWhitespace]);
int firstNonSpace = 0;
if (! tokens.isEmpty())
firstNonSpace = tokens.first().offset;
setCurrentBlockState(m_scanner.state());
onBlockEnd(m_scanner.state(), firstNonSpace);
onBlockEnd(m_scanner.state());
}
bool Highlighter::maybeQmlKeyword(const QStringRef &text) const
......@@ -301,6 +298,12 @@ int Highlighter::onBlockStart()
{
m_currentBlockParentheses.clear();
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 previousState = previousBlockState();
......@@ -308,56 +311,41 @@ int Highlighter::onBlockStart()
state = previousState & 0xff;
m_braceDepth = previousState >> 8;
}
m_foldingIndent = m_braceDepth;
return state;
}
void Highlighter::onBlockEnd(int state, int firstNonSpace)
void Highlighter::onBlockEnd(int state)
{
typedef TextEditor::TextBlockUserData TextEditorBlockData;
setCurrentBlockState((m_braceDepth << 8) | state);
// Set block data parentheses. Force creation of block data unless empty
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);
}
TextEditor::BaseTextDocumentLayout::setParentheses(currentBlock(), m_currentBlockParentheses);
TextEditor::BaseTextDocumentLayout::setFoldingIndent(currentBlock(), m_foldingIndent);
}
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;
// 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));
}
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;
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));
}
......@@ -78,12 +78,12 @@ protected:
virtual void highlightBlock(const QString &text);
int onBlockStart();
void onBlockEnd(int state, int firstNonSpace);
void onBlockEnd(int state);
// The functions are notified whenever parentheses are encountered.
// Custom behaviour can be added, for example storing info for indenting.
void onOpeningParenthesis(QChar parenthesis, int pos);
void onClosingParenthesis(QChar parenthesis, int pos);
void onOpeningParenthesis(QChar parenthesis, int pos, bool atStart);
void onClosingParenthesis(QChar parenthesis, int pos, bool atEnd);
bool maybeQmlKeyword(const QStringRef &text) const;
bool maybeQmlBuiltinType(const QStringRef &text) const;
......@@ -94,6 +94,7 @@ private:
bool m_qmlEnabled;
int m_braceDepth;
int m_foldingIndent;
QmlJS::Scanner m_scanner;
Parentheses m_currentBlockParentheses;
......
......@@ -31,60 +31,6 @@
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()
{
TextMarks marks = m_marks;
......@@ -94,11 +40,6 @@ TextBlockUserData::~TextBlockUserData()
}
}
int TextBlockUserData::collapseAtPos(QChar *character) const
{
return Parenthesis::collapseAtPos(m_parentheses, character);
}
int TextBlockUserData::braceDepthDelta() const
{
int delta = 0;
......@@ -112,96 +53,6 @@ int TextBlockUserData::braceDepthDelta() const
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)
{
QTextBlock block = cursor->block();
......@@ -591,3 +442,76 @@ void BaseTextDocumentLayout::changeBraceDepth(QTextBlock &block, int delta)
if (delta)
setBraceDepth(block, braceDepth(block) + delta);
}
void BaseTextDocumentLayout::setFoldingIndent(const QTextBlock &block, int indent)
{
if (indent == 0) {
if (TextBlockUserData *userData = testUserData(block))
userData->setFoldingIndent(0);
} else {
userData(block)->setFoldingIndent(qMax(0,indent));
}
}
int BaseTextDocumentLayout::foldingIndent(const QTextBlock &block)
{
if (TextBlockUserData *userData = testUserData(block))
return userData->foldingIndent();
return 0;
}
void BaseTextDocumentLayout::changeFoldingIndent(QTextBlock &block, int delta)
{
if (delta)
setFoldingIndent(block, foldingIndent(block) + delta);
}
bool BaseTextDocumentLayout::canFold(const QTextBlock &block)
{
return (block.next().isValid() && foldingIndent(block.next()) > foldingIndent(block));
}
bool BaseTextDocumentLayout::isFolded(const QTextBlock &block)
{
if (TextBlockUserData *userData = testUserData(block))
return userData->folded();
return false;
}
void BaseTextDocumentLayout::setFolded(const QTextBlock &block, bool folded)
{
if (folded)
userData(block)->setFolded(true);
else {
if (TextBlockUserData *userData = testUserData(block))
return userData->setFolded(false);
}
}
void BaseTextDocumentLayout::doFoldOrUnfold(const QTextBlock& block, bool unfold)
{
if (!canFold(block))
return;
QTextBlock b = block.next();
if (b.isVisible() == unfold)
return;
int indent = foldingIndent(block);
while (b.isValid() && foldingIndent(b) > indent && b.next().isValid()) {
b.setVisible(unfold);
b.setLineCount(unfold? qMax(1, b.layout()->lineCount()) : 0);
if (unfold) { // do not unfold folded sub-blocks
if (isFolded(b) && b.next().isValid()) {
int jndent = foldingIndent(b);
b = b.next();
while (b.isValid() && foldingIndent(b) > jndent)
b = b.next();
continue;
}
}
b = b.next();
}
setFolded(block, !unfold);
}
......@@ -52,9 +52,6 @@ struct TEXTEDITOR_EXPORT Parenthesis
Type type;
QChar chr;
int pos;
static int collapseAtPos(const Parentheses &parentheses, QChar *character = 0);
static int closeCollapseAtPos(const Parentheses &parentheses);
static bool hasClosingCollapse(const Parentheses &parentheses);
};
......@@ -62,15 +59,12 @@ class TEXTEDITOR_EXPORT TextBlockUserData : public QTextBlockUserData
{
public:
enum CollapseMode { NoCollapse , CollapseThis, CollapseAfter };
enum ClosingCollapseMode { NoClosingCollapse, ClosingCollapse, ClosingCollapseAtEnd };
inline TextBlockUserData()
: m_collapseIncludesClosure(false),
m_collapseMode(NoCollapse),
m_closingCollapseMode(NoClosingCollapse),
m_collapsed(false),
m_ifdefedOut(false) {}
: m_folded(false),
m_ifdefedOut(false),
m_foldingIndent(0),
m_foldingStartIncluded(false),
m_foldingEndIncluded(false){}
~TextBlockUserData();
inline TextMarks marks() const { return m_marks; }
......@@ -80,21 +74,8 @@ public:
inline void clearMarks() { m_marks.clear(); }
inline void documentClosing() { Q_FOREACH(ITextMark *tm, m_marks) { tm->documentClosing(); } m_marks.clear();}
inline CollapseMode collapseMode() const { return (CollapseMode)m_collapseMode; }
inline void setCollapseMode(CollapseMode c) { m_collapseMode = c; }
inline void setClosingCollapseMode(ClosingCollapseMode c) { m_closingCollapseMode = c; }
inline ClosingCollapseMode closingCollapseMode() const { return (ClosingCollapseMode) m_closingCollapseMode; }
inline bool hasClosingCollapse() const { return closingCollapseMode() != NoClosingCollapse; }
inline bool hasClosingCollapseAtEnd() const { return closingCollapseMode() == ClosingCollapseAtEnd;