diff --git a/include/internal/QCodeEditor.hpp b/include/internal/QCodeEditor.hpp index c1c0f1d..7362d35 100644 --- a/include/internal/QCodeEditor.hpp +++ b/include/internal/QCodeEditor.hpp @@ -143,6 +143,27 @@ class QCodeEditor : public QTextEdit */ void clearSquiggle(); + /** + * @brief Enables or disables Vim Like cursor + */ + void setVimCursor(bool value); + + /** + * @brief Checks if cursor type is Vim Cursor + */ + bool vimCursor() const; + + /** + * @brief Enables or disables current line highlighting + * @note In vim mode this cannot enable line highlighting + */ + void setHighlightCurrentLine(bool enabled); + + /** + * @brief Checks if current line is being higlighted in non vim mode + */ + bool isHighlightingCurrentLine() const; + Q_SIGNALS: /** * @brief Signal, the font is changed by the wheel event. @@ -274,6 +295,13 @@ class QCodeEditor : public QTextEdit */ void focusInEvent(QFocusEvent *e) override; + /** + * @brief Method, that's called on focus loss + * It's required for setting block cursor + * in fakevim mode. + */ + void focusOutEvent(QFocusEvent *e) override; + /** * @brief Method for tooltip generation */ @@ -384,10 +412,12 @@ class QCodeEditor : public QTextEdit bool m_autoIndentation; bool m_replaceTab; bool m_extraBottomMargin; + bool m_vimCursor; + bool m_highlightCurrentLine; QString m_tabReplace; QList extra1, extra2, extra_squiggles; - + QRect m_cursorRect; QVector m_squiggler; QVector m_parentheses; diff --git a/src/internal/QCodeEditor.cpp b/src/internal/QCodeEditor.cpp index 54dc9fc..a571b94 100644 --- a/src/internal/QCodeEditor.cpp +++ b/src/internal/QCodeEditor.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -26,9 +27,9 @@ QCodeEditor::QCodeEditor(QWidget *widget) : QTextEdit(widget), m_highlighter(nullptr), m_syntaxStyle(nullptr), m_lineNumberArea(new QLineNumberArea(this)), - m_completer(nullptr), m_autoIndentation(true), m_replaceTab(true), m_extraBottomMargin(true), - m_tabReplace(QString(4, ' ')), extra1(), extra2(), extra_squiggles(), m_squiggler(), - m_parentheses({{'(', ')'}, {'{', '}'}, {'[', ']'}, {'\"', '\"'}, {'\'', '\''}}) + m_completer(nullptr), m_autoIndentation(true), m_replaceTab(true), m_extraBottomMargin(true), m_vimCursor(false), + m_highlightCurrentLine(true), m_tabReplace(QString(4, ' ')), extra1(), extra2(), extra_squiggles(), + m_cursorRect(), m_squiggler(), m_parentheses({{'(', ')'}, {'{', '}'}, {'[', ']'}, {'\"', '\"'}, {'\'', '\''}}) { initFont(); performConnections(); @@ -418,87 +419,90 @@ void QCodeEditor::toggleBlockComment() void QCodeEditor::highlightParenthesis() { - auto currentSymbol = charUnderCursor(); - auto prevSymbol = charUnderCursor(-1); + const auto text = toPlainText(); + const int currentPosition = textCursor().position(); - for (auto &p : m_parentheses) - { - int direction; - - QChar counterSymbol; - QChar activeSymbol; - auto position = textCursor().position(); + QChar activeSymbol; + int activePosition; + QChar counterSymbol; + int direction = 0; - if (p.left == currentSymbol) - { - direction = 1; - counterSymbol = p.right; - activeSymbol = currentSymbol; - } - else if (p.right == prevSymbol) - { - direction = -1; - counterSymbol = p.left; - activeSymbol = prevSymbol; - position--; - } - else - { + // check both the current character and the previous character if the cursor is between two characters + for (activePosition = currentPosition; activePosition >= (overwriteMode() ? currentPosition : currentPosition - 1); + --activePosition) + { + if (activePosition < 0 || activePosition >= text.length()) continue; - } - auto counter = 1; + activeSymbol = text[activePosition]; - while (counter != 0 && position > 0 && position < (document()->characterCount() - 1)) + for (const auto &p : m_parentheses) { - // Moving position - position += direction; - - auto character = document()->characterAt(position); - // Checking symbol under position - if (character == activeSymbol) + if (p.left == p.right) + continue; + if (activeSymbol == p.left) { - ++counter; + direction = 1; + counterSymbol = p.right; + break; } - else if (character == counterSymbol) + if (activeSymbol == p.right) { - --counter; + direction = -1; + counterSymbol = p.left; + break; } } - auto format = m_syntaxStyle->getFormat("Parentheses"); - - // Found - if (counter == 0) - { - ExtraSelection selection{}; - - auto directionEnum = direction < 0 ? QTextCursor::MoveOperation::Left : QTextCursor::MoveOperation::Right; + if (direction != 0) + break; + } - selection.format = format; - selection.cursor = textCursor(); - selection.cursor.clearSelection(); - selection.cursor.movePosition(directionEnum, QTextCursor::MoveMode::MoveAnchor, - qAbs(textCursor().position() - position)); + if (direction == 0) // not a parenthesis + return; - selection.cursor.movePosition(QTextCursor::MoveOperation::Right, QTextCursor::MoveMode::KeepAnchor, 1); + int matchPosition = -1; + int count = 1; + int singleQuoteCounter = 0; + int doubleQuoteCounter = 0; - extra1.append(selection); + for (int i = activePosition + direction; i >= 0 && i < text.length(); i += direction) + { + if (text[i] == "'") + singleQuoteCounter++; + else if (text[i] == "\"") + doubleQuoteCounter++; + else if (text[i] == activeSymbol && singleQuoteCounter % 2 == 0 && doubleQuoteCounter % 2 == 0) + ++count; + else if (text[i] == counterSymbol && singleQuoteCounter % 2 == 0 && doubleQuoteCounter % 2 == 0) + --count; + if (count == 0) + { + matchPosition = i; + break; + } + } + if (matchPosition >= 0) // Match found + { + auto addExtra = [&](int pos) { + ExtraSelection selection; + selection.format = m_syntaxStyle->getFormat("Parentheses"); selection.cursor = textCursor(); selection.cursor.clearSelection(); - selection.cursor.movePosition(directionEnum, QTextCursor::MoveMode::KeepAnchor, 1); - + selection.cursor.setPosition(pos); + selection.cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor); extra1.append(selection); - } + }; - break; + addExtra(activePosition); + addExtra(matchPosition); } } void QCodeEditor::highlightCurrentLine() { - if (!isReadOnly()) + if (m_highlightCurrentLine && !isReadOnly() && !m_vimCursor) { QTextEdit::ExtraSelection selection{}; @@ -507,7 +511,6 @@ void QCodeEditor::highlightCurrentLine() selection.format.setProperty(QTextFormat::FullWidthSelection, true); selection.cursor = textCursor(); selection.cursor.clearSelection(); - extra1.append(selection); } } @@ -544,6 +547,43 @@ void QCodeEditor::paintEvent(QPaintEvent *e) { updateLineNumberArea(e->rect()); QTextEdit::paintEvent(e); + + if (m_vimCursor) + { + if (!m_cursorRect.isNull() && e->rect().intersects(m_cursorRect)) + { + QRect rect = m_cursorRect; + m_cursorRect = QRect(); + viewport()->update(rect); + } + + // Draw text cursor. + QRect rect = cursorRect(); + if (e->rect().intersects(rect)) + { + QPainter painter(viewport()); + + if (overwriteMode()) + { + QFontMetrics fm(font()); + const int position = textCursor().position(); + const QChar c = document()->characterAt(position); + rect.setWidth(fm.horizontalAdvance(c)); + painter.setPen(Qt::NoPen); + auto cursorColor = m_syntaxStyle->getFormat("Text").foreground().color(); + painter.setBrush(m_syntaxStyle->name() == "Default" ? Qt::white : cursorColor); + painter.setCompositionMode(QPainter::CompositionMode_Difference); + } + else + { + rect.setWidth(cursorWidth()); + painter.setPen(m_syntaxStyle->getFormat("Text").foreground().color()); + } + + painter.drawRect(rect); + m_cursorRect = rect; + } + } } int QCodeEditor::getFirstVisibleBlock() @@ -642,6 +682,12 @@ void QCodeEditor::keyPressEvent(QKeyEvent *e) if (!completerSkip) { + if (m_vimCursor) + { + QTextEdit::keyPressEvent(e); + proceedCompleterEnd(e); + return; + } if ((e->key() == Qt::Key_Return || e->key() == Qt::Key_Enter) && e->modifiers() != Qt::NoModifier) { QKeyEvent pureEnter(QEvent::KeyPress, Qt::Key_Enter, Qt::NoModifier); @@ -740,6 +786,27 @@ void QCodeEditor::keyPressEvent(QKeyEvent *e) return; } + // Toggle Overwrite and insert mode in non-vim modes. + if (e->key() == Qt::Key_Insert && !m_vimCursor) + { + setOverwriteMode(!overwriteMode()); + if (overwriteMode()) + { + QFontMetrics fm(QTextEdit::font()); + const int position = QTextEdit::textCursor().position(); + const QChar c = QTextEdit::document()->characterAt(position); + setCursorWidth(fm.horizontalAdvance(c)); + } + else + { + auto rect = cursorRect(); + setCursorWidth(1); + viewport()->update(rect); + } + + return; + } + if (e->key() == Qt::Key_Backspace && e->modifiers() == Qt::NoModifier && !textCursor().hasSelection()) { auto pre = charUnderCursor(-1); @@ -875,6 +942,29 @@ void QCodeEditor::setTabReplace(bool enabled) m_replaceTab = enabled; } +void QCodeEditor::setHighlightCurrentLine(bool enabled) +{ + m_highlightCurrentLine = enabled; +} + +void QCodeEditor::setVimCursor(bool enabled) +{ + m_vimCursor = enabled; + + setOverwriteMode(false); + setCursorWidth(enabled ? 0 : 1); +} + +bool QCodeEditor::isHighlightingCurrentLine() const +{ + return m_highlightCurrentLine; +} + +bool QCodeEditor::vimCursor() const +{ + return m_vimCursor; +} + bool QCodeEditor::tabReplace() const { return m_replaceTab; @@ -921,6 +1011,16 @@ void QCodeEditor::focusInEvent(QFocusEvent *e) QTextEdit::focusInEvent(e); } +void QCodeEditor::focusOutEvent(QFocusEvent *e) +{ + if (m_vimCursor) + { + setOverwriteMode(true); // makes a block cursor when focus is lost + } + + QTextEdit::focusOutEvent(e); +} + bool QCodeEditor::event(QEvent *event) { if (event->type() == QEvent::ToolTip)