#include "CodeEditor.h" #include "EditorGutter.h" #include #include #include #include // // Adapted from codeeditor example // http://doc.qt.io/qt-5/qtwidgets-widgets-codeeditor-example.html // // Used term gutter as I want to expand it to do other things to like error // position marking. // CodeEditor::CodeEditor(QWidget *parent) : QPlainTextEdit(parent) , gutterArea(new EditorGutter(this)) , colorTheme(GetColorTheme()) { connect(this, SIGNAL(blockCountChanged(int)), this, SLOT(updateGutterAreaWidth(int))); connect(this, SIGNAL(updateRequest(QRect,int)), this, SLOT(updateGutterArea(QRect,int))); connect(this, SIGNAL(cursorPositionChanged()), this, SLOT(highlightCurrentLine())); connect(this, SIGNAL(textChanged()), this, SLOT(onTextChanged())); setWordWrapMode(QTextOption::NoWrap); updateGutterAreaWidth(0); setTabSize(4); highlightCurrentLine(); } int CodeEditor::gutterAreaWidth() { int digits = 1; int max = qMax(1, blockCount()); while (max >= 10) { max /= 10; ++digits; } int space = 3 + fontMetrics().horizontalAdvance(QLatin1Char('9')) * (digits + 2); return space; } void CodeEditor::updateGutterAreaWidth(int /* newBlockCount */) { setViewportMargins(gutterAreaWidth(), 0, 0, 0); } void CodeEditor::updateGutterArea(const QRect &rect, int dy) { if (dy) gutterArea->scroll(0, dy); else gutterArea->update(0, rect.y(), gutterArea->width(), rect.height()); if (rect.contains(viewport()->rect())) updateGutterAreaWidth(0); } void CodeEditor::resizeEvent(QResizeEvent *e) { QPlainTextEdit::resizeEvent(e); QRect cr = contentsRect(); gutterArea->setGeometry(QRect(cr.left(), cr.top(), gutterAreaWidth(), cr.height())); } void CodeEditor::highlightCurrentLine() { QTextEdit::ExtraSelection selection; selection.format.setBackground(colorTheme.currentLine); selection.format.setProperty(QTextFormat::FullWidthSelection, true); selection.cursor = textCursor(); selection.cursor.clearSelection(); currentLine = selection; updateExtraSelections(); } void CodeEditor::onTextChanged() { clearErrorMarkers(); } void CodeEditor::addErrorMarker(int position, int length) { QTextEdit::ExtraSelection selection; selection.format.setBackground(colorTheme.errorLine); selection.format.setFontItalic(true); selection.cursor = textCursor(); selection.cursor.setPosition(position); int lineno = selection.cursor.blockNumber(); errorLines.insert(lineno); selection.cursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, length); errorMarkers.append(selection); update(); updateExtraSelections(); } void CodeEditor::clearErrorMarkers() { errorMarkers.clear(); errorLines.clear(); update(); updateExtraSelections(); } void CodeEditor::updateExtraSelections() { QList extraSelections; extraSelections.append(currentLine); for (auto&& e : errorMarkers) extraSelections.append(e); setExtraSelections(extraSelections); } bool CodeEditor::lineHasError(int blockNumber) const { return errorLines.count(blockNumber) > 0; } void CodeEditor::setCompletionCallback(EditorCompletionCallback callback) { m_completionCallback = std::move(callback); if (!m_completionCallback) hideCompletions(); } void CodeEditor::showCompletions() { updateCompletions(true); } void CodeEditor::hideCompletions() { if (m_completionPopup) m_completionPopup->hide(); } bool CodeEditor::updateCompletions(bool explicitRequest) { if (!m_completionCallback) { hideCompletions(); return false; } const int pos = textCursor().position(); EditorCompletionResult result = m_completionCallback(toPlainText(), pos); bool show = !result.items.empty(); if (show && !explicitRequest) { // do not bother the user while nothing identifier like has been typed if (result.replaceLen == 0 && !result.hasQualifier) show = false; // the only candidate is what has been typed already else if (result.items.size() == 1 && result.replaceLen > 0) { QTextCursor c = textCursor(); c.setPosition(result.replaceStart); c.setPosition(result.replaceStart + result.replaceLen, QTextCursor::KeepAnchor); if (c.selectedText() == result.items[0].insertText) show = false; } } if (!show) { hideCompletions(); return false; } if (!m_completionPopup) { m_completionPopup = new CompletionPopup(this); connect(m_completionPopup, &CompletionPopup::itemAccepted, this, &CodeEditor::acceptCompletion); } m_completionResult = std::move(result); m_completionPopup->setFont(font()); m_completionPopup->setItems(m_completionResult.items); QTextCursor c = textCursor(); c.setPosition(m_completionResult.replaceStart); QRect r = cursorRect(c); m_completionPopup->showAt(viewport()->mapToGlobal(r.bottomLeft())); return true; } void CodeEditor::acceptCompletion(int row) { if (row < 0 || row >= static_cast(m_completionResult.items.size())) return; const QString text = m_completionResult.items[row].insertText; QTextCursor c = textCursor(); c.beginEditBlock(); c.setPosition(m_completionResult.replaceStart); c.setPosition(m_completionResult.replaceStart + m_completionResult.replaceLen, QTextCursor::KeepAnchor); c.insertText(text); c.endEditBlock(); setTextCursor(c); hideCompletions(); } void CodeEditor::focusOutEvent(QFocusEvent *e) { hideCompletions(); QPlainTextEdit::focusOutEvent(e); } void CodeEditor::mousePressEvent(QMouseEvent *e) { hideCompletions(); QPlainTextEdit::mousePressEvent(e); } void CodeEditor::keyPressEvent(QKeyEvent *e) { auto k = e->key(); const bool popupVisible = m_completionPopup && m_completionPopup->isVisible(); if (popupVisible) { switch (k) { case Qt::Key_Down: m_completionPopup->moveSelection(1); return; case Qt::Key_Up: m_completionPopup->moveSelection(-1); return; case Qt::Key_PageDown: m_completionPopup->moveSelection(10); return; case Qt::Key_PageUp: m_completionPopup->moveSelection(-10); return; case Qt::Key_Return: case Qt::Key_Enter: case Qt::Key_Tab: acceptCompletion(m_completionPopup->currentRow()); return; case Qt::Key_Escape: hideCompletions(); return; } } if (k == Qt::Key_Space && e->modifiers().testFlag(Qt::ControlModifier)) { showCompletions(); return; } switch (k) { case Qt::Key_Tab: if (indentSelection(true)) return; break; case Qt::Key_Backtab: if (indentSelection(false)) return; break; case Qt::Key_Equal: case Qt::Key_Plus: if (e->modifiers().testFlag(Qt::ControlModifier)) { auto f = font(); f.setPointSize(f.pointSize() + 1); setFont(f); } break; case Qt::Key_Minus: if (e->modifiers().testFlag(Qt::ControlModifier)) { auto f = font(); f.setPointSize(f.pointSize() - 1); setFont(f); } break; } QPlainTextEdit::keyPressEvent(e); if (m_completionCallback) { if (popupVisible) { if (k == Qt::Key_Backspace || k == Qt::Key_Delete || !e->text().isEmpty()) updateCompletions(false); else if (k == Qt::Key_Left || k == Qt::Key_Right || k == Qt::Key_Home || k == Qt::Key_End) hideCompletions(); } else if (!e->text().isEmpty() && !e->modifiers().testFlag(Qt::ControlModifier)) { const QChar ch = e->text()[0]; if (ch.isLetterOrNumber() || ch == QLatin1Char('_') || ch == QLatin1Char('.') || ch == QLatin1Char('"')) updateCompletions(false); } } } bool CodeEditor::indentSelection(bool indent) { auto cursor = textCursor(); if(!cursor.hasSelection()) return false; auto first_pos = cursor.anchor(); auto end_pos = cursor.position(); if(first_pos > end_pos) std::swap(first_pos, end_pos); cursor.setPosition(first_pos, QTextCursor::MoveAnchor); auto start_block = cursor.block().blockNumber(); cursor.setPosition(end_pos, QTextCursor::MoveAnchor); auto end_block = cursor.block().blockNumber(); if (end_block == start_block) return false; cursor.beginEditBlock(); cursor.setPosition(first_pos, QTextCursor::MoveAnchor); const auto block_count = end_block - start_block; for(int block = 0; block <= block_count; ++block) { cursor.movePosition(QTextCursor::StartOfBlock, QTextCursor::MoveAnchor); if (indent) insertIndentation(cursor); else removeIndentation(cursor); cursor.movePosition(QTextCursor::NextBlock, QTextCursor::MoveAnchor); } cursor.endEditBlock(); makeSelection(cursor, first_pos, end_block); setTextCursor(cursor); return true; } void CodeEditor::insertIndentation(QTextCursor &cursor) { if (m_useTab) cursor.insertText("\t"); else cursor.insertText(QString(m_tabSize, ' ')); } void CodeEditor::removeIndentation(QTextCursor &cursor) { // remove tab if there is a tab cursor.movePosition(QTextCursor::EndOfLine, QTextCursor::KeepAnchor); const QString text = cursor.selectedText(); int index = 0; int pos = 0; while (pos < m_tabSize && index < text.length()) { QChar c = text[index++]; if (c == ' ') ++pos; else if (c == '\t') pos = ((pos + m_tabSize) / m_tabSize) * m_tabSize; else { --index; break; } } cursor.movePosition(QTextCursor::StartOfBlock, QTextCursor::MoveAnchor); for (int i = 0; i < index; ++i) cursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor); cursor.removeSelectedText(); } void CodeEditor::makeSelection(QTextCursor &cursor, int first_pos, int end_block) { cursor.setPosition(first_pos, QTextCursor::MoveAnchor); cursor.movePosition(QTextCursor::StartOfBlock, QTextCursor::MoveAnchor); while(cursor.block().blockNumber() < end_block) cursor.movePosition(QTextCursor::NextBlock, QTextCursor::KeepAnchor); cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor); } void CodeEditor::setFont(const QFont &f) { QWidget::setFont(f); auto orig_tab = m_tabSize; m_tabSize = 0; setTabSize(orig_tab); } void CodeEditor::setTabSize(int chars) { m_tabSize = chars; int pixels = fontMetrics().horizontalAdvance(QString(chars, '0')); this->setTabStopDistance(pixels); }