- Fehler in mehrzeiligen Kommentaren behoben

- Klammerpaare werden farblich hervorgehoben
- Beim einrücken oder ausrücken eines markierten Blocks wird die Zeile, in der der Coursor steht, nicht mehr mitgerückt
- Beim eintippen von Variablen wird eine Liste mit bereits deklarierten Variablen angezeigt
This commit is contained in:
2026-08-21 22:58:08 +02:00
parent aefa627e42
commit 53608725aa
13 changed files with 568 additions and 59 deletions

View File

@@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.16) cmake_minimum_required(VERSION 3.16)
project(BareCode project(BareCode
VERSION 1.1.0 VERSION 1.2.0
DESCRIPTION "A modular code editor built with Qt6" DESCRIPTION "A modular code editor built with Qt6"
LANGUAGES CXX LANGUAGES CXX
) )

View File

@@ -2,7 +2,7 @@
# Projekt Hirnfrei - https://www.projekt-hirnfrei.de # Projekt Hirnfrei - https://www.projekt-hirnfrei.de
pkgname=barecode pkgname=barecode
pkgver=1.1.0 pkgver=1.2.0
pkgrel=1 pkgrel=1
pkgdesc="Schlanker modularer Code-Editor für HTML, PHP, CSS und C++" pkgdesc="Schlanker modularer Code-Editor für HTML, PHP, CSS und C++"
arch=('x86_64' 'aarch64') arch=('x86_64' 'aarch64')
@@ -13,10 +13,10 @@ makedepends=('cmake' 'ninja')
provides=('barecode') provides=('barecode')
conflicts=('barecode-git') conflicts=('barecode-git')
source=("$pkgname-$pkgver.tar.gz::https://git.projekt-hirnfrei.de/diabolus/BareCode/archive/v$pkgver.tar.gz") 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() { build() {
cd "barecode" cd "BareCode"
cmake -B build \ cmake -B build \
-G Ninja \ -G Ninja \
-DCMAKE_BUILD_TYPE=Release \ -DCMAKE_BUILD_TYPE=Release \
@@ -30,7 +30,7 @@ check() {
} }
package() { package() {
cd "barecode" cd "BareCode"
DESTDIR="$pkgdir" cmake --install build DESTDIR="$pkgdir" cmake --install build
install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE" install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE"
} }

View File

@@ -13,7 +13,7 @@ makedepends=('cmake' 'ninja' 'git')
provides=('barecode') provides=('barecode')
conflicts=('barecode') conflicts=('barecode')
source=("$pkgname::git+https://git.projekt-hirnfrei.de/diabolus/BareCode.git") source=("$pkgname::git+https://git.projekt-hirnfrei.de/diabolus/BareCode.git")
sha256sums=('04b11561920101b0444e5427e50658886d1fc62018426586fe512e405f2dc0e6') sha256sums=('SKIP')
pkgver() { pkgver() {
cd "$pkgname" cd "$pkgname"

View File

@@ -1,7 +1,7 @@
# BareCode # BareCode
**Schlanker, modularer Code-Editor für Web-Entwickler** **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**. Entwickelt mit **C++17**, **Qt6** und **CMake**.
Kompiliert auf **Linux**, **Windows** und **Haiku**. Kompiliert auf **Linux**, **Windows** und **Haiku**.
@@ -34,17 +34,19 @@ Siehe Abschnitt **Bauen** weiter unten.
### Editor ### Editor
| Feature | Details | | 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 | | Tabs | Mehrere Dateien gleichzeitig, verschiebbar und schließbar |
| Änderungsindikator | ● im Tab-Titel bei ungespeicherten Änderungen | | Änderungsindikator | ● im Tab-Titel bei ungespeicherten Änderungen |
| Zeilennummern | Eigener Gutter, aktuelle Zeile hervorgehoben | | Zeilennummern | Eigener Gutter, aktuelle Zeile hervorgehoben |
| Einrück-Führungslinien | Vertikale Linien wie in VS Code | | 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 | | 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 | | Smart Backspace | Springt zur vorherigen Tab-Stop-Position |
| Farbvorschau | CSS-Farbwerte (#hex, rgb, hsl) werden inline angezeigt | | Farbvorschau | CSS-Farbwerte (#hex, rgb, hsl) werden inline angezeigt |
| Farbauswahl | Klick auf das Farbquadrat öffnet einen Farbauswahl-Dialog | | Farbauswahl | Klick auf das Farbquadrat öffnet einen Farbauswahl-Dialog |
| Funktionssignatur | Tooltip mit Parametern beim Tippen von PHP-Funktionen | | Funktionssignatur | Tooltip mit Parametern beim Tippen von PHP-Funktionen |
| Variablen-Popup | Vorschläge für $variablen beim Tippen (nur Anzeige, kein Autocomplete) |
### Navigation ### Navigation
| Feature | Details | | Feature | Details |
@@ -109,14 +111,15 @@ BareCode/
├── editor/ # EditorPanel, EditorTab, CodeEditor, ├── editor/ # EditorPanel, EditorTab, CodeEditor,
│ # SearchPanel, FileSearchPanel, │ # SearchPanel, FileSearchPanel,
│ # ColorIndicator, SignatureHelper, │ # ColorIndicator, SignatureHelper,
│ # FunctionScanner, FunctionIndex, │ # VariableCompleter, FunctionScanner,
│ # FunctionListPanel, FunctionListDialog, │ # FunctionIndex, FunctionListPanel,
│ # DeadCodeAnalyzer, DeadCodeDialog │ # FunctionListDialog, DeadCodeAnalyzer,
│ # DeadCodeDialog
├── filetree/ # FileTreePanel ├── filetree/ # FileTreePanel
└── highlighter/ # SyntaxHighlighter, HighlighterFactory └── 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 cmake --build build --config Release
``` ```
**Installer erstellen** (mit [Inno Setup](https://jrsoftware.org/isinfo.php)):
```bat
windeployqt --release build\Release\BareCode.exe
```
--- ---
### Haiku ### Haiku
@@ -176,8 +174,7 @@ cmake --build build -j$(nproc)
./build/BareCode ./build/BareCode
``` ```
> Getestet auf Haiku R1/beta5. `qt6_base_devel` enthält auf Haiku > Getestet auf Haiku R1/beta5.
> sowohl die Header als auch die Build-Tools.
--- ---
@@ -187,8 +184,6 @@ cmake --build build -j$(nproc)
2. `m_rules` im Konstruktor befüllen (siehe `CppHighlighter` als Vorlage) 2. `m_rules` im Konstruktor befüllen (siehe `CppHighlighter` als Vorlage)
3. Dateiendung in `HighlighterFactory.cpp` registrieren 3. Dateiendung in `HighlighterFactory.cpp` registrieren
Keine weiteren Dateien müssen geändert werden.
--- ---
## Lizenz ## Lizenz

Binary file not shown.

View File

@@ -7,7 +7,7 @@ int main(int argc, char *argv[])
QApplication app(argc, argv); QApplication app(argc, argv);
app.setApplicationName("BareCode"); app.setApplicationName("BareCode");
app.setApplicationVersion("1.1.0"); app.setApplicationVersion("1.2.0");
app.setOrganizationName("BareCode"); app.setOrganizationName("BareCode");
// Icon in allen verfügbaren Größen setzen // Icon in allen verfügbaren Größen setzen

View File

@@ -74,7 +74,7 @@ AboutDialog::AboutDialog(QWidget *parent)
info->addLayout(row); info->addLayout(row);
}; };
makeRow(tr("Version"), "1.1.0"); makeRow(tr("Version"), "1.2.0");
makeRow(tr("Entwickler"), "Dany Thinnes"); makeRow(tr("Entwickler"), "Dany Thinnes");
makeRow(tr("Projekt"), "Projekt Hirnfrei"); makeRow(tr("Projekt"), "Projekt Hirnfrei");
makeRow(tr("Framework"), QString("Qt %1").arg(qVersion())); makeRow(tr("Framework"), QString("Qt %1").arg(qVersion()));

View File

@@ -29,6 +29,8 @@ set(EDITOR_SOURCES
DeadCodeAnalyzer.h DeadCodeAnalyzer.h
DeadCodeDialog.cpp DeadCodeDialog.cpp
DeadCodeDialog.h DeadCodeDialog.h
VariableCompleter.cpp
VariableCompleter.h
) )
add_library(BareCode_Editor STATIC ${EDITOR_SOURCES}) add_library(BareCode_Editor STATIC ${EDITOR_SOURCES})

View File

@@ -3,6 +3,7 @@
#include "ColorIndicator.h" #include "ColorIndicator.h"
#include "SignatureHelper.h" #include "SignatureHelper.h"
#include "FunctionIndex.h" #include "FunctionIndex.h"
#include "VariableCompleter.h"
#include "core/Settings.h" #include "core/Settings.h"
#include "highlighter/HighlighterFactory.h" #include "highlighter/HighlighterFactory.h"
@@ -27,6 +28,7 @@ CodeEditor::CodeEditor(Settings *settings, QWidget *parent)
m_lineNumberArea = new LineNumberArea(this); m_lineNumberArea = new LineNumberArea(this);
m_colorIndicator = new ColorIndicator(this); m_colorIndicator = new ColorIndicator(this);
m_signatureHelper = new SignatureHelper(this); m_signatureHelper = new SignatureHelper(this);
m_varCompleter = new VariableCompleter(this);
setupEditor(); setupEditor();
connect(this, &CodeEditor::blockCountChanged, connect(this, &CodeEditor::blockCountChanged,
@@ -390,7 +392,7 @@ void CodeEditor::lineNumberAreaPaintEvent(QPaintEvent *event)
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Current line highlight // Current line highlight + Klammerzugehörigkeit
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
void CodeEditor::highlightCurrentLine() void CodeEditor::highlightCurrentLine()
{ {
@@ -398,20 +400,161 @@ void CodeEditor::highlightCurrentLine()
if (!isReadOnly()) 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); // Klammerzugehörigkeit
selection.format.setBackground(lineColor); matchBrackets(extraSelections);
selection.format.setProperty(QTextFormat::FullWidthSelection, true);
selection.cursor = textCursor();
selection.cursor.clearSelection();
extraSelections.append(selection);
} }
setExtraSelections(extraSelections); setExtraSelections(extraSelections);
} }
void CodeEditor::matchBrackets(QList<QTextEdit::ExtraSelection> &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 // 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 startBlock = document()->findBlock(cursor.selectionStart());
QTextBlock endBlock = document()->findBlock(cursor.selectionEnd()); 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(); cursor.beginEditBlock();
for (QTextBlock b = startBlock; b != endBlock.next(); b = b.next()) for (QTextBlock b = startBlock; b != endBlock.next(); b = b.next())
{ {
@@ -438,22 +591,14 @@ void CodeEditor::keyPressEvent(QKeyEvent *event)
if (m_settings->useSpacesForTabs()) if (m_settings->useSpacesForTabs())
{ {
// Bis zu tabSize führende Leerzeichen entfernen
for (int i = 0; i < tabSize && i < lineText.length(); ++i) for (int i = 0; i < tabSize && i < lineText.length(); ++i)
{ {
if (lineText[i] == ' ') if (lineText[i] == ' ') { ++toRemove; }
{ else { break; }
++toRemove;
}
else
{
break;
}
} }
} }
else else
{ {
// Einen führenden Tab entfernen
if (!lineText.isEmpty() && lineText[0] == '\t') if (!lineText.isEmpty() && lineText[0] == '\t')
{ {
toRemove = 1; toRemove = 1;
@@ -483,10 +628,16 @@ void CodeEditor::keyPressEvent(QKeyEvent *event)
if (cursor.hasSelection()) if (cursor.hasSelection())
{ {
// Mehrere Zeilen einrücken
QTextBlock startBlock = document()->findBlock(cursor.selectionStart()); QTextBlock startBlock = document()->findBlock(cursor.selectionStart());
QTextBlock endBlock = document()->findBlock(cursor.selectionEnd()); 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(); cursor.beginEditBlock();
for (QTextBlock b = startBlock; b != endBlock.next(); b = b.next()) for (QTextBlock b = startBlock; b != endBlock.next(); b = b.next())
{ {
@@ -591,5 +742,8 @@ void CodeEditor::keyPressEvent(QKeyEvent *event)
} }
QPlainTextEdit::keyPressEvent(event); QPlainTextEdit::keyPressEvent(event);
// Variablen-Popup nach jedem Tastendruck aktualisieren
m_varCompleter->handleKeyPress(event);
} }

View File

@@ -11,6 +11,7 @@ class SyntaxHighlighter;
class ColorIndicator; class ColorIndicator;
class SignatureHelper; class SignatureHelper;
class FunctionIndex; class FunctionIndex;
class VariableCompleter;
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// CodeEditor Core editing widget. // CodeEditor Core editing widget.
@@ -55,8 +56,7 @@ signals:
void fileSaved(const QString &filePath); void fileSaved(const QString &filePath);
void navigateToRequested(const QString &filePath, int line); void navigateToRequested(const QString &filePath, int line);
protected: protected: void resizeEvent(QResizeEvent *event) override;
void resizeEvent(QResizeEvent *event) override;
void keyPressEvent(QKeyEvent *event) override; void keyPressEvent(QKeyEvent *event) override;
void paintEvent(QPaintEvent *event) override; void paintEvent(QPaintEvent *event) override;
void mousePressEvent(QMouseEvent *event) override; void mousePressEvent(QMouseEvent *event) override;
@@ -71,6 +71,7 @@ private:
void setupEditor(); void setupEditor();
void installHighlighter(const QString &filePath); void installHighlighter(const QString &filePath);
bool writeToFile(const QString &filePath); bool writeToFile(const QString &filePath);
void matchBrackets(QList<QTextEdit::ExtraSelection> &selections);
Settings *m_settings = nullptr; Settings *m_settings = nullptr;
LineNumberArea *m_lineNumberArea = nullptr; LineNumberArea *m_lineNumberArea = nullptr;
@@ -78,5 +79,6 @@ private:
ColorIndicator *m_colorIndicator = nullptr; ColorIndicator *m_colorIndicator = nullptr;
SignatureHelper *m_signatureHelper = nullptr; SignatureHelper *m_signatureHelper = nullptr;
FunctionIndex *m_functionIndex = nullptr; FunctionIndex *m_functionIndex = nullptr;
VariableCompleter *m_varCompleter = nullptr;
QString m_filePath; QString m_filePath;
}; };

View File

@@ -0,0 +1,243 @@
#include "VariableCompleter.h"
#include "CodeEditor.h"
#include <QTextCursor>
#include <QTextBlock>
#include <QKeyEvent>
#include <QListWidgetItem>
#include <QAbstractItemView>
#include <QScrollBar>
#include <QApplication>
#include <QScreen>
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<QString> 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();
}

View File

@@ -0,0 +1,38 @@
#pragma once
#include <QObject>
#include <QListWidget>
#include <QStringList>
#include <QRegularExpression>
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;
};

View File

@@ -11,6 +11,7 @@ SyntaxHighlighter::SyntaxHighlighter(QTextDocument *parent)
void SyntaxHighlighter::highlightBlock(const QString &text) void SyntaxHighlighter::highlightBlock(const QString &text)
{ {
// Einzel-Zeilen-Regeln anwenden
for (const HighlightRule &rule : m_rules) for (const HighlightRule &rule : m_rules)
{ {
QRegularExpressionMatchIterator it = rule.pattern.globalMatch(text); QRegularExpressionMatchIterator it = rule.pattern.globalMatch(text);
@@ -27,21 +28,65 @@ void SyntaxHighlighter::highlightBlock(const QString &text)
if (!m_hasMultiLineComment) if (!m_hasMultiLineComment)
{ {
setCurrentBlockState(0);
return; return;
} }
// Mehrzeilige Kommentare
// Zustand 0 = normal, 1 = mitten in /* ... */
setCurrentBlockState(0); setCurrentBlockState(0);
int startIndex = 0; // Leere Zeile mitten im Kommentar — Zustand weitertragen
if (previousBlockState() != 1) 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<int>(endMatch.capturedStart())
+ static_cast<int>(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<int>(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); QRegularExpressionMatch m = m_commentStartExpression.match(text);
startIndex = m.hasMatch() ? static_cast<int>(m.capturedStart()) : -1; startIndex = m.hasMatch() ? static_cast<int>(m.capturedStart()) : -1;
} }
while (startIndex >= 0) while (startIndex >= 0)
{ {
QRegularExpressionMatch endMatch = m_commentEndExpression.match(text, startIndex); QRegularExpressionMatch endMatch =
m_commentEndExpression.match(text, startIndex);
int commentLength = 0; int commentLength = 0;
if (endMatch.hasMatch()) if (endMatch.hasMatch())
@@ -52,6 +97,7 @@ void SyntaxHighlighter::highlightBlock(const QString &text)
} }
else else
{ {
// Kommentar geht über Zeilenende
setCurrentBlockState(1); setCurrentBlockState(1);
commentLength = text.length() - startIndex; commentLength = text.length() - startIndex;
} }
@@ -63,6 +109,7 @@ void SyntaxHighlighter::highlightBlock(const QString &text)
break; break;
} }
// Nach weiteren Kommentaren in dieser Zeile suchen
QRegularExpressionMatch nextStart = QRegularExpressionMatch nextStart =
m_commentStartExpression.match(text, startIndex + commentLength); m_commentStartExpression.match(text, startIndex + commentLength);
startIndex = nextStart.hasMatch() 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 blockOpen(R"(/\*)");
static const QRegularExpression blockClose(R"(\*/)"); static const QRegularExpression blockClose(R"(\*/)");
int searchFrom = 0; 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) if (previousBlockState() == 3)
{ {
QRegularExpressionMatch closeMatch = blockClose.match(phpText, 0); QRegularExpressionMatch closeMatch = blockClose.match(phpText, 0);
@@ -391,18 +446,15 @@ void PhpHighlighter::highlightPhpRange(const QString &text, int start, int lengt
+ static_cast<int>(closeMatch.capturedLength()); + static_cast<int>(closeMatch.capturedLength());
setFormat(start, end, m_phpCommentFormat); setFormat(start, end, m_phpCommentFormat);
searchFrom = end; searchFrom = end;
// Block-Kommentar geschlossen — Zustand wird weiter unten gesetzt
} }
else else
{ {
// Gesamter Bereich ist noch Kommentar
setFormat(start, length, m_phpCommentFormat); setFormat(start, length, m_phpCommentFormat);
setCurrentBlockState(3); setCurrentBlockState(3);
return; return;
} }
} }
// Neue /* ... */ Kommentare innerhalb dieses PHP-Bereichs suchen
while (searchFrom < phpText.length()) while (searchFrom < phpText.length())
{ {
QRegularExpressionMatch openMatch = blockOpen.match(phpText, searchFrom); QRegularExpressionMatch openMatch = blockOpen.match(phpText, searchFrom);
@@ -423,7 +475,6 @@ void PhpHighlighter::highlightPhpRange(const QString &text, int start, int lengt
} }
else else
{ {
// Kein schließendes */ gefunden — geht über Zeilenende
setFormat(start + openPos, length - openPos, m_phpCommentFormat); setFormat(start + openPos, length - openPos, m_phpCommentFormat);
setCurrentBlockState(3); setCurrentBlockState(3);
return; return;
@@ -434,13 +485,39 @@ void PhpHighlighter::highlightPhpRange(const QString &text, int start, int lengt
void PhpHighlighter::highlightBlock(const QString &text) void PhpHighlighter::highlightBlock(const QString &text)
{ {
// Zuerst HTML-Basis-Regeln auf den gesamten Text anwenden // Zuerst HTML-Basis-Regeln auf den gesamten Text anwenden
// (setzt auch den State für HTML <!-- --> Kommentare)
SyntaxHighlighter::highlightBlock(text); SyntaxHighlighter::highlightBlock(text);
// Block-Zustände: // Block-Zustände:
// 0 = HTML-Modus // 0 = HTML-Modus
// 1 = HTML <!-- --> Kommentar (von Basisklasse verwaltet)
// 2 = innerhalb PHP-Block (kein /* */ Kommentar) // 2 = innerhalb PHP-Block (kein /* */ Kommentar)
// 3 = innerhalb PHP /* */ Block-Kommentar // 3 = innerhalb PHP /* */ Block-Kommentar
// 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); 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?)", static const QRegularExpression phpOpen(R"(<\?(?:php|=)?\s?)",
QRegularExpression::CaseInsensitiveOption); QRegularExpression::CaseInsensitiveOption);
@@ -466,8 +543,6 @@ void PhpHighlighter::highlightBlock(const QString &text)
else else
{ {
highlightPhpRange(text, 0, text.length()); 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) if (currentBlockState() != 3)
{ {
setCurrentBlockState(2); setCurrentBlockState(2);