#include "CodeEditor.h" #include "LineNumberArea.h" #include "ColorIndicator.h" #include "SignatureHelper.h" #include "FunctionIndex.h" #include "VariableCompleter.h" #include "core/Settings.h" #include "highlighter/HighlighterFactory.h" #include #include #include #include #include #include #include #include #include #include #include #include #include CodeEditor::CodeEditor(Settings *settings, QWidget *parent) : QPlainTextEdit(parent) , m_settings(settings) { m_lineNumberArea = new LineNumberArea(this); m_colorIndicator = new ColorIndicator(this); m_signatureHelper = new SignatureHelper(this); m_varCompleter = new VariableCompleter(this); setupEditor(); connect(this, &CodeEditor::blockCountChanged, this, &CodeEditor::updateLineNumberAreaWidth); connect(this, &CodeEditor::updateRequest, this, &CodeEditor::updateLineNumberArea); connect(this, &CodeEditor::cursorPositionChanged, this, &CodeEditor::highlightCurrentLine); updateLineNumberAreaWidth(0); highlightCurrentLine(); } CodeEditor::~CodeEditor() = default; // --------------------------------------------------------------------------- // Setup // --------------------------------------------------------------------------- void CodeEditor::setupEditor() { applySettings(); setLineWrapMode(QPlainTextEdit::NoWrap); } void CodeEditor::applySettings() { setFont(m_settings->editorFont()); const int tabStop = m_settings->tabSize(); // Set tab stop width in pixels using font metrics QFontMetrics fm(m_settings->editorFont()); setTabStopDistance(static_cast(tabStop) * fm.horizontalAdvance(' ')); } // --------------------------------------------------------------------------- // File I/O // --------------------------------------------------------------------------- void CodeEditor::loadFile(const QString &filePath) { QFile file(filePath); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { QMessageBox::warning(this, tr("Open File"), tr("Cannot open file:\n%1").arg(filePath)); return; } m_filePath = filePath; QTextStream in(&file); #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) in.setEncoding(QStringConverter::Utf8); #else in.setCodec("UTF-8"); #endif setPlainText(in.readAll()); document()->setModified(false); installHighlighter(filePath); } QString CodeEditor::filePath() const { return m_filePath; } bool CodeEditor::save() { if (m_filePath.isEmpty()) { return saveAs(); } return writeToFile(m_filePath); } bool CodeEditor::saveAs() { const QString path = QFileDialog::getSaveFileName( this, tr("Speichern unter"), m_filePath ); if (path.isEmpty()) { return false; } m_filePath = path; installHighlighter(m_filePath); return writeToFile(m_filePath); } bool CodeEditor::writeToFile(const QString &filePath) { QFile file(filePath); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { QMessageBox::warning(this, tr("Speichern"), tr("Datei konnte nicht gespeichert werden:\n%1").arg(filePath)); return false; } QTextStream out(&file); #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) out.setEncoding(QStringConverter::Utf8); #else out.setCodec("UTF-8"); #endif out << toPlainText(); document()->setModified(false); emit fileSaved(filePath); return true; } void CodeEditor::installHighlighter(const QString &filePath) { // Remove old highlighter first delete m_highlighter; m_highlighter = nullptr; m_highlighter = HighlighterFactory::createForFile(filePath, document()); } bool CodeEditor::isModified() const { return document()->isModified(); } // --------------------------------------------------------------------------- // Line number area // --------------------------------------------------------------------------- int CodeEditor::lineNumberAreaWidth() const { int digits = 1; int max = qMax(1, blockCount()); while (max >= 10) { max /= 10; ++digits; } const int padding = 8; return fontMetrics().horizontalAdvance('9') * digits + padding * 2; } void CodeEditor::updateLineNumberAreaWidth(int /*newBlockCount*/) { setViewportMargins(lineNumberAreaWidth(), 0, 0, 0); } void CodeEditor::updateLineNumberArea(const QRect &rect, int dy) { if (dy != 0) { m_lineNumberArea->scroll(0, dy); } else { m_lineNumberArea->update(0, rect.y(), m_lineNumberArea->width(), rect.height()); } if (rect.contains(viewport()->rect())) { updateLineNumberAreaWidth(0); } } void CodeEditor::paintEvent(QPaintEvent *event) { // Zuerst den normalen Editor-Inhalt zeichnen QPlainTextEdit::paintEvent(event); // Einrück-Führungslinien const int tabSize = m_settings->tabSize(); if (tabSize > 0) { QPainter painter(viewport()); QColor guideColor = palette().color(QPalette::Text); guideColor.setAlpha(30); painter.setPen(QPen(guideColor, 1, Qt::SolidLine)); const QFontMetrics fm(font()); const int spaceWidth = fm.horizontalAdvance(' '); const int tabPixels = tabSize * spaceWidth; if (tabPixels > 0) { int textOriginX = 0; { QTextBlock firstBlock = firstVisibleBlock(); if (!firstBlock.isValid()) { firstBlock = document()->begin(); } if (firstBlock.isValid()) { const QRectF blockRect = blockBoundingGeometry(firstBlock) .translated(contentOffset()); const QTextLayout *layout = firstBlock.layout(); if (layout && layout->lineCount() > 0) { textOriginX = static_cast(blockRect.left() + layout->lineAt(0).position().x()); } else { textOriginX = static_cast(blockRect.left()); } } } const int scrollX = horizontalScrollBar()->value(); QTextBlock block = firstVisibleBlock(); const int bottom = event->rect().bottom(); while (block.isValid()) { const QRectF blockRect = blockBoundingGeometry(block) .translated(contentOffset()); if (blockRect.top() > bottom) { break; } if (block.isVisible()) { const QString text = block.text(); int indentSpaces = 0; for (const QChar &ch : text) { if (ch == ' ') { ++indentSpaces; } else if (ch == '\t') { indentSpaces = ((indentSpaces / tabSize) + 1) * tabSize; } else { break; } } const int indentStops = indentSpaces / tabSize; for (int stop = 1; stop <= indentStops; ++stop) { const int xPixel = textOriginX + stop * tabPixels - scrollX; if (xPixel < lineNumberAreaWidth() || xPixel > viewport()->width()) { continue; } painter.drawLine(xPixel, static_cast(blockRect.top()), xPixel, static_cast(blockRect.bottom())); } } block = block.next(); } } // Farbvorschau-Quadrate zeichnen m_colorIndicator->paint(painter, event); } } void CodeEditor::setFunctionIndex(FunctionIndex *index) { m_functionIndex = index; } void CodeEditor::mouseDoubleClickEvent(QMouseEvent *event) { // Zuerst normales Verhalten — markiert das Wort unter dem Cursor QPlainTextEdit::mouseDoubleClickEvent(event); if (!m_functionIndex || !m_functionIndex->isReady()) { return; } // Markiertes Wort auslesen const QString word = textCursor().selectedText().trimmed(); if (word.isEmpty() || word.contains(' ')) { return; } // Im Funktionsindex nachschlagen const FunctionScanner::FunctionInfo info = m_functionIndex->lookup(word); if (info.filePath.isEmpty()) { return; // Nicht gefunden — normales Verhalten bleibt } // Nicht zur eigenen Definition springen wenn wir bereits dort sind if (info.filePath == m_filePath && info.line == textCursor().blockNumber() + 1) { return; } emit navigateToRequested(info.filePath, info.line); } void CodeEditor::mousePressEvent(QMouseEvent *event) { // Zuerst prüfen ob ein Farbquadrat geklickt wurde if (m_colorIndicator->handleMousePress(event)) { return; } QPlainTextEdit::mousePressEvent(event); } void CodeEditor::resizeEvent(QResizeEvent *event) { QPlainTextEdit::resizeEvent(event); const QRect cr = contentsRect(); m_lineNumberArea->setGeometry( QRect(cr.left(), cr.top(), lineNumberAreaWidth(), cr.height()) ); } void CodeEditor::lineNumberAreaPaintEvent(QPaintEvent *event) { QPainter painter(m_lineNumberArea); // Background const QColor bgColor = palette().color(QPalette::Window).darker(110); painter.fillRect(event->rect(), bgColor); const QColor lineNumColor = palette().color(QPalette::Mid); const QColor activeColor = palette().color(QPalette::Text); const int currentLine = textCursor().blockNumber(); QTextBlock block = firstVisibleBlock(); int blockNumber = block.blockNumber(); int top = static_cast(blockBoundingGeometry(block).translated(contentOffset()).top()); int bottom = top + static_cast(blockBoundingRect(block).height()); while (block.isValid() && top <= event->rect().bottom()) { if (block.isVisible() && bottom >= event->rect().top()) { const QString number = QString::number(blockNumber + 1); painter.setPen(blockNumber == currentLine ? activeColor : lineNumColor); painter.drawText( 0, top, m_lineNumberArea->width() - 4, fontMetrics().height(), Qt::AlignRight, number ); } block = block.next(); top = bottom; bottom = top + static_cast(blockBoundingRect(block).height()); ++blockNumber; } } // --------------------------------------------------------------------------- // Current line highlight + Klammerzugehörigkeit // --------------------------------------------------------------------------- void CodeEditor::highlightCurrentLine() { QList extraSelections; if (!isReadOnly()) { // Aktuelle Zeile hervorheben QTextEdit::ExtraSelection lineSelection; lineSelection.format.setBackground(palette().color(QPalette::AlternateBase)); lineSelection.format.setProperty(QTextFormat::FullWidthSelection, true); lineSelection.cursor = textCursor(); lineSelection.cursor.clearSelection(); extraSelections.append(lineSelection); // Klammerzugehörigkeit matchBrackets(extraSelections); } setExtraSelections(extraSelections); } void CodeEditor::matchBrackets(QList &selections) { static const QString openBrackets = "({["; static const QString closeBrackets = ")}]"; QTextCursor cursor = textCursor(); const QString blockText = cursor.block().text(); const int col = cursor.columnNumber(); // Zeichen unter oder links vom Cursor prüfen QChar ch; int charPos = -1; // Zuerst Zeichen unter dem Cursor if (col < blockText.length()) { ch = blockText[col]; if (openBrackets.contains(ch) || closeBrackets.contains(ch)) { charPos = col; } } // Dann Zeichen links vom Cursor if (charPos == -1 && col > 0) { ch = blockText[col - 1]; if (openBrackets.contains(ch) || closeBrackets.contains(ch)) { charPos = col - 1; } } if (charPos == -1) { return; } // Passende Klammer suchen const bool isOpen = openBrackets.contains(ch); const int bracketIndex = isOpen ? openBrackets.indexOf(ch) : closeBrackets.indexOf(ch); const QChar matchChar = isOpen ? closeBrackets[bracketIndex] : openBrackets[bracketIndex]; // Dokumentposition der gefundenen Klammer const int startPos = cursor.block().position() + charPos; // Passende Klammer suchen — vorwärts oder rückwärts int depth = 0; int matchPos = -1; if (isOpen) { // Vorwärts suchen QTextCursor search(document()); search.setPosition(startPos); while (!search.atEnd()) { search.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor); const QChar c = search.selectedText()[0]; search.clearSelection(); if (c == ch) { ++depth; } else if (c == matchChar) { --depth; if (depth == 0) { matchPos = search.position() - 1; break; } } } } else { // Rückwärts suchen QTextCursor search(document()); search.setPosition(startPos + 1); while (search.position() > 0) { search.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor); const QChar c = search.selectedText()[0]; search.clearSelection(); if (c == ch) { ++depth; } else if (c == matchChar) { --depth; if (depth == 0) { matchPos = search.position(); break; } } } } // Formatierung QTextCharFormat matchFormat; if (matchPos >= 0) { // Gefunden — beide Klammern grün hervorheben matchFormat.setBackground(QColor("#1a5a1a")); matchFormat.setForeground(QColor("#88ff88")); matchFormat.setFontWeight(QFont::Bold); } else { // Kein Match — rot markieren matchFormat.setBackground(QColor("#5a1a1a")); matchFormat.setForeground(QColor("#ff8888")); matchFormat.setFontWeight(QFont::Bold); } // Öffnende / schließende Klammer markieren QTextEdit::ExtraSelection sel1; sel1.cursor = QTextCursor(document()); sel1.cursor.setPosition(startPos); sel1.cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor); sel1.format = matchFormat; selections.append(sel1); // Passende Klammer markieren (nur wenn gefunden) if (matchPos >= 0) { QTextEdit::ExtraSelection sel2; sel2.cursor = QTextCursor(document()); sel2.cursor.setPosition(matchPos); sel2.cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor); sel2.format = matchFormat; selections.append(sel2); } } // --------------------------------------------------------------------------- // Key handling – Tab/Shift+Tab, Smart-Backspace, Auto-Indent // --------------------------------------------------------------------------- void CodeEditor::keyPressEvent(QKeyEvent *event) { const int tabSize = m_settings->tabSize(); // ----------------------------------------------------------------------- // Escape: Signatur-Tooltip sofort schließen (unabhängig vom Cursor- // Kontext). Das Variablen-Popup behandelt Escape bereits selbst weiter // unten in m_varCompleter->handleKeyPress(). // ----------------------------------------------------------------------- if (event->key() == Qt::Key_Escape) { m_signatureHelper->dismiss(); } // ----------------------------------------------------------------------- // Shift+Tab: Einrückung zurückziehen // ----------------------------------------------------------------------- if (event->key() == Qt::Key_Backtab || (event->key() == Qt::Key_Tab && event->modifiers() & Qt::ShiftModifier)) { QTextCursor cursor = textCursor(); QTextBlock startBlock = document()->findBlock(cursor.selectionStart()); QTextBlock endBlock = document()->findBlock(cursor.selectionEnd()); // Wenn die Selektion genau am Anfang des letzten Blocks endet, // diesen Block nicht mit einbeziehen — der Cursor steht dort nur // mit Position 0, der Nutzer hat die Zeile nicht markiert if (cursor.hasSelection() && cursor.selectionEnd() == endBlock.position() && endBlock != startBlock) { endBlock = endBlock.previous(); } cursor.beginEditBlock(); for (QTextBlock b = startBlock; b != endBlock.next(); b = b.next()) { const QString lineText = b.text(); int toRemove = 0; if (m_settings->useSpacesForTabs()) { for (int i = 0; i < tabSize && i < lineText.length(); ++i) { if (lineText[i] == ' ') { ++toRemove; } else { break; } } } else { if (!lineText.isEmpty() && lineText[0] == '\t') { toRemove = 1; } } if (toRemove > 0) { QTextCursor lineCursor(b); lineCursor.movePosition(QTextCursor::StartOfBlock); lineCursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, toRemove); lineCursor.removeSelectedText(); } } cursor.endEditBlock(); return; } // ----------------------------------------------------------------------- // Tab: Einrücken (Leerzeichen oder echter Tab) // ----------------------------------------------------------------------- if (event->key() == Qt::Key_Tab) { QTextCursor cursor = textCursor(); if (cursor.hasSelection()) { QTextBlock startBlock = document()->findBlock(cursor.selectionStart()); QTextBlock endBlock = document()->findBlock(cursor.selectionEnd()); // Gleiche Korrektur: Cursor am Zeilenanfang → Zeile nicht einrücken if (cursor.selectionEnd() == endBlock.position() && endBlock != startBlock) { endBlock = endBlock.previous(); } cursor.beginEditBlock(); for (QTextBlock b = startBlock; b != endBlock.next(); b = b.next()) { QTextCursor lineCursor(b); lineCursor.movePosition(QTextCursor::StartOfBlock); if (m_settings->useSpacesForTabs()) { lineCursor.insertText(QString(tabSize, ' ')); } else { lineCursor.insertText("\t"); } } cursor.endEditBlock(); } else { if (m_settings->useSpacesForTabs()) { // Zum nächsten Tab-Stop auffüllen const int col = cursor.columnNumber(); const int spacesNeeded = tabSize - (col % tabSize); cursor.insertText(QString(spacesNeeded, ' ')); } else { cursor.insertText("\t"); } } return; } // ----------------------------------------------------------------------- // Smart Backspace: springt zur vorherigen Einrückungsstufe // ----------------------------------------------------------------------- if (event->key() == Qt::Key_Backspace && !textCursor().hasSelection() && m_settings->useSpacesForTabs()) { QTextCursor cursor = textCursor(); const int col = cursor.columnNumber(); if (col > 0) { // Prüfen ob links vom Cursor nur Leerzeichen bis Zeilenbeginn stehen const QString lineText = cursor.block().text(); const QString leftOfCursor = lineText.left(col); const bool onlySpaces = leftOfCursor.trimmed().isEmpty(); if (onlySpaces && col > 0) { // Zur vorherigen Tab-Stop-Position springen const int targetCol = ((col - 1) / tabSize) * tabSize; const int toDelete = col - targetCol; cursor.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor, toDelete); cursor.removeSelectedText(); return; } } } // ----------------------------------------------------------------------- // Enter / Return: Auto-Indent // ----------------------------------------------------------------------- if (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) { QTextCursor cursor = textCursor(); const QString currentLine = cursor.block().text(); // Führende Leerzeichen der aktuellen Zeile zählen int leadingSpaces = 0; for (const QChar &ch : currentLine) { if (ch == ' ') { ++leadingSpaces; } else if (ch == '\t') { leadingSpaces += tabSize; } else { break; } } QPlainTextEdit::keyPressEvent(event); if (leadingSpaces > 0) { const QString indent = m_settings->useSpacesForTabs() ? QString(leadingSpaces, ' ') : QString(leadingSpaces / tabSize, '\t'); textCursor().insertText(indent); } return; } QPlainTextEdit::keyPressEvent(event); // Variablen-Popup nach jedem Tastendruck aktualisieren m_varCompleter->handleKeyPress(event); } // --------------------------------------------------------------------------- // Fokus verloren — z. B. beim Wechsel zu einem anderen Tab. // Ein noch offenes Variablen-Popup muss hier geschlossen werden, sonst // bleibt es als verwaistes Fenster stehen und kann bei mehreren offenen // Dateien den Fokus blockieren. // --------------------------------------------------------------------------- void CodeEditor::focusOutEvent(QFocusEvent *event) { QPlainTextEdit::focusOutEvent(event); m_varCompleter->notifyFocusLost(); m_signatureHelper->notifyFocusLost(); }