diff --git a/CMakeLists.txt b/CMakeLists.txt index 2b8c544..793ef8c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.16) project(BareCode - VERSION 1.1.0 + VERSION 1.2.0 DESCRIPTION "A modular code editor built with Qt6" LANGUAGES CXX ) diff --git a/PKGBUILD b/PKGBUILD index ed944a0..bd36be7 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -2,7 +2,7 @@ # Projekt Hirnfrei - https://www.projekt-hirnfrei.de pkgname=barecode -pkgver=1.1.0 +pkgver=1.2.0 pkgrel=1 pkgdesc="Schlanker modularer Code-Editor für HTML, PHP, CSS und C++" arch=('x86_64' 'aarch64') @@ -13,10 +13,10 @@ makedepends=('cmake' 'ninja') provides=('barecode') conflicts=('barecode-git') source=("$pkgname-$pkgver.tar.gz::https://git.projekt-hirnfrei.de/diabolus/BareCode/archive/v$pkgver.tar.gz") -sha256sums=('04b11561920101b0444e5427e50658886d1fc62018426586fe512e405f2dc0e6') # Nach erstem Download ersetzen: sha256sum barecode-1.1.0.tar.gz +sha256sums=('SKIP') # Nach erstem Download ersetzen: sha256sum barecode-1.1.0.tar.gz build() { - cd "barecode" + cd "BareCode" cmake -B build \ -G Ninja \ -DCMAKE_BUILD_TYPE=Release \ @@ -30,7 +30,7 @@ check() { } package() { - cd "barecode" + cd "BareCode" DESTDIR="$pkgdir" cmake --install build install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE" } diff --git a/PKGBUILD-git b/PKGBUILD-git index 81b1310..566f386 100644 --- a/PKGBUILD-git +++ b/PKGBUILD-git @@ -13,7 +13,7 @@ makedepends=('cmake' 'ninja' 'git') provides=('barecode') conflicts=('barecode') source=("$pkgname::git+https://git.projekt-hirnfrei.de/diabolus/BareCode.git") -sha256sums=('04b11561920101b0444e5427e50658886d1fc62018426586fe512e405f2dc0e6') +sha256sums=('SKIP') pkgver() { cd "$pkgname" diff --git a/README.md b/README.md index ed5dad2..94dacb8 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # BareCode **Schlanker, modularer Code-Editor für Web-Entwickler** -Version 1.1.0 — von **Dany Thinnes** | [Projekt Hirnfrei](https://www.projekt-hirnfrei.de) | [Discord](https://discord.projekt-hirnfrei.de) +Version 1.2.0 — von **Dany Thinnes** | [Projekt Hirnfrei](https://www.projekt-hirnfrei.de) | [Discord](https://discord.projekt-hirnfrei.de) Entwickelt mit **C++17**, **Qt6** und **CMake**. Kompiliert auf **Linux**, **Windows** und **Haiku**. @@ -34,17 +34,19 @@ Siehe Abschnitt **Bauen** weiter unten. ### Editor | Feature | Details | |---|---| -| Syntax-Highlighting | HTML, PHP, CSS, C/C++ — inkl. mehrzeiliger Kommentare | +| Syntax-Highlighting | HTML, PHP, CSS, C/C++ — inkl. mehrzeiliger Kommentare über Leerzeilen | | Tabs | Mehrere Dateien gleichzeitig, verschiebbar und schließbar | | Änderungsindikator | ● im Tab-Titel bei ungespeicherten Änderungen | | Zeilennummern | Eigener Gutter, aktuelle Zeile hervorgehoben | | Einrück-Führungslinien | Vertikale Linien wie in VS Code | +| Klammerzugehörigkeit | Zusammengehörige Klammern werden grün hervorgehoben, fehlende rot | | Auto-Indent | Einrückungstiefe wird bei Enter übernommen | -| Tab / Shift+Tab | Ein- und Ausrücken, auch für mehrere Zeilen gleichzeitig | +| Tab / Shift+Tab | Ein- und Ausrücken für einzelne Zeilen und Selektionen | | Smart Backspace | Springt zur vorherigen Tab-Stop-Position | | Farbvorschau | CSS-Farbwerte (#hex, rgb, hsl) werden inline angezeigt | | Farbauswahl | Klick auf das Farbquadrat öffnet einen Farbauswahl-Dialog | | Funktionssignatur | Tooltip mit Parametern beim Tippen von PHP-Funktionen | +| Variablen-Popup | Vorschläge für $variablen beim Tippen (nur Anzeige, kein Autocomplete) | ### Navigation | Feature | Details | @@ -109,14 +111,15 @@ BareCode/ ├── editor/ # EditorPanel, EditorTab, CodeEditor, │ # SearchPanel, FileSearchPanel, │ # ColorIndicator, SignatureHelper, - │ # FunctionScanner, FunctionIndex, - │ # FunctionListPanel, FunctionListDialog, - │ # DeadCodeAnalyzer, DeadCodeDialog + │ # VariableCompleter, FunctionScanner, + │ # FunctionIndex, FunctionListPanel, + │ # FunctionListDialog, DeadCodeAnalyzer, + │ # DeadCodeDialog ├── filetree/ # FileTreePanel └── highlighter/ # SyntaxHighlighter, HighlighterFactory ``` -Jedes Unterverzeichnis kompiliert als eigene statische Bibliothek — Komponenten lassen sich unabhängig austauschen und erweitern. +Jedes Unterverzeichnis kompiliert als eigene statische Bibliothek. --- @@ -160,11 +163,6 @@ cmake -B build -G "Visual Studio 17 2022" -A x64 cmake --build build --config Release ``` -**Installer erstellen** (mit [Inno Setup](https://jrsoftware.org/isinfo.php)): -```bat -windeployqt --release build\Release\BareCode.exe -``` - --- ### Haiku @@ -176,8 +174,7 @@ cmake --build build -j$(nproc) ./build/BareCode ``` -> Getestet auf Haiku R1/beta5. `qt6_base_devel` enthält auf Haiku -> sowohl die Header als auch die Build-Tools. +> Getestet auf Haiku R1/beta5. --- @@ -187,8 +184,6 @@ cmake --build build -j$(nproc) 2. `m_rules` im Konstruktor befüllen (siehe `CppHighlighter` als Vorlage) 3. Dateiendung in `HighlighterFactory.cpp` registrieren -Keine weiteren Dateien müssen geändert werden. - --- ## Lizenz diff --git a/barecode-1.1.0.tar.gz b/barecode-1.1.0.tar.gz deleted file mode 100644 index 25212fe..0000000 Binary files a/barecode-1.1.0.tar.gz and /dev/null differ diff --git a/main.cpp b/main.cpp index babe9b5..798cde8 100644 --- a/main.cpp +++ b/main.cpp @@ -7,7 +7,7 @@ int main(int argc, char *argv[]) QApplication app(argc, argv); app.setApplicationName("BareCode"); - app.setApplicationVersion("1.1.0"); + app.setApplicationVersion("1.2.0"); app.setOrganizationName("BareCode"); // Icon in allen verfügbaren Größen setzen diff --git a/src/core/AboutDialog.cpp b/src/core/AboutDialog.cpp index 9feeca1..d6621d7 100644 --- a/src/core/AboutDialog.cpp +++ b/src/core/AboutDialog.cpp @@ -74,7 +74,7 @@ AboutDialog::AboutDialog(QWidget *parent) info->addLayout(row); }; - makeRow(tr("Version"), "1.1.0"); + makeRow(tr("Version"), "1.2.0"); makeRow(tr("Entwickler"), "Dany Thinnes"); makeRow(tr("Projekt"), "Projekt Hirnfrei"); makeRow(tr("Framework"), QString("Qt %1").arg(qVersion())); diff --git a/src/editor/CMakeLists.txt b/src/editor/CMakeLists.txt index b889116..c730a1f 100644 --- a/src/editor/CMakeLists.txt +++ b/src/editor/CMakeLists.txt @@ -29,6 +29,8 @@ set(EDITOR_SOURCES DeadCodeAnalyzer.h DeadCodeDialog.cpp DeadCodeDialog.h + VariableCompleter.cpp + VariableCompleter.h ) add_library(BareCode_Editor STATIC ${EDITOR_SOURCES}) diff --git a/src/editor/CodeEditor.cpp b/src/editor/CodeEditor.cpp index 3f2701c..70a06d4 100644 --- a/src/editor/CodeEditor.cpp +++ b/src/editor/CodeEditor.cpp @@ -3,6 +3,7 @@ #include "ColorIndicator.h" #include "SignatureHelper.h" #include "FunctionIndex.h" +#include "VariableCompleter.h" #include "core/Settings.h" #include "highlighter/HighlighterFactory.h" @@ -27,6 +28,7 @@ CodeEditor::CodeEditor(Settings *settings, QWidget *parent) 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, @@ -390,7 +392,7 @@ void CodeEditor::lineNumberAreaPaintEvent(QPaintEvent *event) } // --------------------------------------------------------------------------- -// Current line highlight +// Current line highlight + Klammerzugehörigkeit // --------------------------------------------------------------------------- void CodeEditor::highlightCurrentLine() { @@ -398,20 +400,161 @@ void CodeEditor::highlightCurrentLine() if (!isReadOnly()) { - QTextEdit::ExtraSelection selection; + // 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); - const QColor lineColor = palette().color(QPalette::AlternateBase); - selection.format.setBackground(lineColor); - selection.format.setProperty(QTextFormat::FullWidthSelection, true); - selection.cursor = textCursor(); - selection.cursor.clearSelection(); - - extraSelections.append(selection); + // 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 // --------------------------------------------------------------------------- @@ -430,6 +573,16 @@ void CodeEditor::keyPressEvent(QKeyEvent *event) 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()) { @@ -438,22 +591,14 @@ void CodeEditor::keyPressEvent(QKeyEvent *event) if (m_settings->useSpacesForTabs()) { - // Bis zu tabSize führende Leerzeichen entfernen for (int i = 0; i < tabSize && i < lineText.length(); ++i) { - if (lineText[i] == ' ') - { - ++toRemove; - } - else - { - break; - } + if (lineText[i] == ' ') { ++toRemove; } + else { break; } } } else { - // Einen führenden Tab entfernen if (!lineText.isEmpty() && lineText[0] == '\t') { toRemove = 1; @@ -483,10 +628,16 @@ void CodeEditor::keyPressEvent(QKeyEvent *event) if (cursor.hasSelection()) { - // Mehrere Zeilen einrücken 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()) { @@ -591,5 +742,8 @@ void CodeEditor::keyPressEvent(QKeyEvent *event) } QPlainTextEdit::keyPressEvent(event); + + // Variablen-Popup nach jedem Tastendruck aktualisieren + m_varCompleter->handleKeyPress(event); } diff --git a/src/editor/CodeEditor.h b/src/editor/CodeEditor.h index 24f94aa..9e24905 100644 --- a/src/editor/CodeEditor.h +++ b/src/editor/CodeEditor.h @@ -11,6 +11,7 @@ class SyntaxHighlighter; class ColorIndicator; class SignatureHelper; class FunctionIndex; +class VariableCompleter; // --------------------------------------------------------------------------- // CodeEditor – Core editing widget. @@ -55,8 +56,7 @@ signals: void fileSaved(const QString &filePath); void navigateToRequested(const QString &filePath, int line); -protected: - void resizeEvent(QResizeEvent *event) override; +protected: void resizeEvent(QResizeEvent *event) override; void keyPressEvent(QKeyEvent *event) override; void paintEvent(QPaintEvent *event) override; void mousePressEvent(QMouseEvent *event) override; @@ -71,6 +71,7 @@ private: void setupEditor(); void installHighlighter(const QString &filePath); bool writeToFile(const QString &filePath); + void matchBrackets(QList &selections); Settings *m_settings = nullptr; LineNumberArea *m_lineNumberArea = nullptr; @@ -78,5 +79,6 @@ private: ColorIndicator *m_colorIndicator = nullptr; SignatureHelper *m_signatureHelper = nullptr; FunctionIndex *m_functionIndex = nullptr; + VariableCompleter *m_varCompleter = nullptr; QString m_filePath; }; diff --git a/src/editor/VariableCompleter.cpp b/src/editor/VariableCompleter.cpp new file mode 100644 index 0000000..021ad58 --- /dev/null +++ b/src/editor/VariableCompleter.cpp @@ -0,0 +1,243 @@ +#include "VariableCompleter.h" +#include "CodeEditor.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +VariableCompleter::VariableCompleter(CodeEditor *editor) + : QObject(editor) + , m_editor(editor) +{ + // Popup als eigenständiges, rahmenloses Werkzeugfenster + m_popup = new QListWidget(nullptr); + m_popup->setWindowFlags(Qt::Tool | Qt::FramelessWindowHint | + Qt::WindowStaysOnTopHint); + m_popup->setAttribute(Qt::WA_ShowWithoutActivating); + m_popup->setFocusPolicy(Qt::NoFocus); + m_popup->setMaximumHeight(200); + m_popup->setMinimumWidth(180); + m_popup->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + m_popup->setStyleSheet( + "QListWidget {" + " border: 1px solid palette(mid);" + " background: palette(toolTipBase);" + " color: palette(toolTipText);" + " font-family: Monospace;" + "}" + "QListWidget::item:selected {" + " background: palette(highlight);" + " color: palette(highlightedText);" + "}" + ); + + connect(m_popup, &QListWidget::itemActivated, + this, &VariableCompleter::onItemActivated); +} + +// --------------------------------------------------------------------------- +// Nach jedem Tastendruck aufrufen +// --------------------------------------------------------------------------- +void VariableCompleter::handleKeyPress(QKeyEvent *event) +{ + // Popup-Navigation + if (m_popup->isVisible()) + { + if (event->key() == Qt::Key_Escape) + { + hidePopup(); + return; + } + if (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) + { + if (m_popup->currentItem()) + { + onItemActivated(m_popup->currentItem()); + } + hidePopup(); + return; + } + if (event->key() == Qt::Key_Down) + { + const int next = qMin(m_popup->currentRow() + 1, + m_popup->count() - 1); + m_popup->setCurrentRow(next); + return; + } + if (event->key() == Qt::Key_Up) + { + const int prev = qMax(m_popup->currentRow() - 1, 0); + m_popup->setCurrentRow(prev); + return; + } + } + + // Nach dem normalen Tastendruck Popup aktualisieren + // (der Event wurde bereits an QPlainTextEdit weitergegeben) + updatePopup(); +} + +// --------------------------------------------------------------------------- +// Popup aktualisieren +// --------------------------------------------------------------------------- +void VariableCompleter::updatePopup() +{ + const QString prefix = currentPrefix(); + + // Nur bei $ und mindestens einem weiteren Zeichen anzeigen + if (prefix.length() < 2 || !prefix.startsWith('$')) + { + hidePopup(); + return; + } + + const QStringList allVars = collectVariables(); + const QString filter = prefix.toLower(); + + QStringList matches; + for (const QString &var : allVars) + { + if (var.toLower().startsWith(filter) && var != prefix) + { + matches.append(var); + } + } + + if (matches.isEmpty()) + { + hidePopup(); + return; + } + + // Popup befüllen + m_popup->clear(); + for (const QString &var : matches) + { + m_popup->addItem(var); + } + m_popup->setCurrentRow(0); + + // Größe anpassen + const int itemHeight = m_popup->sizeHintForRow(0) + 2; + const int height = qMin(matches.size() * itemHeight + 4, 200); + m_popup->setFixedHeight(height); + + // Position unter dem Cursor + const QRect cursorRect = m_editor->cursorRect(); + QPoint pos = m_editor->viewport()->mapToGlobal( + QPoint(cursorRect.left(), cursorRect.bottom() + 2) + ); + + // Nicht aus dem Bildschirm herausragen + const QRect screen = QApplication::primaryScreen()->availableGeometry(); + if (pos.x() + m_popup->width() > screen.right()) + { + pos.setX(screen.right() - m_popup->width()); + } + if (pos.y() + height > screen.bottom()) + { + pos.setY(cursorRect.top() - height - 2); + } + + m_popup->move(pos); + m_popup->show(); + m_popup->raise(); +} + +void VariableCompleter::hidePopup() +{ + m_popup->hide(); + m_popup->clear(); +} + +// --------------------------------------------------------------------------- +// Variablen im aktuellen Dokument sammeln +// --------------------------------------------------------------------------- +QStringList VariableCompleter::collectVariables() const +{ + static const QRegularExpression varRegex(R"(\$[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)"); + + QSet seen; + QStringList result; + + const QString text = m_editor->toPlainText(); + QRegularExpressionMatchIterator it = varRegex.globalMatch(text); + + while (it.hasNext()) + { + const QString var = it.next().captured(0); + if (!seen.contains(var)) + { + seen.insert(var); + result.append(var); + } + } + + result.sort(Qt::CaseInsensitive); + return result; +} + +// --------------------------------------------------------------------------- +// Text links vom Cursor — das angefangene $variable +// --------------------------------------------------------------------------- +QString VariableCompleter::currentPrefix() const +{ + const QTextCursor cursor = m_editor->textCursor(); + const QString line = cursor.block().text(); + const int col = cursor.columnNumber(); + + if (col == 0) + { + return QString(); + } + + // Rückwärts gehen bis zum $ oder einem Nicht-Bezeichner-Zeichen + int start = col - 1; + while (start > 0) + { + const QChar c = line[start - 1]; + if (!c.isLetterOrNumber() && c != '_' && c != '$') + { + break; + } + --start; + } + + const QString token = line.mid(start, col - start); + return token.startsWith('$') ? token : QString(); +} + +// --------------------------------------------------------------------------- +// Klick oder Enter — Variable einfügen +// --------------------------------------------------------------------------- +void VariableCompleter::onItemActivated(QListWidgetItem *item) +{ + if (!item) + { + return; + } + + const QString selected = item->text(); + const QString prefix = currentPrefix(); + + if (prefix.isEmpty()) + { + hidePopup(); + return; + } + + // Prefix durch den vollständigen Variablennamen ersetzen + QTextCursor cursor = m_editor->textCursor(); + cursor.movePosition(QTextCursor::Left, + QTextCursor::KeepAnchor, + prefix.length()); + cursor.insertText(selected); + + m_editor->setTextCursor(cursor); + hidePopup(); +} diff --git a/src/editor/VariableCompleter.h b/src/editor/VariableCompleter.h new file mode 100644 index 0000000..2c67bd9 --- /dev/null +++ b/src/editor/VariableCompleter.h @@ -0,0 +1,38 @@ +#pragma once + +#include +#include +#include +#include + +class CodeEditor; + +// --------------------------------------------------------------------------- +// VariableCompleter – Zeigt ein Popup mit passenden Variablennamen +// wenn der Nutzer $ tippt und weiter eingibt. +// +// Kein Autocomplete — nur Anzeige. Klick oder Enter übernimmt. +// Escape oder Weiterschreiben ohne Treffer schließt das Popup. +// --------------------------------------------------------------------------- +class VariableCompleter : public QObject +{ + Q_OBJECT + +public: + explicit VariableCompleter(CodeEditor *editor); + + // Muss nach jedem Tastendruck aufgerufen werden + void handleKeyPress(QKeyEvent *event); + +private slots: + void onItemActivated(QListWidgetItem *item); + +private: + void updatePopup(); + void hidePopup(); + QStringList collectVariables() const; + QString currentPrefix() const; // "$me" etc. links vom Cursor + + CodeEditor *m_editor = nullptr; + QListWidget *m_popup = nullptr; +}; diff --git a/src/highlighter/SyntaxHighlighter.cpp b/src/highlighter/SyntaxHighlighter.cpp index 2a93936..4658293 100644 --- a/src/highlighter/SyntaxHighlighter.cpp +++ b/src/highlighter/SyntaxHighlighter.cpp @@ -11,6 +11,7 @@ SyntaxHighlighter::SyntaxHighlighter(QTextDocument *parent) void SyntaxHighlighter::highlightBlock(const QString &text) { + // Einzel-Zeilen-Regeln anwenden for (const HighlightRule &rule : m_rules) { QRegularExpressionMatchIterator it = rule.pattern.globalMatch(text); @@ -27,21 +28,65 @@ void SyntaxHighlighter::highlightBlock(const QString &text) if (!m_hasMultiLineComment) { + setCurrentBlockState(0); return; } + // Mehrzeilige Kommentare + // Zustand 0 = normal, 1 = mitten in /* ... */ setCurrentBlockState(0); - int startIndex = 0; - if (previousBlockState() != 1) + // Leere Zeile mitten im Kommentar — Zustand weitertragen + if (text.isEmpty()) { + if (previousBlockState() == 1) + { + setCurrentBlockState(1); + } + return; + } + + int startIndex = 0; + + if (previousBlockState() == 1) + { + // Vorherige Zeile war mitten im Kommentar — ab Zeilenanfang nach Ende suchen + QRegularExpressionMatch endMatch = m_commentEndExpression.match(text, 0); + + if (endMatch.hasMatch()) + { + // Ende des Kommentars gefunden + const int commentLength = static_cast(endMatch.capturedStart()) + + static_cast(endMatch.capturedLength()); + setFormat(0, commentLength, m_multiLineCommentFormat); + + // Nach weiteren Kommentaren in der gleichen Zeile suchen + QRegularExpressionMatch nextStart = + m_commentStartExpression.match(text, commentLength); + startIndex = nextStart.hasMatch() + ? static_cast(nextStart.capturedStart()) + : -1; + } + else + { + // Noch kein Ende — gesamte Zeile ist Kommentar + setFormat(0, text.length(), m_multiLineCommentFormat); + setCurrentBlockState(1); + return; + } + } + else + { + // Neuen Kommentaranfang suchen QRegularExpressionMatch m = m_commentStartExpression.match(text); startIndex = m.hasMatch() ? static_cast(m.capturedStart()) : -1; } while (startIndex >= 0) { - QRegularExpressionMatch endMatch = m_commentEndExpression.match(text, startIndex); + QRegularExpressionMatch endMatch = + m_commentEndExpression.match(text, startIndex); + int commentLength = 0; if (endMatch.hasMatch()) @@ -52,6 +97,7 @@ void SyntaxHighlighter::highlightBlock(const QString &text) } else { + // Kommentar geht über Zeilenende setCurrentBlockState(1); commentLength = text.length() - startIndex; } @@ -63,6 +109,7 @@ void SyntaxHighlighter::highlightBlock(const QString &text) break; } + // Nach weiteren Kommentaren in dieser Zeile suchen QRegularExpressionMatch nextStart = m_commentStartExpression.match(text, startIndex + commentLength); startIndex = nextStart.hasMatch() @@ -374,14 +421,22 @@ void PhpHighlighter::highlightPhpRange(const QString &text, int start, int lengt } } - // /* */ Block-Kommentare innerhalb des PHP-Bereichs mehrzeilig behandeln. - // Block-Zustand 3 = wir sind mitten in einem /* ... */ PHP-Kommentar. static const QRegularExpression blockOpen(R"(/\*)"); static const QRegularExpression blockClose(R"(\*/)"); int searchFrom = 0; - // Waren wir bereits in einem Block-Kommentar? + // Leere Zeile mitten im PHP-Blockkommentar — Zustand weitertragen + if (phpText.trimmed().isEmpty()) + { + if (previousBlockState() == 3) + { + setFormat(start, length, m_phpCommentFormat); + setCurrentBlockState(3); + } + return; + } + if (previousBlockState() == 3) { QRegularExpressionMatch closeMatch = blockClose.match(phpText, 0); @@ -391,18 +446,15 @@ void PhpHighlighter::highlightPhpRange(const QString &text, int start, int lengt + static_cast(closeMatch.capturedLength()); setFormat(start, end, m_phpCommentFormat); searchFrom = end; - // Block-Kommentar geschlossen — Zustand wird weiter unten gesetzt } else { - // Gesamter Bereich ist noch Kommentar setFormat(start, length, m_phpCommentFormat); setCurrentBlockState(3); return; } } - // Neue /* ... */ Kommentare innerhalb dieses PHP-Bereichs suchen while (searchFrom < phpText.length()) { QRegularExpressionMatch openMatch = blockOpen.match(phpText, searchFrom); @@ -423,7 +475,6 @@ void PhpHighlighter::highlightPhpRange(const QString &text, int start, int lengt } else { - // Kein schließendes */ gefunden — geht über Zeilenende setFormat(start + openPos, length - openPos, m_phpCommentFormat); setCurrentBlockState(3); return; @@ -434,13 +485,39 @@ void PhpHighlighter::highlightPhpRange(const QString &text, int start, int lengt void PhpHighlighter::highlightBlock(const QString &text) { // Zuerst HTML-Basis-Regeln auf den gesamten Text anwenden + // (setzt auch den State für HTML Kommentare) SyntaxHighlighter::highlightBlock(text); // Block-Zustände: // 0 = HTML-Modus + // 1 = HTML Kommentar (von Basisklasse verwaltet) // 2 = innerhalb PHP-Block (kein /* */ Kommentar) // 3 = innerhalb PHP /* */ Block-Kommentar - setCurrentBlockState(0); + + // State 1 (HTML-Kommentar) wurde von der Basisklasse gesetzt — nicht überschreiben + if (currentBlockState() == 1) + { + return; + } + + // Wenn der vorherige Block ein HTML-Kommentar war und dieser noch nicht + // abgeschlossen wurde, hat die Basisklasse das bereits korrekt behandelt. + // Wir setzen nur dann auf 0 zurück wenn wir sicher nicht in HTML-Kommentar sind. + if (previousBlockState() != 1) + { + setCurrentBlockState(0); + } + + // Leere Zeile — Zustand weitertragen + if (text.isEmpty()) + { + const int prev = previousBlockState(); + if (prev == 2 || prev == 3) + { + setCurrentBlockState(prev); + } + return; + } static const QRegularExpression phpOpen(R"(<\?(?:php|=)?\s?)", QRegularExpression::CaseInsensitiveOption); @@ -466,8 +543,6 @@ void PhpHighlighter::highlightBlock(const QString &text) else { highlightPhpRange(text, 0, text.length()); - // highlightPhpRange setzt den Zustand auf 3 falls nötig, - // sonst behalten wir 2 (offener PHP-Block ohne Kommentar) if (currentBlockState() != 3) { setCurrentBlockState(2);