Dateien, die als Parameter übergeben werden, werden nach dem Start direkt geladen

This commit is contained in:
2026-08-25 14:56:27 +02:00
parent 7264b68f11
commit 04c5212d54
78 changed files with 9528 additions and 0 deletions

View File

@@ -0,0 +1,49 @@
set(EDITOR_SOURCES
EditorPanel.cpp
EditorPanel.h
CodeEditor.cpp
CodeEditor.h
LineNumberArea.cpp
LineNumberArea.h
EditorTab.cpp
EditorTab.h
SearchPanel.cpp
SearchPanel.h
FileSearchPanel.cpp
FileSearchPanel.h
ColorIndicator.cpp
ColorIndicator.h
SignatureHelper.cpp
SignatureHelper.h
SignatureTooltip.cpp
SignatureTooltip.h
FunctionScanner.cpp
FunctionScanner.h
FunctionIndex.cpp
FunctionIndex.h
FunctionListPanel.cpp
FunctionListPanel.h
FunctionListDialog.cpp
FunctionListDialog.h
DeadCodeAnalyzer.cpp
DeadCodeAnalyzer.h
DeadCodeDialog.cpp
DeadCodeDialog.h
VariableCompleter.cpp
VariableCompleter.h
)
add_library(BareCode_Editor STATIC ${EDITOR_SOURCES})
target_link_libraries(BareCode_Editor PUBLIC
Qt6::Core
Qt6::Gui
Qt6::Widgets
Qt6::Concurrent
BareCode_Highlighter
)
target_include_directories(BareCode_Editor PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/..
)

View File

@@ -0,0 +1,762 @@
#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 <QPainter>
#include <QTextBlock>
#include <QPaintEvent>
#include <QResizeEvent>
#include <QKeyEvent>
#include <QMouseEvent>
#include <QFocusEvent>
#include <QScrollBar>
#include <QFile>
#include <QTextStream>
#include <QFileInfo>
#include <QFileDialog>
#include <QMessageBox>
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<qreal>(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<int>(blockRect.left()
+ layout->lineAt(0).position().x());
}
else
{
textOriginX = static_cast<int>(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<int>(blockRect.top()),
xPixel,
static_cast<int>(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<int>(blockBoundingGeometry(block).translated(contentOffset()).top());
int bottom = top + static_cast<int>(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<int>(blockBoundingRect(block).height());
++blockNumber;
}
}
// ---------------------------------------------------------------------------
// Current line highlight + Klammerzugehörigkeit
// ---------------------------------------------------------------------------
void CodeEditor::highlightCurrentLine()
{
QList<QTextEdit::ExtraSelection> 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<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
// ---------------------------------------------------------------------------
void CodeEditor::keyPressEvent(QKeyEvent *event)
{
const int tabSize = m_settings->tabSize();
// -----------------------------------------------------------------------
// 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();
}

View File

@@ -0,0 +1,86 @@
#pragma once
#include <QPlainTextEdit>
#include <QFont>
#include <QString>
#include <QTextBlock>
class LineNumberArea;
class Settings;
class SyntaxHighlighter;
class ColorIndicator;
class SignatureHelper;
class FunctionIndex;
class VariableCompleter;
// ---------------------------------------------------------------------------
// CodeEditor Core editing widget.
// Features:
// • Line number gutter
// • Current-line highlight
// • Auto-indent on Enter
// • Tab → spaces (configurable)
// • Syntax highlighting (via pluggable SyntaxHighlighter)
// ---------------------------------------------------------------------------
class CodeEditor : public QPlainTextEdit
{
Q_OBJECT
public:
explicit CodeEditor(Settings *settings, QWidget *parent = nullptr);
~CodeEditor() override;
void loadFile(const QString &filePath);
void applySettings();
// Speichern
bool save();
bool saveAs();
// Called by LineNumberArea
int lineNumberAreaWidth() const;
void lineNumberAreaPaintEvent(QPaintEvent *event);
QString filePath() const;
bool isModified() const;
// Öffentliche Hilfsmethoden für ColorIndicator
// (die Qt-Originale sind protected und von außen nicht erreichbar)
QTextBlock firstVisibleBlockPublic() const { return firstVisibleBlock(); }
QRectF blockBoundingGeometryPublic(const QTextBlock &b) const { return blockBoundingGeometry(b); }
QPointF contentOffsetPublic() const { return contentOffset(); }
void setFunctionIndex(FunctionIndex *index);
signals:
void fileSaved(const QString &filePath);
void navigateToRequested(const QString &filePath, int line);
protected:
void resizeEvent(QResizeEvent *event) override;
void keyPressEvent(QKeyEvent *event) override;
void paintEvent(QPaintEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
void mouseDoubleClickEvent(QMouseEvent *event) override;
void focusOutEvent(QFocusEvent *event) override;
private slots:
void updateLineNumberAreaWidth(int newBlockCount);
void highlightCurrentLine();
void updateLineNumberArea(const QRect &rect, int dy);
private:
void setupEditor();
void installHighlighter(const QString &filePath);
bool writeToFile(const QString &filePath);
void matchBrackets(QList<QTextEdit::ExtraSelection> &selections);
Settings *m_settings = nullptr;
LineNumberArea *m_lineNumberArea = nullptr;
SyntaxHighlighter *m_highlighter = nullptr;
ColorIndicator *m_colorIndicator = nullptr;
SignatureHelper *m_signatureHelper = nullptr;
FunctionIndex *m_functionIndex = nullptr;
VariableCompleter *m_varCompleter = nullptr;
QString m_filePath;
};

View File

@@ -0,0 +1,361 @@
#include "ColorIndicator.h"
#include "CodeEditor.h"
#include <QPainter>
#include <QPaintEvent>
#include <QMouseEvent>
#include <QColorDialog>
#include <QTextBlock>
#include <QTextCursor>
#include <QTextDocument>
#include <QScrollBar>
#include <QHash>
// ---------------------------------------------------------------------------
// Kombinierter Regex — erfasst alle CSS-Farbformate in einer Runde
// ---------------------------------------------------------------------------
const QRegularExpression ColorIndicator::s_colorRegex(
// #rgb / #rrggbb / #rrggbbaa
R"(#(?:[0-9A-Fa-f]{8}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{3})(?=[^0-9A-Fa-f]|$))"
R"(|rgba?\s*\([^)]+\))"
R"(|hsla?\s*\([^)]+\))",
QRegularExpression::CaseInsensitiveOption
);
// ---------------------------------------------------------------------------
// Konstruktor
// ---------------------------------------------------------------------------
ColorIndicator::ColorIndicator(CodeEditor *editor)
: QObject(editor)
, m_editor(editor)
{
// Cache invalidieren und Viewport neu zeichnen wenn sich der Text ändert
connect(m_editor->document(), &QTextDocument::contentsChanged,
this, [this]()
{
m_cacheFirstBlock = -1;
m_cacheLastBlock = -1;
m_editor->viewport()->update();
});
// Auch beim Scrollen neu zeichnen (Cache bleibt gültig, nur Position ändert sich)
connect(m_editor->verticalScrollBar(), &QScrollBar::valueChanged,
this, [this]()
{
m_cacheFirstBlock = -1;
m_cacheLastBlock = -1;
});
}
// ---------------------------------------------------------------------------
// Paint wird aus CodeEditor::paintEvent aufgerufen
// ---------------------------------------------------------------------------
void ColorIndicator::paint(QPainter &painter, QPaintEvent *event)
{
rebuildCache();
const int squareSize = m_editor->fontMetrics().height() - 4;
const int radius = 2;
for (const ColorMatch &m : m_cache)
{
if (!event->rect().intersects(m.rect))
{
continue;
}
// Rahmen
painter.setPen(QColor(0, 0, 0, 80));
painter.setBrush(m.color);
painter.drawRoundedRect(m.rect, radius, radius);
// Schachbrettmuster als Hintergrund für transparente Farben
if (m.color.alpha() < 255)
{
const int half = squareSize / 2;
painter.setPen(Qt::NoPen);
painter.setBrush(QColor(180, 180, 180));
painter.drawRect(m.rect.x(), m.rect.y(), half, half);
painter.drawRect(m.rect.x() + half, m.rect.y() + half, half, half);
painter.setBrush(m.color);
painter.drawRoundedRect(m.rect, radius, radius);
}
}
}
// ---------------------------------------------------------------------------
// Mouse wird aus CodeEditor::mousePressEvent aufgerufen
// ---------------------------------------------------------------------------
bool ColorIndicator::handleMousePress(QMouseEvent *event)
{
for (const ColorMatch &m : m_cache)
{
if (!m.rect.contains(event->pos()))
{
continue;
}
// Farb-Dialog öffnen
QColorDialog dlg(m.color, m_editor);
dlg.setOption(QColorDialog::ShowAlphaChannel, true);
dlg.setWindowTitle(QObject::tr("Farbe wählen"));
if (dlg.exec() != QDialog::Accepted)
{
return true;
}
const QColor newColor = dlg.selectedColor();
// Ursprünglichen Farbwert im Dokument ersetzen
QTextBlock block = m_editor->document()->findBlockByNumber(m.blockNumber);
if (!block.isValid())
{
return true;
}
// Neuen Farbwert als Hex-String formatieren
QString newValue;
if (newColor.alpha() < 255)
{
newValue = newColor.name(QColor::HexArgb); // #aarrggbb
// CSS erwartet #rrggbbaa — Bytes umstellen
// Qt liefert #aarrggbb, CSS will #rrggbbaa
newValue = QString("#%1%2%3%4")
.arg(newColor.red(), 2, 16, QChar('0'))
.arg(newColor.green(), 2, 16, QChar('0'))
.arg(newColor.blue(), 2, 16, QChar('0'))
.arg(newColor.alpha(), 2, 16, QChar('0'));
}
else
{
newValue = newColor.name(QColor::HexRgb); // #rrggbb
}
QTextCursor cursor(block);
cursor.setPosition(block.position() + m.posInBlock);
cursor.setPosition(block.position() + m.posInBlock + m.length,
QTextCursor::KeepAnchor);
cursor.insertText(newValue);
// Cache invalidieren
m_cache.clear();
m_cacheFirstBlock = -1;
m_cacheLastBlock = -1;
return true;
}
return false;
}
// ---------------------------------------------------------------------------
// Cache aufbauen nur für sichtbare Blöcke
// ---------------------------------------------------------------------------
void ColorIndicator::rebuildCache()
{
QTextBlock firstVisible = m_editor->firstVisibleBlockPublic();
const int firstNum = firstVisible.blockNumber();
// Letzten sichtbaren Block bestimmen
int lastNum = firstNum;
{
QTextBlock b = firstVisible;
const int bot = m_editor->viewport()->height();
while (b.isValid())
{
const QRectF r = m_editor->blockBoundingGeometryPublic(b)
.translated(m_editor->contentOffsetPublic());
if (r.top() > bot)
{
break;
}
lastNum = b.blockNumber();
b = b.next();
}
}
// Cache noch aktuell?
if (firstNum == m_cacheFirstBlock && lastNum == m_cacheLastBlock)
{
return;
}
m_cache.clear();
m_cacheFirstBlock = firstNum;
m_cacheLastBlock = lastNum;
const int squareSize = m_editor->fontMetrics().height() - 4;
const int scrollX = m_editor->horizontalScrollBar()->value();
QTextBlock block = firstVisible;
while (block.isValid() && block.blockNumber() <= lastNum)
{
const QRectF blockRect = m_editor->blockBoundingGeometryPublic(block)
.translated(m_editor->contentOffsetPublic());
const QList<ColorMatch> found = findColorsInBlock(block.text(),
block.blockNumber());
for (ColorMatch m : found)
{
// X-Position des Farbwerts im Viewport berechnen
const QTextLayout *layout = block.layout();
if (!layout || layout->lineCount() == 0)
{
continue;
}
const QTextLine line = layout->lineAt(0);
// Position nach dem Ende des Farbwerts
const qreal endCharX = line.cursorToX(m.posInBlock + m.length);
const int x = static_cast<int>(blockRect.left() + endCharX)
- scrollX + 3;
if (x + squareSize > m_editor->viewport()->width())
{
continue;
}
const int y = static_cast<int>(blockRect.top())
+ (static_cast<int>(blockRect.height()) - squareSize) / 2;
m.rect = QRect(x, y, squareSize, squareSize);
m_cache.append(m);
}
block = block.next();
}
}
// ---------------------------------------------------------------------------
// Farbwerte in einer Zeile suchen
// ---------------------------------------------------------------------------
QList<ColorIndicator::ColorMatch> ColorIndicator::findColorsInBlock(
const QString &text, int blockNumber) const
{
QList<ColorMatch> result;
QRegularExpressionMatchIterator it = s_colorRegex.globalMatch(text);
while (it.hasNext())
{
QRegularExpressionMatch match = it.next();
const QString token = match.captured(0);
const QColor color = parseColor(token);
if (!color.isValid())
{
continue;
}
ColorMatch m;
m.blockNumber = blockNumber;
m.posInBlock = static_cast<int>(match.capturedStart());
m.length = static_cast<int>(match.capturedLength());
m.color = color;
result.append(m);
}
return result;
}
// ---------------------------------------------------------------------------
// Farb-Parser
// ---------------------------------------------------------------------------
QColor ColorIndicator::parseColor(const QString &token)
{
const QString t = token.trimmed();
if (t.startsWith('#')) { return parseHex(t); }
if (t.startsWith("rgba", Qt::CaseInsensitive)) { return parseRgba(t); }
if (t.startsWith("rgb", Qt::CaseInsensitive)) { return parseRgb(t); }
if (t.startsWith("hsla", Qt::CaseInsensitive)) { return parseHsla(t); }
if (t.startsWith("hsl", Qt::CaseInsensitive)) { return parseHsl(t); }
return QColor();
}
QColor ColorIndicator::parseHex(const QString &s)
{
// #rgb → #rrggbb
if (s.length() == 4)
{
return QColor(QString("#%1%1%2%2%3%3")
.arg(s[1]).arg(s[2]).arg(s[3]));
}
// #rrggbb
if (s.length() == 7)
{
return QColor(s);
}
// #rrggbbaa (CSS) → Qt braucht #aarrggbb
if (s.length() == 9)
{
const QString rr = s.mid(1, 2);
const QString gg = s.mid(3, 2);
const QString bb = s.mid(5, 2);
const QString aa = s.mid(7, 2);
return QColor(QString("#%1%2%3%4").arg(aa, rr, gg, bb));
}
return QColor();
}
QColor ColorIndicator::parseRgb(const QString &s)
{
// rgb(r, g, b)
static const QRegularExpression re(
R"(rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\))",
QRegularExpression::CaseInsensitiveOption);
QRegularExpressionMatch m = re.match(s);
if (!m.hasMatch()) { return QColor(); }
return QColor(m.captured(1).toInt(),
m.captured(2).toInt(),
m.captured(3).toInt());
}
QColor ColorIndicator::parseRgba(const QString &s)
{
// rgba(r, g, b, a) — a ist 0.01.0
static const QRegularExpression re(
R"(rgba\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*([0-9.]+)\s*\))",
QRegularExpression::CaseInsensitiveOption);
QRegularExpressionMatch m = re.match(s);
if (!m.hasMatch()) { return QColor(); }
return QColor(m.captured(1).toInt(),
m.captured(2).toInt(),
m.captured(3).toInt(),
qRound(m.captured(4).toDouble() * 255.0));
}
QColor ColorIndicator::parseHsl(const QString &s)
{
// hsl(h, s%, l%)
static const QRegularExpression re(
R"(hsl\s*\(\s*(\d+)\s*,\s*(\d+)%\s*,\s*(\d+)%\s*\))",
QRegularExpression::CaseInsensitiveOption);
QRegularExpressionMatch m = re.match(s);
if (!m.hasMatch()) { return QColor(); }
QColor c;
c.setHsl(m.captured(1).toInt(),
qRound(m.captured(2).toInt() * 2.55),
qRound(m.captured(3).toInt() * 2.55));
return c;
}
QColor ColorIndicator::parseHsla(const QString &s)
{
// hsla(h, s%, l%, a)
static const QRegularExpression re(
R"(hsla\s*\(\s*(\d+)\s*,\s*(\d+)%\s*,\s*(\d+)%\s*,\s*([0-9.]+)\s*\))",
QRegularExpression::CaseInsensitiveOption);
QRegularExpressionMatch m = re.match(s);
if (!m.hasMatch()) { return QColor(); }
QColor c;
c.setHsl(m.captured(1).toInt(),
qRound(m.captured(2).toInt() * 2.55),
qRound(m.captured(3).toInt() * 2.55),
qRound(m.captured(4).toDouble() * 255.0));
return c;
}

View File

@@ -0,0 +1,67 @@
#pragma once
#include <QObject>
#include <QColor>
#include <QRect>
#include <QList>
#include <QRegularExpression>
#include <QString>
class CodeEditor;
class QPainter;
class QPaintEvent;
class QMouseEvent;
// ---------------------------------------------------------------------------
// ColorIndicator Zeichnet kleine Farbquadrate neben CSS-Farbwerten und
// öffnet einen QColorDialog wenn der Nutzer darauf klickt.
//
// Unterstützte Formate:
// #rgb #rrggbb #rrggbbaa
// rgb(r, g, b) rgba(r, g, b, a)
// hsl(h, s%, l%) hsla(h, s%, l%, a)
// 140 benannte CSS-Farben (red, blue, cornflowerblue, ...)
// ---------------------------------------------------------------------------
class ColorIndicator : public QObject
{
Q_OBJECT
public:
explicit ColorIndicator(CodeEditor *editor);
// Wird aus CodeEditor::paintEvent aufgerufen
void paint(QPainter &painter, QPaintEvent *event);
// Wird aus CodeEditor::mousePressEvent aufgerufen
// Gibt true zurück wenn der Klick auf einem Farbquadrat war
bool handleMousePress(QMouseEvent *event);
private:
struct ColorMatch
{
QRect rect; // Position des Quadrats im Viewport
QColor color; // Erkannte Farbe
int blockNumber;
int posInBlock; // Zeichenposition des Farbwerts im Block
int length; // Länge des Farbwerts im Text
};
void rebuildCache();
QList<ColorMatch> findColorsInBlock(const QString &text,
int blockNumber) const;
static QColor parseColor(const QString &token);
static QColor parseHex(const QString &s);
static QColor parseRgb(const QString &s);
static QColor parseRgba(const QString &s);
static QColor parseHsl(const QString &s);
static QColor parseHsla(const QString &s);
CodeEditor *m_editor = nullptr;
QList<ColorMatch> m_cache;
int m_cacheFirstBlock = -1;
int m_cacheLastBlock = -1;
// Kombinierter Regex für alle Farbformate
static const QRegularExpression s_colorRegex;
};

View File

@@ -0,0 +1,181 @@
#include "DeadCodeAnalyzer.h"
#include <QDir>
#include <QDirIterator>
#include <QFile>
#include <QTextStream>
#include <QRegularExpression>
#include <QSet>
#include <QFileInfo>
DeadCodeAnalyzer::DeadCodeAnalyzer(QObject *parent)
: QObject(parent)
{
m_scanner = new FunctionScanner(this);
}
QList<DeadCodeAnalyzer::DeadFunction> DeadCodeAnalyzer::analyze(
const QString &projectRoot,
const QStringList &extensions,
const QStringList &excludeDirs) const
{
// -----------------------------------------------------------------------
// Schritt 1: Alle Dateien sammeln (mit Exclude-Filter)
// -----------------------------------------------------------------------
QStringList allFiles;
QDirIterator it(projectRoot, extensions, QDir::Files,
QDirIterator::Subdirectories);
while (it.hasNext())
{
const QString path = it.next();
// Exclude-Verzeichnisse prüfen
bool excluded = false;
if (!excludeDirs.isEmpty())
{
const QStringList parts = path.split('/');
for (const QString &excl : excludeDirs)
{
if (parts.contains(excl.trimmed(), Qt::CaseInsensitive))
{
excluded = true;
break;
}
}
}
if (!excluded)
{
allFiles.append(path);
}
}
const int totalFiles = allFiles.size();
// -----------------------------------------------------------------------
// Schritt 2: Alle Funktionsdefinitionen aus nicht-excluded Dateien
// -----------------------------------------------------------------------
QHash<QString, FunctionScanner::FunctionInfo> definitions;
for (const QString &path : allFiles)
{
const QList<FunctionScanner::FunctionInfo> funcs = m_scanner->scanFile(path);
for (const FunctionScanner::FunctionInfo &func : funcs)
{
if (func.name.startsWith("__"))
{
continue;
}
const QString key = func.name.toLower();
if (!definitions.contains(key))
{
definitions.insert(key, func);
}
}
}
if (definitions.isEmpty())
{
return {};
}
// -----------------------------------------------------------------------
// Schritt 3: Alle Dateien EINMAL lesen, alle Aufrufe sammeln
// -----------------------------------------------------------------------
QSet<QString> calledFunctions;
static const QRegularExpression identifierRegex(
R"((?:->|::|\b)([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)\s*\()"
R"(|['"]([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)['"])"
);
static const QRegularExpression defLineRegex(
R"(\bfunction\s+([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*))"
);
int filesScanned = 0;
for (const QString &filePath : allFiles)
{
++filesScanned;
// Fortschritt signalisieren
emit progressUpdate(filesScanned, totalFiles,
QFileInfo(filePath).fileName());
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
{
continue;
}
QTextStream stream(&file);
stream.setEncoding(QStringConverter::Utf8);
while (!stream.atEnd())
{
const QString line = stream.readLine();
const QString trimmed = line.trimmed();
if (trimmed.startsWith("//") ||
trimmed.startsWith("*") ||
trimmed.startsWith("#") ||
trimmed.startsWith("/*"))
{
continue;
}
// Definitionen auf dieser Zeile nicht als Aufruf werten
QSet<QString> definedOnThisLine;
{
QRegularExpressionMatchIterator dit = defLineRegex.globalMatch(line);
while (dit.hasNext())
{
definedOnThisLine.insert(dit.next().captured(1).toLower());
}
}
QRegularExpressionMatchIterator mit = identifierRegex.globalMatch(line);
while (mit.hasNext())
{
const QRegularExpressionMatch match = mit.next();
const QString name = match.captured(1).isEmpty()
? match.captured(2).toLower()
: match.captured(1).toLower();
if (name.isEmpty() || definedOnThisLine.contains(name))
{
continue;
}
if (definitions.contains(name))
{
calledFunctions.insert(name);
}
}
}
}
// -----------------------------------------------------------------------
// Schritt 4: Nicht aufgerufene Funktionen zurückgeben
// -----------------------------------------------------------------------
QList<DeadFunction> dead;
for (auto it2 = definitions.begin(); it2 != definitions.end(); ++it2)
{
if (!calledFunctions.contains(it2.key()))
{
const FunctionScanner::FunctionInfo &func = it2.value();
DeadFunction d;
d.info = func;
d.reason = func.className.isEmpty()
? QObject::tr("Globale Funktion — kein Aufruf gefunden")
: QObject::tr("Methode von %1 — kein Aufruf gefunden")
.arg(func.className);
dead.append(d);
}
}
return dead;
}

View File

@@ -0,0 +1,39 @@
#pragma once
#include <QObject>
#include <QString>
#include <QStringList>
#include <QList>
#include "FunctionScanner.h"
// ---------------------------------------------------------------------------
// DeadCodeAnalyzer Findet Funktionen die definiert aber nie aufgerufen werden.
// ---------------------------------------------------------------------------
class DeadCodeAnalyzer : public QObject
{
Q_OBJECT
public:
struct DeadFunction
{
FunctionScanner::FunctionInfo info;
QString reason;
};
explicit DeadCodeAnalyzer(QObject *parent = nullptr);
// excludeDirs: Verzeichnisnamen die komplett übersprungen werden
// z.B. {"vendor", "lib", "node_modules"}
QList<DeadFunction> analyze(const QString &projectRoot,
const QStringList &extensions = {"*.php"},
const QStringList &excludeDirs = {}) const;
signals:
// Fortschritt während der Analyse (läuft im Thread)
void progressUpdate(int filesScanned, int filesTotal,
const QString &currentFile) const;
private:
FunctionScanner *m_scanner = nullptr;
};

View File

@@ -0,0 +1,461 @@
#include "DeadCodeDialog.h"
#include <QtConcurrent/QtConcurrent>
#include <QFileInfo>
#include <QDir>
#include <QFont>
#include <QSettings>
#include <QCloseEvent>
#include <QFileDialog>
#include <QTextStream>
#include <QMessageBox>
#include <QHeaderView>
#include <QDateTime>
#include <QMetaObject>
DeadCodeDialog::DeadCodeDialog(QWidget *parent)
: QDialog(parent, Qt::Window)
{
setWindowTitle(tr("Tote Funktionen BareCode"));
setMinimumSize(650, 500);
QSettings s(QSettings::IniFormat, QSettings::UserScope, "BareCode", "BareCode");
if (s.contains("deadcode/geometry"))
{
restoreGeometry(s.value("deadcode/geometry").toByteArray());
}
else
{
resize(780, 620);
}
m_analyzer = new DeadCodeAnalyzer(this);
m_watcher = new QFutureWatcher<QList<DeadCodeAnalyzer::DeadFunction>>(this);
connect(m_watcher, &QFutureWatcher<QList<DeadCodeAnalyzer::DeadFunction>>::finished,
this, &DeadCodeDialog::onAnalysisFinished);
// Fortschritts-Signal aus dem Analyzer-Thread sicher in den UI-Thread leiten
connect(m_analyzer, &DeadCodeAnalyzer::progressUpdate,
this, &DeadCodeDialog::onProgressUpdate,
Qt::QueuedConnection);
setupUi();
}
// ---------------------------------------------------------------------------
// UI
// ---------------------------------------------------------------------------
void DeadCodeDialog::setupUi()
{
QVBoxLayout *root = new QVBoxLayout(this);
root->setSpacing(8);
root->setContentsMargins(12, 12, 12, 12);
// ---- Warnbox ----
QFrame *warningBox = new QFrame(this);
warningBox->setStyleSheet(
"QFrame {"
" background: #3a2a00;"
" border: 1px solid #7a5a00;"
" border-radius: 4px;"
"}"
);
QHBoxLayout *warnLayout = new QHBoxLayout(warningBox);
warnLayout->setContentsMargins(8, 6, 8, 6);
QLabel *warnIcon = new QLabel(warningBox);
warnIcon->setFixedSize(20, 20);
warnIcon->setStyleSheet(
"background: #ffcc00;"
"color: #1a1a00;"
"font-weight: bold;"
"font-size: 13px;"
"border-radius: 10px;"
);
warnIcon->setText("!");
warnIcon->setAlignment(Qt::AlignCenter);
warnLayout->addWidget(warnIcon);
QLabel *warnText = new QLabel(
tr("Kandidatenliste — kein Aufruf per Regex gefunden. "
"Dynamische Aufrufe (call_user_func, Strings, Hooks) werden nicht erkannt. "
"Bitte vor dem Löschen manuell prüfen."),
warningBox
);
warnText->setWordWrap(true);
warnText->setStyleSheet("color: #ffcc88;");
warnLayout->addWidget(warnText, 1);
root->addWidget(warningBox);
// ---- Exclude-Verzeichnisse ----
QHBoxLayout *excludeRow = new QHBoxLayout();
excludeRow->addWidget(new QLabel(tr("Ausschließen:"), this));
m_excludeEdit = new QLineEdit(this);
m_excludeEdit->setPlaceholderText(tr("vendor lib node_modules (leerzeichen-getrennt)"));
m_excludeEdit->setToolTip(tr(
"Verzeichnisnamen die bei der Analyse übersprungen werden.\n"
"Leerzeichen-getrennt, z.B.: vendor lib node_modules cache"
));
// Letzte Einstellung wiederherstellen
QSettings s(QSettings::IniFormat, QSettings::UserScope, "BareCode", "BareCode");
m_excludeEdit->setText(s.value("deadcode/excludeDirs", "vendor lib node_modules").toString());
excludeRow->addWidget(m_excludeEdit, 1);
// Verzeichnis-Auswahl Button
QPushButton *btnBrowse = new QPushButton(tr("+ Verzeichnis"), this);
btnBrowse->setToolTip(tr("Verzeichnis aus dem Projekt auswählen und zur Ausschlussliste hinzufügen"));
connect(btnBrowse, &QPushButton::clicked, this, [this]()
{
const QString startPath = m_projectRoot.isEmpty()
? QDir::homePath()
: m_projectRoot;
const QString dir = QFileDialog::getExistingDirectory(
this,
tr("Verzeichnis ausschließen"),
startPath,
QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks
);
if (dir.isEmpty())
{
return;
}
// Nur den letzten Verzeichnisnamen verwenden, nicht den vollen Pfad
// So funktioniert der Exclude auch für gleichnamige Unterverzeichnisse
const QString dirName = QFileInfo(dir).fileName();
// Prüfen ob bereits in der Liste
const QStringList current = m_excludeEdit->text()
.simplified()
.split(' ', Qt::SkipEmptyParts);
if (current.contains(dirName, Qt::CaseInsensitive))
{
return;
}
// Anhängen
QString newText = m_excludeEdit->text().trimmed();
if (!newText.isEmpty())
{
newText += ' ';
}
newText += dirName;
m_excludeEdit->setText(newText);
});
excludeRow->addWidget(btnBrowse);
// Löschen-Button — ganzes Feld leeren
QPushButton *btnClear = new QPushButton(tr(""), this);
btnClear->setFixedWidth(28);
btnClear->setFlat(true);
btnClear->setToolTip(tr("Ausschlussliste leeren"));
connect(btnClear, &QPushButton::clicked, this, [this]()
{
m_excludeEdit->clear();
});
excludeRow->addWidget(btnClear);
root->addLayout(excludeRow);
// ---- Steuerleiste ----
QHBoxLayout *ctrlRow = new QHBoxLayout();
m_btnAnalyze = new QPushButton(tr("Analyse starten"), this);
ctrlRow->addWidget(m_btnAnalyze);
m_btnExport = new QPushButton(tr("Als TXT exportieren"), this);
m_btnExport->setEnabled(false);
ctrlRow->addWidget(m_btnExport);
ctrlRow->addStretch();
m_statusLabel = new QLabel(this);
m_statusLabel->setStyleSheet("color: palette(mid);");
ctrlRow->addWidget(m_statusLabel);
root->addLayout(ctrlRow);
// ---- Fortschrittsbereich ----
m_progress = new QProgressBar(this);
m_progress->setRange(0, 100);
m_progress->setTextVisible(true);
m_progress->setFormat("%v / %m Dateien");
m_progress->hide();
root->addWidget(m_progress);
m_progressLabel = new QLabel(this);
m_progressLabel->setStyleSheet("color: palette(mid); font-size: 11px;");
m_progressLabel->hide();
root->addWidget(m_progressLabel);
// ---- Ergebnistabelle ----
m_results = new QTreeWidget(this);
m_results->setColumnCount(4);
m_results->setHeaderLabels({
tr("Funktion"),
tr("Klasse"),
tr("Datei"),
tr("Zeile")
});
m_results->setRootIsDecorated(false);
m_results->setAlternatingRowColors(true);
m_results->setSortingEnabled(true);
m_results->sortByColumn(0, Qt::AscendingOrder);
m_results->setSelectionMode(QAbstractItemView::SingleSelection);
m_results->header()->setSectionResizeMode(0, QHeaderView::Interactive);
m_results->header()->setSectionResizeMode(1, QHeaderView::Interactive);
m_results->header()->setSectionResizeMode(2, QHeaderView::Stretch);
m_results->header()->setSectionResizeMode(3, QHeaderView::Fixed);
m_results->header()->resizeSection(0, 200);
m_results->header()->resizeSection(1, 120);
m_results->header()->resizeSection(3, 60);
QFont mono("Monospace");
mono.setStyleHint(QFont::Monospace);
m_results->setFont(mono);
root->addWidget(m_results, 1);
// ---- Verbindungen ----
connect(m_btnAnalyze, &QPushButton::clicked, this, &DeadCodeDialog::onAnalyze);
connect(m_btnExport, &QPushButton::clicked, this, &DeadCodeDialog::onExportClicked);
connect(m_results, &QTreeWidget::itemActivated,
this, &DeadCodeDialog::onItemActivated);
}
// ---------------------------------------------------------------------------
// Öffentliche Schnittstelle
// ---------------------------------------------------------------------------
void DeadCodeDialog::setProjectRoot(const QString &path)
{
// Alte Exclude-Liste für das vorherige Projekt speichern
if (!m_projectRoot.isEmpty())
{
QSettings s(QSettings::IniFormat, QSettings::UserScope, "BareCode", "BareCode");
const QString oldKey = "deadcode/exclude/" +
QString(m_projectRoot).replace('/', '_').replace('\\', '_');
s.setValue(oldKey, m_excludeEdit->text());
}
m_projectRoot = path;
m_results->clear();
m_statusLabel->setText(QString());
m_btnExport->setEnabled(false);
// Exclude-Liste für das neue Projekt laden
if (!path.isEmpty())
{
QSettings s(QSettings::IniFormat, QSettings::UserScope, "BareCode", "BareCode");
const QString key = "deadcode/exclude/" +
QString(path).replace('/', '_').replace('\\', '_');
// Projektspezifisch vorhanden? Sonst globalen Fallback nehmen
const QString saved = s.value(key,
s.value("deadcode/excludeDirs", "vendor lib node_modules").toString()
).toString();
m_excludeEdit->setText(saved);
}
}
// ---------------------------------------------------------------------------
// Analyse starten
// ---------------------------------------------------------------------------
void DeadCodeDialog::onAnalyze()
{
if (m_projectRoot.isEmpty())
{
QMessageBox::information(this, tr("Analyse"),
tr("Bitte zuerst ein Projekt öffnen."));
return;
}
if (m_watcher->isRunning())
{
m_watcher->cancel();
m_watcher->waitForFinished();
}
// Exclude-Liste projektspezifisch speichern
QSettings s(QSettings::IniFormat, QSettings::UserScope, "BareCode", "BareCode");
const QString key = "deadcode/exclude/" +
QString(m_projectRoot).replace('/', '_').replace('\\', '_');
s.setValue(key, m_excludeEdit->text());
s.setValue("deadcode/excludeDirs", m_excludeEdit->text()); // globaler Fallback
const QStringList excludeDirs = m_excludeEdit->text()
.simplified()
.split(' ', Qt::SkipEmptyParts);
m_results->clear();
m_btnAnalyze->setEnabled(false);
m_btnExport->setEnabled(false);
m_progress->setValue(0);
m_progress->setMaximum(0); // Unbestimmter Modus bis wir die Dateizahl kennen
m_progress->show();
m_progressLabel->setText(tr("Dateien werden gezählt…"));
m_progressLabel->show();
m_statusLabel->setText(tr("Analysiere…"));
const QString root = m_projectRoot;
DeadCodeAnalyzer *analyzer = m_analyzer;
QFuture<QList<DeadCodeAnalyzer::DeadFunction>> future =
QtConcurrent::run([analyzer, root, excludeDirs]()
{
return analyzer->analyze(root, {"*.php"}, excludeDirs);
});
m_watcher->setFuture(future);
}
// ---------------------------------------------------------------------------
// Fortschritt aktualisieren (QueuedConnection — Thread-sicher)
// ---------------------------------------------------------------------------
void DeadCodeDialog::onProgressUpdate(int filesScanned, int filesTotal,
const QString &currentFile)
{
if (m_progress->maximum() != filesTotal)
{
m_progress->setMaximum(filesTotal);
}
m_progress->setValue(filesScanned);
m_progressLabel->setText(tr("(%1 / %2) %3")
.arg(filesScanned)
.arg(filesTotal)
.arg(currentFile));
}
// ---------------------------------------------------------------------------
// Analyse abgeschlossen
// ---------------------------------------------------------------------------
void DeadCodeDialog::onAnalysisFinished()
{
m_progress->hide();
m_progressLabel->hide();
m_btnAnalyze->setEnabled(true);
if (m_watcher->isCanceled())
{
return;
}
const QList<DeadCodeAnalyzer::DeadFunction> dead = m_watcher->result();
m_results->setSortingEnabled(false);
for (const DeadCodeAnalyzer::DeadFunction &d : dead)
{
QTreeWidgetItem *item = new QTreeWidgetItem(m_results);
item->setText(0, d.info.signature);
item->setText(1, d.info.className.isEmpty() ? tr("(global)") : d.info.className);
item->setText(2, QFileInfo(d.info.filePath).fileName());
item->setText(3, QString::number(d.info.line));
item->setToolTip(0, d.reason);
item->setToolTip(2, d.info.filePath);
item->setData(0, Qt::UserRole, d.info.filePath);
item->setData(0, Qt::UserRole + 1, d.info.line);
if (d.info.className.isEmpty())
{
item->setForeground(1, QColor("#888888"));
}
}
m_results->setSortingEnabled(true);
m_results->sortByColumn(0, Qt::AscendingOrder);
if (dead.isEmpty())
{
m_statusLabel->setText(tr("✓ Keine ungenutzten Funktionen gefunden."));
m_statusLabel->setStyleSheet("color: #44bb44;");
}
else
{
m_statusLabel->setText(tr("%1 Kandidaten gefunden").arg(dead.size()));
m_statusLabel->setStyleSheet("color: palette(mid);");
m_btnExport->setEnabled(true);
}
}
// ---------------------------------------------------------------------------
// Klick auf Treffer
// ---------------------------------------------------------------------------
void DeadCodeDialog::onItemActivated(QTreeWidgetItem *item, int /*column*/)
{
const QString path = item->data(0, Qt::UserRole).toString();
const int line = item->data(0, Qt::UserRole + 1).toInt();
if (!path.isEmpty() && line > 0)
{
emit fileLineRequested(path, line);
}
}
// ---------------------------------------------------------------------------
// Export
// ---------------------------------------------------------------------------
void DeadCodeDialog::onExportClicked()
{
const QString path = QFileDialog::getSaveFileName(
this,
tr("Ergebnis exportieren"),
QString("barecode_deadcode_%1.txt")
.arg(QDateTime::currentDateTime().toString("yyyyMMdd_HHmm")),
tr("Textdateien (*.txt)")
);
if (path.isEmpty()) { return; }
QFile file(path);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text))
{
QMessageBox::warning(this, tr("Export"),
tr("Datei konnte nicht geschrieben werden:\n%1").arg(path));
return;
}
QTextStream out(&file);
out.setEncoding(QStringConverter::Utf8);
out << "BareCode Analyse ungenutzter Funktionen\n";
out << "Projekt: " << m_projectRoot << "\n";
out << "Ausgeschlossen: " << m_excludeEdit->text() << "\n";
out << "Datum: " << QDateTime::currentDateTime().toString("dd.MM.yyyy HH:mm") << "\n";
out << QString("").repeated(70) << "\n\n";
for (int i = 0; i < m_results->topLevelItemCount(); ++i)
{
QTreeWidgetItem *item = m_results->topLevelItem(i);
out << item->text(0) << "\n";
out << " Klasse: " << item->text(1) << "\n";
out << " Datei: " << item->toolTip(2) << "\n";
out << " Zeile: " << item->text(3) << "\n\n";
}
out << QString("").repeated(70) << "\n";
out << m_results->topLevelItemCount() << " Kandidaten\n";
out << "Hinweis: Dynamische Aufrufe werden nicht erkannt.\n";
QMessageBox::information(this, tr("Export"),
tr("Exportiert nach:\n%1").arg(path));
}
// ---------------------------------------------------------------------------
// Fenstergeometrie speichern
// ---------------------------------------------------------------------------
void DeadCodeDialog::closeEvent(QCloseEvent *event)
{
if (m_watcher->isRunning())
{
m_watcher->cancel();
}
QSettings s(QSettings::IniFormat, QSettings::UserScope, "BareCode", "BareCode");
s.setValue("deadcode/geometry", saveGeometry());
event->accept();
}

View File

@@ -0,0 +1,62 @@
#pragma once
#include <QDialog>
#include <QTreeWidget>
#include <QTreeWidgetItem>
#include <QLabel>
#include <QPushButton>
#include <QProgressBar>
#include <QLineEdit>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QFutureWatcher>
#include <QString>
#include "DeadCodeAnalyzer.h"
class DeadCodeDialog : public QDialog
{
Q_OBJECT
public:
explicit DeadCodeDialog(QWidget *parent = nullptr);
void setProjectRoot(const QString &path);
signals:
void fileLineRequested(const QString &filePath, int line);
private slots:
void onAnalyze();
void onAnalysisFinished();
void onProgressUpdate(int filesScanned, int filesTotal,
const QString &currentFile);
void onItemActivated(QTreeWidgetItem *item, int column);
void onExportClicked();
protected:
void closeEvent(QCloseEvent *event) override;
private:
void setupUi();
QString m_projectRoot;
// Einstellungen
QLineEdit *m_excludeEdit = nullptr;
// Steuerung
QPushButton *m_btnAnalyze = nullptr;
QPushButton *m_btnExport = nullptr;
// Fortschritt
QProgressBar *m_progress = nullptr;
QLabel *m_progressLabel = nullptr;
// Status + Ergebnisse
QLabel *m_statusLabel = nullptr;
QTreeWidget *m_results = nullptr;
DeadCodeAnalyzer *m_analyzer = nullptr;
QFutureWatcher<QList<DeadCodeAnalyzer::DeadFunction>> *m_watcher = nullptr;
};

View File

@@ -0,0 +1,301 @@
#include "EditorPanel.h"
#include "EditorTab.h"
#include "CodeEditor.h"
#include "SearchPanel.h"
#include "FileSearchPanel.h"
#include "FunctionListDialog.h"
#include "DeadCodeDialog.h"
#include "FunctionIndex.h"
#include <QFileInfo>
#include <QFile>
#include <QTextDocument>
#include <QTextBlock>
EditorPanel::EditorPanel(Settings *settings, QWidget *parent)
: QWidget(parent)
, m_settings(settings)
{
setupUi();
}
// ---------------------------------------------------------------------------
// Setup
// ---------------------------------------------------------------------------
void EditorPanel::setupUi()
{
m_layout = new QVBoxLayout(this);
m_layout->setContentsMargins(0, 0, 0, 0);
m_layout->setSpacing(0);
m_tabWidget = new QTabWidget(this);
m_tabWidget->setTabsClosable(true);
m_tabWidget->setMovable(true);
m_tabWidget->setDocumentMode(true);
m_searchPanel = new SearchPanel(this);
m_fileSearch = new FileSearchPanel(this);
m_funcDialog = new FunctionListDialog(window());
m_deadCode = new DeadCodeDialog(window());
m_funcIndex = new FunctionIndex(this);
m_layout->addWidget(m_tabWidget, 1);
m_layout->addWidget(m_searchPanel, 0);
m_layout->addWidget(m_fileSearch, 0);
connect(m_tabWidget, &QTabWidget::tabCloseRequested,
this, &EditorPanel::onTabCloseRequested);
connect(m_tabWidget, &QTabWidget::currentChanged,
this, &EditorPanel::onCurrentTabChanged);
connect(m_fileSearch, &FileSearchPanel::fileLineRequested,
this, &EditorPanel::goToLine);
connect(m_funcDialog, &FunctionListDialog::fileLineRequested,
this, &EditorPanel::goToLine);
connect(m_deadCode, &DeadCodeDialog::fileLineRequested,
this, &EditorPanel::goToLine);
}
// ---------------------------------------------------------------------------
// Hilfsmethoden
// ---------------------------------------------------------------------------
EditorTab *EditorPanel::currentTab() const
{
return qobject_cast<EditorTab *>(m_tabWidget->currentWidget());
}
int EditorPanel::findTabForFile(const QString &filePath) const
{
EditorTab *tab = m_openTabs.value(filePath, nullptr);
return tab ? m_tabWidget->indexOf(tab) : -1;
}
// ---------------------------------------------------------------------------
// Öffentliche Slots
// ---------------------------------------------------------------------------
void EditorPanel::openFile(const QString &filePath)
{
const int existing = findTabForFile(filePath);
if (existing != -1)
{
m_tabWidget->setCurrentIndex(existing);
return;
}
EditorTab *tab = new EditorTab(filePath, m_settings, m_tabWidget);
const int index = m_tabWidget->addTab(tab, tab->fileName());
m_tabWidget->setCurrentIndex(index);
m_tabWidget->setTabToolTip(index, filePath);
m_openTabs.insert(filePath, tab);
// FunctionIndex dem Editor mitgeben für Doppelklick-Navigation
tab->editor()->setFunctionIndex(m_funcIndex);
// Doppelklick auf Funktion → zur Definition springen
connect(tab->editor(), &CodeEditor::navigateToRequested,
this, &EditorPanel::goToLine);
// Änderungsindikator im Tab-Titel (● = ungespeichert)
connect(tab->editor()->document(), &QTextDocument::modificationChanged,
this, [this, tab](bool modified)
{
const int idx = m_tabWidget->indexOf(tab);
if (idx == -1)
{
return;
}
const QString name = QFileInfo(tab->filePath()).fileName();
m_tabWidget->setTabText(idx, modified ? "" + name : name);
});
// Tab-Titel nach "Speichern unter" aktualisieren (neuer Dateiname, kein Punkt)
connect(tab->editor(), &CodeEditor::fileSaved, this, [this, tab](const QString &savedPath)
{
const int idx = m_tabWidget->indexOf(tab);
if (idx != -1)
{
m_tabWidget->setTabText(idx, QFileInfo(savedPath).fileName());
m_tabWidget->setTabToolTip(idx, savedPath);
}
emit currentFileSaved(savedPath);
// Index und Funktionsliste aktualisieren
m_funcIndex->refresh();
if (m_funcDialog->isVisible())
{
m_funcDialog->refresh();
}
});
}
void EditorPanel::saveCurrentFile()
{
if (EditorTab *tab = currentTab())
{
tab->save();
}
}
void EditorPanel::saveCurrentFileAs()
{
if (EditorTab *tab = currentTab())
{
tab->saveAs();
}
}
void EditorPanel::saveAllFiles()
{
for (int i = 0; i < m_tabWidget->count(); ++i)
{
EditorTab *tab = qobject_cast<EditorTab *>(m_tabWidget->widget(i));
if (tab && tab->isModified())
{
tab->save();
}
}
}
void EditorPanel::showSearchPanel()
{
m_fileSearch->hide();
m_searchPanel->activate();
}
void EditorPanel::showFileSearchPanel()
{
m_searchPanel->hide();
m_fileSearch->activate();
}
void EditorPanel::showFunctionList()
{
m_funcDialog->show();
m_funcDialog->raise();
m_funcDialog->activateWindow();
}
void EditorPanel::showDeadCode()
{
m_deadCode->show();
m_deadCode->raise();
m_deadCode->activateWindow();
}
void EditorPanel::setSearchRoot(const QString &path)
{
m_fileSearch->setSearchRoot(path);
m_funcDialog->setProjectRoot(path);
m_deadCode->setProjectRoot(path);
m_funcIndex->setProjectRoot(path);
}
void EditorPanel::goToLine(const QString &filePath, int line)
{
// Datei öffnen falls noch nicht geöffnet
openFile(filePath);
EditorTab *tab = m_openTabs.value(filePath, nullptr);
if (!tab)
{
return;
}
m_tabWidget->setCurrentWidget(tab);
// Zur gewünschten Zeile springen
CodeEditor *editor = tab->editor();
QTextBlock block = editor->document()->findBlockByLineNumber(line - 1);
if (block.isValid())
{
QTextCursor cursor(block);
cursor.movePosition(QTextCursor::StartOfBlock);
editor->setTextCursor(cursor);
editor->centerCursor();
editor->setFocus();
}
}
QStringList EditorPanel::openFilePaths() const
{
QStringList paths;
for (int i = 0; i < m_tabWidget->count(); ++i)
{
EditorTab *tab = qobject_cast<EditorTab *>(m_tabWidget->widget(i));
if (tab)
{
paths.append(tab->filePath());
}
}
return paths;
}
QString EditorPanel::activeFilePath() const
{
EditorTab *tab = currentTab();
return tab ? tab->filePath() : QString();
}
void EditorPanel::restoreSession(const QStringList &files, const QString &activeFile)
{
for (const QString &path : files)
{
if (QFile::exists(path))
{
openFile(path);
}
}
// Aktiven Tab wiederherstellen
if (!activeFile.isEmpty())
{
const int idx = findTabForFile(activeFile);
if (idx != -1)
{
m_tabWidget->setCurrentIndex(idx);
}
}
}
void EditorPanel::undo()
{
if (EditorTab *tab = currentTab())
{
tab->editor()->undo();
}
}
void EditorPanel::redo()
{
if (EditorTab *tab = currentTab())
{
tab->editor()->redo();
}
}
// ---------------------------------------------------------------------------
// Private Slots
// ---------------------------------------------------------------------------
void EditorPanel::onTabCloseRequested(int index)
{
EditorTab *tab = qobject_cast<EditorTab *>(m_tabWidget->widget(index));
if (!tab)
{
return;
}
m_openTabs.remove(tab->filePath());
m_tabWidget->removeTab(index);
tab->deleteLater();
m_searchPanel->setEditor(currentTab() ? currentTab()->editor() : nullptr);
}
void EditorPanel::onCurrentTabChanged(int /*index*/)
{
EditorTab *tab = currentTab();
m_searchPanel->setEditor(tab ? tab->editor() : nullptr);
}

View File

@@ -0,0 +1,68 @@
#pragma once
#include <QWidget>
#include <QTabWidget>
#include <QVBoxLayout>
#include <QHash>
#include <QString>
class EditorTab;
class Settings;
class SearchPanel;
class FileSearchPanel;
class FunctionListDialog;
class DeadCodeDialog;
class FunctionIndex;
// ---------------------------------------------------------------------------
// EditorPanel Rechtes Panel: Tab-Leiste + Editoren + Such/Ersetzen-Panel.
// ---------------------------------------------------------------------------
class EditorPanel : public QWidget
{
Q_OBJECT
public:
explicit EditorPanel(Settings *settings, QWidget *parent = nullptr);
public slots:
void openFile(const QString &filePath);
void saveCurrentFile();
void saveCurrentFileAs();
void saveAllFiles();
void setSearchRoot(const QString &path);
void showSearchPanel();
void showFileSearchPanel();
void showFunctionList();
void showDeadCode();
void goToLine(const QString &filePath, int line);
// Session
QStringList openFilePaths() const;
QString activeFilePath() const;
void restoreSession(const QStringList &files, const QString &activeFile);
void undo();
void redo();
signals:
void currentFileSaved(const QString &filePath);
private slots:
void onTabCloseRequested(int index);
void onCurrentTabChanged(int index);
private:
void setupUi();
int findTabForFile(const QString &filePath) const;
EditorTab *currentTab() const;
Settings *m_settings = nullptr;
QVBoxLayout *m_layout = nullptr;
QTabWidget *m_tabWidget = nullptr;
SearchPanel *m_searchPanel = nullptr;
FileSearchPanel *m_fileSearch = nullptr;
FunctionListDialog *m_funcDialog = nullptr;
DeadCodeDialog *m_deadCode = nullptr;
FunctionIndex *m_funcIndex = nullptr;
QHash<QString, EditorTab *> m_openTabs;
};

View File

@@ -0,0 +1,53 @@
#include "EditorTab.h"
#include "CodeEditor.h"
#include <QFileInfo>
EditorTab::EditorTab(const QString &filePath, Settings *settings, QWidget *parent)
: QWidget(parent)
, m_filePath(filePath)
{
m_layout = new QVBoxLayout(this);
m_layout->setContentsMargins(0, 0, 0, 0);
m_layout->setSpacing(0);
m_editor = new CodeEditor(settings, this);
m_editor->loadFile(filePath);
m_layout->addWidget(m_editor);
}
QString EditorTab::filePath() const
{
return m_filePath;
}
QString EditorTab::fileName() const
{
return QFileInfo(m_filePath).fileName();
}
CodeEditor *EditorTab::editor() const
{
return m_editor;
}
bool EditorTab::isModified() const
{
return m_editor->isModified();
}
bool EditorTab::save()
{
const bool ok = m_editor->save();
// Path may have changed if this was an untitled buffer saved for the first time
m_filePath = m_editor->filePath();
return ok;
}
bool EditorTab::saveAs()
{
const bool ok = m_editor->saveAs();
m_filePath = m_editor->filePath();
return ok;
}

View File

@@ -0,0 +1,33 @@
#pragma once
#include <QWidget>
#include <QString>
#include <QVBoxLayout>
class CodeEditor;
class Settings;
// ---------------------------------------------------------------------------
// EditorTab Widget placed inside each tab of the tab bar.
// Owns a CodeEditor for a single file.
// ---------------------------------------------------------------------------
class EditorTab : public QWidget
{
Q_OBJECT
public:
explicit EditorTab(const QString &filePath, Settings *settings, QWidget *parent = nullptr);
QString filePath() const;
QString fileName() const;
CodeEditor *editor() const;
bool isModified() const;
bool save();
bool saveAs();
private:
QString m_filePath;
QVBoxLayout *m_layout = nullptr;
CodeEditor *m_editor = nullptr;
};

View File

@@ -0,0 +1,330 @@
#include "FileSearchPanel.h"
#include <QDir>
#include <QDirIterator>
#include <QFile>
#include <QTextStream>
#include <QRegularExpression>
#include <QKeyEvent>
#include <QtConcurrent/QtConcurrent>
#include <QFuture>
// ---------------------------------------------------------------------------
// Konstruktor
// ---------------------------------------------------------------------------
FileSearchPanel::FileSearchPanel(QWidget *parent)
: QWidget(parent)
{
setupUi();
hide();
m_watcher = new QFutureWatcher<QList<Match>>(this);
connect(m_watcher, &QFutureWatcher<QList<Match>>::finished,
this, &FileSearchPanel::onSearchFinished);
}
// ---------------------------------------------------------------------------
// UI
// ---------------------------------------------------------------------------
void FileSearchPanel::setupUi()
{
QVBoxLayout *root = new QVBoxLayout(this);
root->setContentsMargins(6, 4, 6, 4);
root->setSpacing(4);
// ---- Zeile 1: Suchbegriff ----
QHBoxLayout *row1 = new QHBoxLayout();
row1->addWidget(new QLabel(tr("Suchen in Dateien:"), this));
m_searchEdit = new QLineEdit(this);
m_searchEdit->setPlaceholderText(tr("Suchbegriff…"));
m_searchEdit->setClearButtonEnabled(true);
row1->addWidget(m_searchEdit, 1);
m_btnSearch = new QPushButton(tr("Suchen"), this);
m_btnSearch->setDefault(true);
row1->addWidget(m_btnSearch);
m_btnClose = new QPushButton(tr(""), this);
m_btnClose->setFixedWidth(24);
m_btnClose->setFlat(true);
m_btnClose->setToolTip(tr("Schließen"));
row1->addWidget(m_btnClose);
root->addLayout(row1);
// ---- Zeile 2: Optionen + Filter ----
QHBoxLayout *row2 = new QHBoxLayout();
m_chkCase = new QCheckBox(tr("Groß-/Kleinschreibung"), this);
m_chkWord = new QCheckBox(tr("Ganzes Wort"), this);
m_chkRegex = new QCheckBox(tr("Regex"), this);
row2->addWidget(m_chkCase);
row2->addWidget(m_chkWord);
row2->addWidget(m_chkRegex);
row2->addSpacing(12);
row2->addWidget(new QLabel(tr("Dateitypen:"), this));
m_filterEdit = new QLineEdit(this);
m_filterEdit->setText("*.html *.php *.css *.js *.c *.cpp *.h");
m_filterEdit->setFixedWidth(220);
m_filterEdit->setToolTip(tr("Leerzeichen-getrennte Muster, z.B.: *.php *.html"));
row2->addWidget(m_filterEdit);
row2->addStretch();
root->addLayout(row2);
// ---- Fortschritt + Status ----
m_progress = new QProgressBar(this);
m_progress->setRange(0, 0); // Unbestimmter Modus
m_progress->setFixedHeight(4);
m_progress->hide();
root->addWidget(m_progress);
m_statusLabel = new QLabel(this);
m_statusLabel->setStyleSheet("color: palette(mid);");
root->addWidget(m_statusLabel);
// ---- Ergebnisliste ----
m_results = new QTreeWidget(this);
m_results->setHeaderHidden(true);
m_results->setRootIsDecorated(true);
m_results->setIndentation(16);
m_results->setUniformRowHeights(true);
m_results->setAlternatingRowColors(true);
root->addWidget(m_results, 1);
// ---- Verbindungen ----
connect(m_btnSearch, &QPushButton::clicked, this, &FileSearchPanel::onSearch);
connect(m_searchEdit, &QLineEdit::returnPressed, this, &FileSearchPanel::onSearch);
connect(m_btnClose, &QPushButton::clicked, this, [this]()
{
hide();
});
connect(m_results, &QTreeWidget::itemActivated,
this, &FileSearchPanel::onResultActivated);
}
// ---------------------------------------------------------------------------
// Öffentliche Schnittstelle
// ---------------------------------------------------------------------------
void FileSearchPanel::setSearchRoot(const QString &path)
{
m_searchRoot = path;
}
void FileSearchPanel::activate()
{
show();
m_searchEdit->setFocus();
m_searchEdit->selectAll();
}
// ---------------------------------------------------------------------------
// Suche starten
// ---------------------------------------------------------------------------
void FileSearchPanel::onSearch()
{
const QString needle = m_searchEdit->text().trimmed();
if (needle.isEmpty())
{
return;
}
if (m_searchRoot.isEmpty())
{
m_statusLabel->setText(tr("Kein Projektverzeichnis geöffnet."));
return;
}
// Laufende Suche abbrechen
if (m_watcher->isRunning())
{
m_watcher->cancel();
m_watcher->waitForFinished();
}
m_results->clear();
m_statusLabel->setText(tr("Suche läuft…"));
m_progress->show();
m_btnSearch->setEnabled(false);
const QString root = m_searchRoot;
const bool cs = m_chkCase->isChecked();
const bool word = m_chkWord->isChecked();
const bool regex = m_chkRegex->isChecked();
const QStringList extensions = m_filterEdit->text().simplified().split(' ',
Qt::SkipEmptyParts);
QFuture<QList<Match>> future = QtConcurrent::run(
[this, root, needle, cs, word, regex, extensions]()
{
return searchInFiles(root, needle, cs, word, regex, extensions);
}
);
m_watcher->setFuture(future);
}
// ---------------------------------------------------------------------------
// Suchergebnisse anzeigen
// ---------------------------------------------------------------------------
void FileSearchPanel::onSearchFinished()
{
m_progress->hide();
m_btnSearch->setEnabled(true);
if (m_watcher->isCanceled())
{
return;
}
const QList<Match> matches = m_watcher->result();
// Ergebnisse gruppiert nach Datei aufbauen
QString currentFile;
QTreeWidgetItem *fileItem = nullptr;
int fileCount = 0;
int matchCount = 0;
for (const Match &m : matches)
{
if (m.filePath != currentFile)
{
currentFile = m.filePath;
++fileCount;
fileItem = new QTreeWidgetItem(m_results);
fileItem->setText(0, QFileInfo(m.filePath).fileName());
fileItem->setToolTip(0, m.filePath);
fileItem->setData(0, Qt::UserRole, m.filePath);
fileItem->setData(0, Qt::UserRole + 1, -1);
QFont boldFont = fileItem->font(0);
boldFont.setBold(true);
fileItem->setFont(0, boldFont);
fileItem->setExpanded(true);
}
QTreeWidgetItem *lineItem = new QTreeWidgetItem(fileItem);
lineItem->setText(0, QString(" Zeile %1: %2")
.arg(m.line)
.arg(m.content.trimmed().left(120)));
lineItem->setToolTip(0, m.content.trimmed());
lineItem->setData(0, Qt::UserRole, m.filePath);
lineItem->setData(0, Qt::UserRole + 1, m.line);
++matchCount;
}
// Datei-Titelzeilen um Trefferanzahl ergänzen
for (int i = 0; i < m_results->topLevelItemCount(); ++i)
{
QTreeWidgetItem *item = m_results->topLevelItem(i);
const int count = item->childCount();
item->setText(0, QString("%1 (%2 Treffer)")
.arg(QFileInfo(item->data(0, Qt::UserRole).toString()).fileName())
.arg(count));
}
if (matchCount == 0)
{
m_statusLabel->setText(tr("Keine Treffer gefunden."));
}
else
{
m_statusLabel->setText(tr("%1 Treffer in %2 Datei(en).")
.arg(matchCount)
.arg(fileCount));
}
}
// ---------------------------------------------------------------------------
// Klick auf Treffer → Datei + Zeile öffnen
// ---------------------------------------------------------------------------
void FileSearchPanel::onResultActivated(QTreeWidgetItem *item, int /*column*/)
{
const QString path = item->data(0, Qt::UserRole).toString();
const int line = item->data(0, Qt::UserRole + 1).toInt();
if (path.isEmpty() || line < 0)
{
// Datei-Titelzeile: nur auf-/zuklappen
item->setExpanded(!item->isExpanded());
return;
}
emit fileLineRequested(path, line);
}
// ---------------------------------------------------------------------------
// Eigentliche Suchroutine (läuft in Thread-Pool)
// ---------------------------------------------------------------------------
QList<FileSearchPanel::Match> FileSearchPanel::searchInFiles(
const QString &root,
const QString &needle,
bool caseSensitive,
bool wholeWord,
bool useRegex,
const QStringList &extensions) const
{
QList<Match> results;
// Regulären Ausdruck vorbereiten
QString pattern = useRegex ? needle : QRegularExpression::escape(needle);
if (wholeWord)
{
pattern = "\\b" + pattern + "\\b";
}
QRegularExpression re(pattern,
caseSensitive
? QRegularExpression::NoPatternOption
: QRegularExpression::CaseInsensitiveOption);
if (!re.isValid())
{
return results;
}
// Verzeichnis rekursiv durchsuchen
QDirIterator it(root,
extensions.isEmpty()
? QStringList("*")
: extensions,
QDir::Files,
QDirIterator::Subdirectories);
while (it.hasNext())
{
if (m_watcher->isCanceled())
{
break;
}
const QString filePath = it.next();
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
{
continue;
}
QTextStream stream(&file);
stream.setEncoding(QStringConverter::Utf8);
int lineNumber = 0;
while (!stream.atEnd())
{
++lineNumber;
const QString line = stream.readLine();
if (re.match(line).hasMatch())
{
results.append({ filePath, lineNumber, line });
}
}
}
return results;
}

View File

@@ -0,0 +1,77 @@
#pragma once
#include <QWidget>
#include <QLineEdit>
#include <QPushButton>
#include <QCheckBox>
#include <QTreeWidget>
#include <QTreeWidgetItem>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QProgressBar>
#include <QFutureWatcher>
#include <QString>
#include <QStringList>
// ---------------------------------------------------------------------------
// FileSearchPanel Suche in allen Dateien eines Verzeichnisses.
//
// Ergebnisse werden als aufklappbare Liste angezeigt:
// Dateiname (N Treffer)
// └ Zeile 12: <Zeileninhalt>
// └ Zeile 34: <Zeileninhalt>
//
// Klick auf einen Treffer öffnet die Datei im Editor und springt zur Zeile.
// ---------------------------------------------------------------------------
class FileSearchPanel : public QWidget
{
Q_OBJECT
public:
explicit FileSearchPanel(QWidget *parent = nullptr);
void setSearchRoot(const QString &path);
public slots:
void activate();
signals:
void fileLineRequested(const QString &filePath, int line);
private slots:
void onSearch();
void onResultActivated(QTreeWidgetItem *item, int column);
void onSearchFinished();
private:
struct Match
{
QString filePath;
int line;
QString content;
};
void setupUi();
QList<Match> searchInFiles(const QString &root,
const QString &needle,
bool caseSensitive,
bool wholeWord,
bool useRegex,
const QStringList &extensions) const;
QString m_searchRoot;
QLineEdit *m_searchEdit = nullptr;
QCheckBox *m_chkCase = nullptr;
QCheckBox *m_chkWord = nullptr;
QCheckBox *m_chkRegex = nullptr;
QLineEdit *m_filterEdit = nullptr; // Dateiendungen-Filter
QPushButton *m_btnSearch = nullptr;
QPushButton *m_btnClose = nullptr;
QLabel *m_statusLabel = nullptr;
QTreeWidget *m_results = nullptr;
QProgressBar *m_progress = nullptr;
QFutureWatcher<QList<Match>> *m_watcher = nullptr;
};

View File

@@ -0,0 +1,83 @@
#include "FunctionIndex.h"
#include <QtConcurrent/QtConcurrent>
FunctionIndex::FunctionIndex(QObject *parent)
: QObject(parent)
{
m_scanner = new FunctionScanner(this);
m_watcher = new QFutureWatcher<QList<FunctionScanner::FunctionInfo>>(this);
connect(m_watcher, &QFutureWatcher<QList<FunctionScanner::FunctionInfo>>::finished,
this, &FunctionIndex::onScanFinished);
}
void FunctionIndex::setProjectRoot(const QString &path)
{
m_projectRoot = path;
m_ready = false;
m_index.clear();
if (!path.isEmpty())
{
refresh();
}
}
void FunctionIndex::refresh()
{
if (m_projectRoot.isEmpty())
{
return;
}
if (m_watcher->isRunning())
{
m_watcher->cancel();
m_watcher->waitForFinished();
}
FunctionScanner *scanner = m_scanner;
const QString root = m_projectRoot;
m_watcher->setFuture(
QtConcurrent::run([scanner, root]()
{
return scanner->scanDirectory(root);
})
);
}
void FunctionIndex::onScanFinished()
{
if (m_watcher->isCanceled())
{
return;
}
m_index.clear();
const QList<FunctionScanner::FunctionInfo> all = m_watcher->result();
for (const FunctionScanner::FunctionInfo &func : all)
{
const QString key = func.name.toLower();
// Ersten Eintrag bevorzugen (globale Funktionen vor Methoden)
if (!m_index.contains(key) || func.className.isEmpty())
{
m_index.insert(key, func);
}
}
m_ready = true;
emit indexReady();
}
FunctionScanner::FunctionInfo FunctionIndex::lookup(const QString &name) const
{
return m_index.value(name.toLower(), FunctionScanner::FunctionInfo{});
}
bool FunctionIndex::isReady() const
{
return m_ready;
}

View File

@@ -0,0 +1,47 @@
#pragma once
#include <QObject>
#include <QHash>
#include <QString>
#include <QStringList>
#include <QFutureWatcher>
#include "FunctionScanner.h"
// ---------------------------------------------------------------------------
// FunctionIndex Singleton-artiger Index aller Projektfunktionen.
// Wird von CodeEditor (Doppelklick-Navigation) und SignatureHelper genutzt.
// Aktualisiert sich asynchron wenn setProjectRoot() aufgerufen wird.
// ---------------------------------------------------------------------------
class FunctionIndex : public QObject
{
Q_OBJECT
public:
explicit FunctionIndex(QObject *parent = nullptr);
void setProjectRoot(const QString &path);
void refresh();
// Sucht eine Funktion nach Name (Groß-/Kleinschreibung ignoriert)
// Gibt ungültige FunctionInfo zurück wenn nicht gefunden (filePath ist leer)
FunctionScanner::FunctionInfo lookup(const QString &name) const;
bool isReady() const;
signals:
void indexReady();
private slots:
void onScanFinished();
private:
QString m_projectRoot;
FunctionScanner *m_scanner = nullptr;
QFutureWatcher<QList<FunctionScanner::FunctionInfo>> *m_watcher = nullptr;
// name.toLower() → FunctionInfo
QHash<QString, FunctionScanner::FunctionInfo> m_index;
bool m_ready = false;
};

View File

@@ -0,0 +1,52 @@
#include "FunctionListDialog.h"
#include "FunctionListPanel.h"
#include <QVBoxLayout>
#include <QSettings>
#include <QCloseEvent>
FunctionListDialog::FunctionListDialog(QWidget *parent)
: QDialog(parent, Qt::Window)
{
setWindowTitle(tr("Projektfunktionen BareCode"));
setMinimumSize(500, 400);
// Fenstergröße und -position wiederherstellen
QSettings s(QSettings::IniFormat, QSettings::UserScope, "BareCode", "BareCode");
if (s.contains("funclist/geometry"))
{
restoreGeometry(s.value("funclist/geometry").toByteArray());
}
else
{
resize(600, 700);
}
QVBoxLayout *layout = new QVBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
layout->setSpacing(0);
m_panel = new FunctionListPanel(this);
m_panel->show(); // Immer sichtbar — Panel ist jetzt der gesamte Dialog-Inhalt
layout->addWidget(m_panel);
connect(m_panel, &FunctionListPanel::fileLineRequested,
this, &FunctionListDialog::fileLineRequested);
}
void FunctionListDialog::setProjectRoot(const QString &path)
{
m_panel->setProjectRoot(path);
}
void FunctionListDialog::refresh()
{
m_panel->refresh();
}
void FunctionListDialog::closeEvent(QCloseEvent *event)
{
QSettings s(QSettings::IniFormat, QSettings::UserScope, "BareCode", "BareCode");
s.setValue("funclist/geometry", saveGeometry());
event->accept();
}

View File

@@ -0,0 +1,30 @@
#pragma once
#include <QDialog>
#include <QString>
class FunctionListPanel;
// ---------------------------------------------------------------------------
// FunctionListDialog Eigenständiges Fenster für die Projektfunktionen.
// Bleibt offen während man im Editor arbeitet (non-modal).
// ---------------------------------------------------------------------------
class FunctionListDialog : public QDialog
{
Q_OBJECT
public:
explicit FunctionListDialog(QWidget *parent = nullptr);
void setProjectRoot(const QString &path);
void refresh();
protected:
void closeEvent(QCloseEvent *event) override;
signals:
void fileLineRequested(const QString &filePath, int line);
private:
FunctionListPanel *m_panel = nullptr;
};

View File

@@ -0,0 +1,332 @@
#include "FunctionListPanel.h"
#include <QtConcurrent/QtConcurrent>
#include <QFileInfo>
#include <QFont>
#include <QProgressBar>
FunctionListPanel::FunctionListPanel(QWidget *parent)
: QWidget(parent)
{
m_scanner = new FunctionScanner(this);
m_watcher = new QFutureWatcher<QList<FunctionScanner::FunctionInfo>>(this);
connect(m_watcher, &QFutureWatcher<QList<FunctionScanner::FunctionInfo>>::finished,
this, &FunctionListPanel::onScanFinished);
setupUi();
}
// ---------------------------------------------------------------------------
// UI
// ---------------------------------------------------------------------------
void FunctionListPanel::setupUi()
{
QVBoxLayout *root = new QVBoxLayout(this);
root->setContentsMargins(0, 0, 0, 0);
root->setSpacing(0);
// ---- Kopfzeile ----
QWidget *header = new QWidget(this);
header->setStyleSheet("background: palette(mid);");
QHBoxLayout *headerLayout = new QHBoxLayout(header);
headerLayout->setContentsMargins(6, 4, 6, 4);
headerLayout->setSpacing(4);
QLabel *title = new QLabel(tr("Projektfunktionen"), header);
QFont boldFont = title->font();
boldFont.setBold(true);
title->setFont(boldFont);
headerLayout->addWidget(title);
headerLayout->addStretch();
m_btnRefresh = new QPushButton(tr(""), header);
m_btnRefresh->setFixedSize(24, 24);
m_btnRefresh->setFlat(true);
m_btnRefresh->setToolTip(tr("Neu scannen"));
headerLayout->addWidget(m_btnRefresh);
root->addWidget(header);
// ---- Filter + Gruppierung ----
QHBoxLayout *toolRow = new QHBoxLayout();
toolRow->setContentsMargins(6, 4, 6, 4);
toolRow->setSpacing(4);
m_filterEdit = new QLineEdit(this);
m_filterEdit->setPlaceholderText(tr("Funktion suchen…"));
m_filterEdit->setClearButtonEnabled(true);
toolRow->addWidget(m_filterEdit, 1);
m_groupCombo = new QComboBox(this);
m_groupCombo->addItem(tr("Nach Datei"), "file");
m_groupCombo->addItem(tr("Nach Klasse"), "class");
m_groupCombo->addItem(tr("Alphabetisch"), "alpha");
m_groupCombo->setFixedWidth(120);
toolRow->addWidget(m_groupCombo);
root->addLayout(toolRow);
// ---- Status ----
m_statusLabel = new QLabel(this);
m_statusLabel->setContentsMargins(6, 0, 6, 2);
m_statusLabel->setStyleSheet("color: palette(mid); font-size: 11px;");
root->addWidget(m_statusLabel);
// ---- Ergebnisbaum ----
m_tree = new QTreeWidget(this);
m_tree->setHeaderHidden(true);
m_tree->setRootIsDecorated(true);
m_tree->setIndentation(14);
m_tree->setAlternatingRowColors(false);
m_tree->setAnimated(true);
// Monospace-Font für Parameter
QFont monoFont("Monospace", m_tree->font().pointSize());
monoFont.setStyleHint(QFont::Monospace);
m_tree->setFont(monoFont);
root->addWidget(m_tree, 1);
// ---- Verbindungen ----
connect(m_btnRefresh, &QPushButton::clicked,
this, &FunctionListPanel::refresh);
connect(m_filterEdit, &QLineEdit::textChanged,
this, &FunctionListPanel::onFilterChanged);
connect(m_groupCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, &FunctionListPanel::onGroupingChanged);
connect(m_tree, &QTreeWidget::itemActivated,
this, &FunctionListPanel::onItemActivated);
}
// ---------------------------------------------------------------------------
// Öffentliche Schnittstelle
// ---------------------------------------------------------------------------
void FunctionListPanel::setProjectRoot(const QString &path)
{
m_projectRoot = path;
if (!path.isEmpty())
{
refresh();
}
else
{
m_tree->clear();
m_statusLabel->setText(QString());
m_lastResult.clear();
}
}
void FunctionListPanel::activate()
{
m_filterEdit->setFocus();
m_filterEdit->selectAll();
if (m_lastResult.isEmpty() && !m_projectRoot.isEmpty())
{
refresh();
}
}
// ---------------------------------------------------------------------------
// Scan starten
// ---------------------------------------------------------------------------
void FunctionListPanel::refresh()
{
if (m_projectRoot.isEmpty())
{
m_statusLabel->setText(tr("Kein Projekt geöffnet."));
return;
}
if (m_watcher->isRunning())
{
m_watcher->cancel();
m_watcher->waitForFinished();
}
m_statusLabel->setText(tr("Scanne…"));
m_btnRefresh->setEnabled(false);
const QString root = m_projectRoot;
FunctionScanner *scanner = m_scanner;
QFuture<QList<FunctionScanner::FunctionInfo>> future =
QtConcurrent::run([scanner, root]()
{
return scanner->scanDirectory(root);
});
m_watcher->setFuture(future);
}
// ---------------------------------------------------------------------------
// Scan abgeschlossen
// ---------------------------------------------------------------------------
void FunctionListPanel::onScanFinished()
{
m_btnRefresh->setEnabled(true);
if (m_watcher->isCanceled())
{
return;
}
m_lastResult = m_watcher->result();
populateTree(m_lastResult);
}
// ---------------------------------------------------------------------------
// Baum befüllen
// ---------------------------------------------------------------------------
void FunctionListPanel::populateTree(const QList<FunctionScanner::FunctionInfo> &functions)
{
m_tree->clear();
const QString groupBy = m_groupCombo->currentData().toString();
const QString filter = m_filterEdit->text().trimmed().toLower();
// Funktionen filtern
QList<FunctionScanner::FunctionInfo> filtered;
for (const auto &f : functions)
{
if (filter.isEmpty() ||
f.name.toLower().contains(filter) ||
f.parameters.toLower().contains(filter) ||
f.className.toLower().contains(filter))
{
filtered.append(f);
}
}
if (groupBy == "alpha")
{
// Alphabetisch — keine Gruppen
std::sort(filtered.begin(), filtered.end(),
[](const FunctionScanner::FunctionInfo &a,
const FunctionScanner::FunctionInfo &b)
{
return a.name.toLower() < b.name.toLower();
});
for (const auto &f : filtered)
{
QTreeWidgetItem *item = new QTreeWidgetItem(m_tree);
formatFunctionItem(item, f);
}
}
else
{
// Gruppiert nach Datei oder Klasse
QMap<QString, QList<FunctionScanner::FunctionInfo>> groups;
for (const auto &f : filtered)
{
QString key;
if (groupBy == "class")
{
key = f.className.isEmpty() ? tr("(global)") : f.className;
}
else
{
key = QFileInfo(f.filePath).fileName();
}
groups[key].append(f);
}
for (auto it = groups.begin(); it != groups.end(); ++it)
{
QTreeWidgetItem *groupItem = new QTreeWidgetItem(m_tree);
groupItem->setText(0, QString("%1 (%2)")
.arg(it.key())
.arg(it.value().size()));
groupItem->setData(0, Qt::UserRole, QString());
groupItem->setData(0, Qt::UserRole + 1, -1);
QFont boldFont = groupItem->font(0);
boldFont.setBold(true);
groupItem->setFont(0, boldFont);
groupItem->setExpanded(true);
// Funktionen innerhalb der Gruppe alphabetisch
auto &list = it.value();
std::sort(list.begin(), list.end(),
[](const FunctionScanner::FunctionInfo &a,
const FunctionScanner::FunctionInfo &b)
{
return a.name.toLower() < b.name.toLower();
});
for (const auto &f : list)
{
QTreeWidgetItem *item = new QTreeWidgetItem(groupItem);
formatFunctionItem(item, f);
}
}
}
// Statuszeile
const int total = functions.size();
const int shown = filtered.size();
if (filter.isEmpty())
{
m_statusLabel->setText(tr("%1 Funktionen gefunden").arg(total));
}
else
{
m_statusLabel->setText(tr("%1 von %2 Funktionen").arg(shown).arg(total));
}
}
void FunctionListPanel::formatFunctionItem(QTreeWidgetItem *item,
const FunctionScanner::FunctionInfo &f)
{
// Anzeige: name(parameter) — Zeilennummer klein dahinter
const QString display = QString("%1(%2)")
.arg(f.name)
.arg(f.parameters);
item->setText(0, display);
item->setToolTip(0, QString("%1\nZeile %2").arg(f.filePath).arg(f.line));
item->setData(0, Qt::UserRole, f.filePath);
item->setData(0, Qt::UserRole + 1, f.line);
// Zeilennummer in gedimmter Farbe als zweite Spalte simulieren
// (über ToolTip gelöst da wir nur eine Spalte haben)
}
// ---------------------------------------------------------------------------
// Slots
// ---------------------------------------------------------------------------
void FunctionListPanel::onItemActivated(QTreeWidgetItem *item, int /*column*/)
{
const QString path = item->data(0, Qt::UserRole).toString();
const int line = item->data(0, Qt::UserRole + 1).toInt();
if (path.isEmpty() || line < 0)
{
item->setExpanded(!item->isExpanded());
return;
}
emit fileLineRequested(path, line);
}
void FunctionListPanel::onFilterChanged(const QString &text)
{
Q_UNUSED(text)
if (!m_lastResult.isEmpty())
{
populateTree(m_lastResult);
}
}
void FunctionListPanel::onGroupingChanged(int /*index*/)
{
if (!m_lastResult.isEmpty())
{
populateTree(m_lastResult);
}
}

View File

@@ -0,0 +1,67 @@
#pragma once
#include <QWidget>
#include <QTreeWidget>
#include <QTreeWidgetItem>
#include <QLineEdit>
#include <QLabel>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QPushButton>
#include <QComboBox>
#include <QFutureWatcher>
#include <QString>
#include "FunctionScanner.h"
// ---------------------------------------------------------------------------
// FunctionListPanel Zeigt alle im Projekt definierten Funktionen
// als durchsuchbare, sortierbare Liste.
//
// Gruppierung: nach Datei oder nach Klasse
// Klick: öffnet Datei und springt zur Definition
// Aktualisierung: automatisch nach jedem Speichern
// ---------------------------------------------------------------------------
class FunctionListPanel : public QWidget
{
Q_OBJECT
public:
explicit FunctionListPanel(QWidget *parent = nullptr);
void setProjectRoot(const QString &path);
public slots:
void refresh();
void activate();
signals:
void fileLineRequested(const QString &filePath, int line);
private slots:
void onScanFinished();
void onItemActivated(QTreeWidgetItem *item, int column);
void onFilterChanged(const QString &text);
void onGroupingChanged(int index);
private:
void setupUi();
void populateTree(const QList<FunctionScanner::FunctionInfo> &functions);
void applyFilter(const QString &text);
static void formatFunctionItem(QTreeWidgetItem *item,
const FunctionScanner::FunctionInfo &f);
QString m_projectRoot;
QLineEdit *m_filterEdit = nullptr;
QComboBox *m_groupCombo = nullptr;
QPushButton *m_btnRefresh = nullptr;
QLabel *m_statusLabel = nullptr;
QTreeWidget *m_tree = nullptr;
FunctionScanner *m_scanner = nullptr;
QFutureWatcher<QList<FunctionScanner::FunctionInfo>> *m_watcher = nullptr;
// Letztes Scan-Ergebnis für Filter ohne Neuscan
QList<FunctionScanner::FunctionInfo> m_lastResult;
};

View File

@@ -0,0 +1,158 @@
#include "FunctionScanner.h"
#include <QDir>
#include <QDirIterator>
#include <QFile>
#include <QTextStream>
#include <QRegularExpression>
FunctionScanner::FunctionScanner(QObject *parent)
: QObject(parent)
{
}
// ---------------------------------------------------------------------------
// Verzeichnis rekursiv scannen
// ---------------------------------------------------------------------------
QList<FunctionScanner::FunctionInfo> FunctionScanner::scanDirectory(
const QString &rootPath,
const QStringList &extensions) const
{
QList<FunctionInfo> results;
QDirIterator it(rootPath,
extensions,
QDir::Files,
QDirIterator::Subdirectories);
while (it.hasNext())
{
const QString path = it.next();
results.append(scanFile(path));
}
return results;
}
// ---------------------------------------------------------------------------
// Einzelne Datei scannen
// ---------------------------------------------------------------------------
QList<FunctionScanner::FunctionInfo> FunctionScanner::scanFile(const QString &filePath) const
{
QList<FunctionInfo> results;
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
{
return results;
}
QTextStream stream(&file);
stream.setEncoding(QStringConverter::Utf8);
QStringList lines;
while (!stream.atEnd())
{
lines.append(stream.readLine());
}
// Regex für Funktionsdefinitionen:
// optional: public/protected/private/static/abstract/final
// gefolgt von: function name(
static const QRegularExpression funcRegex(
R"((?:(?:public|protected|private|static|abstract|final)\s+)*)"
R"(function\s+(&?\s*[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)\s*\()",
QRegularExpression::CaseInsensitiveOption
);
for (int i = 0; i < lines.size(); ++i)
{
const QString &line = lines[i];
// Zeilen in Kommentaren überspringen
const QString trimmed = line.trimmed();
if (trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("#"))
{
continue;
}
QRegularExpressionMatch match = funcRegex.match(line);
if (!match.hasMatch())
{
continue;
}
FunctionInfo info;
info.name = match.captured(1).trimmed();
info.filePath = filePath;
info.line = i + 1;
info.className = detectClassContext(lines, i);
info.parameters = extractParameters(line, static_cast<int>(match.capturedEnd()) - 1);
info.signature = info.name + "(" + info.parameters + ")";
// Konstruktoren und magische Methoden kennzeichnen
// aber trotzdem aufnehmen — sie sind nützlich in der Liste
results.append(info);
}
return results;
}
// ---------------------------------------------------------------------------
// Parameter aus der Funktionssignatur extrahieren
// Behandelt auch mehrzeilige Signaturen mit öffnender Klammer am Ende
// ---------------------------------------------------------------------------
QString FunctionScanner::extractParameters(const QString &line, int parenPos)
{
// Inhalt zwischen ( und ) extrahieren
// Einfache Version: nur die erste Zeile — reicht für 99% aller Fälle
const int openParen = parenPos;
int depth = 0;
int closePos = -1;
for (int i = openParen; i < line.length(); ++i)
{
if (line[i] == '(') { ++depth; }
else if (line[i] == ')')
{
--depth;
if (depth == 0)
{
closePos = i;
break;
}
}
}
if (closePos == -1)
{
// Klammer geht über Zeilenende — gekürzt anzeigen
return line.mid(openParen + 1).trimmed() + "";
}
return line.mid(openParen + 1, closePos - openParen - 1).trimmed();
}
// ---------------------------------------------------------------------------
// Klassenkontext erkennen — rückwärts durch den Code suchen
// ---------------------------------------------------------------------------
QString FunctionScanner::detectClassContext(const QStringList &lines, int functionLineIndex)
{
static const QRegularExpression classRegex(
R"((?:class|interface|trait|enum)\s+([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*))",
QRegularExpression::CaseInsensitiveOption
);
// Rückwärts suchen — letztes class/interface/trait vor dieser Zeile
for (int i = functionLineIndex - 1; i >= 0; --i)
{
QRegularExpressionMatch match = classRegex.match(lines[i]);
if (match.hasMatch())
{
return match.captured(1);
}
}
return QString(); // Globale Funktion
}

View File

@@ -0,0 +1,47 @@
#pragma once
#include <QObject>
#include <QString>
#include <QStringList>
#include <QList>
// ---------------------------------------------------------------------------
// FunctionScanner Scannt PHP-Dateien nach Funktionsdefinitionen.
//
// Liefert für jede gefundene Funktion:
// - Name
// - Parameter (vollständige Signatur)
// - Dateipfad
// - Zeilennummer
//
// Wird von FunctionListPanel (Anzeige) und DeadCodeAnalyzer (Stufe 1)
// gemeinsam genutzt.
// ---------------------------------------------------------------------------
class FunctionScanner : public QObject
{
Q_OBJECT
public:
struct FunctionInfo
{
QString name; // Funktionsname
QString parameters; // Parameter wie definiert, z.B. "$id, $name = null"
QString signature; // name(parameters) — fertig formatiert
QString filePath; // Absoluter Pfad zur Datei
int line = 0; // Zeilennummer der Definition
QString className; // Klassen- oder Namespace-Kontext, leer = global
};
explicit FunctionScanner(QObject *parent = nullptr);
// Synchroner Scan — für direkten Aufruf aus Threads
QList<FunctionInfo> scanDirectory(const QString &rootPath,
const QStringList &extensions = {"*.php"}) const;
// Scannt eine einzelne Datei
QList<FunctionInfo> scanFile(const QString &filePath) const;
private:
static QString extractParameters(const QString &line, int parenPos);
static QString detectClassContext(const QStringList &lines, int functionLineIndex);
};

View File

@@ -0,0 +1,18 @@
#include "LineNumberArea.h"
#include "CodeEditor.h"
LineNumberArea::LineNumberArea(CodeEditor *editor)
: QWidget(editor)
, m_codeEditor(editor)
{
}
QSize LineNumberArea::sizeHint() const
{
return QSize(m_codeEditor->lineNumberAreaWidth(), 0);
}
void LineNumberArea::paintEvent(QPaintEvent *event)
{
m_codeEditor->lineNumberAreaPaintEvent(event);
}

View File

@@ -0,0 +1,25 @@
#pragma once
#include <QWidget>
class CodeEditor;
// ---------------------------------------------------------------------------
// LineNumberArea Thin widget painted on the left side of the CodeEditor.
// Painted by CodeEditor::lineNumberAreaPaintEvent().
// ---------------------------------------------------------------------------
class LineNumberArea : public QWidget
{
Q_OBJECT
public:
explicit LineNumberArea(CodeEditor *editor);
QSize sizeHint() const override;
protected:
void paintEvent(QPaintEvent *event) override;
private:
CodeEditor *m_codeEditor;
};

View File

@@ -0,0 +1,492 @@
#include "SearchPanel.h"
#include "CodeEditor.h"
#include <QTextCursor>
#include <QTextBlock>
#include <QRegularExpression>
#include <QMessageBox>
#include <QKeyEvent>
#include <QShortcut>
SearchPanel::SearchPanel(QWidget *parent)
: QWidget(parent)
{
setupUi();
hide();
}
// ---------------------------------------------------------------------------
// UI
// ---------------------------------------------------------------------------
void SearchPanel::setupUi()
{
m_grid = new QGridLayout(this);
m_grid->setContentsMargins(6, 4, 6, 4);
m_grid->setSpacing(4);
// ---- Row 0: Suchen ----
m_searchEdit = new QLineEdit(this);
m_searchEdit->setPlaceholderText(tr("Suchen…"));
m_searchEdit->setClearButtonEnabled(true);
m_btnPrev = new QPushButton(tr(""), this);
m_btnNext = new QPushButton(tr(""), this);
m_btnPrev->setFixedWidth(28);
m_btnNext->setFixedWidth(28);
m_btnPrev->setToolTip(tr("Vorheriger Treffer (Shift+F3)"));
m_btnNext->setToolTip(tr("Nächster Treffer (F3)"));
m_matchLabel = new QLabel(this);
m_matchLabel->setMinimumWidth(80);
m_btnClose = new QPushButton(tr(""), this);
m_btnClose->setFixedWidth(24);
m_btnClose->setToolTip(tr("Schließen (Esc)"));
m_btnClose->setFlat(true);
QHBoxLayout *searchRow = new QHBoxLayout();
searchRow->addWidget(new QLabel(tr("Suchen:"), this));
searchRow->addWidget(m_searchEdit, 1);
searchRow->addWidget(m_btnPrev);
searchRow->addWidget(m_btnNext);
searchRow->addWidget(m_matchLabel);
searchRow->addWidget(m_btnClose);
m_grid->addLayout(searchRow, 0, 0);
// ---- Row 1: Ersetzen ----
m_replaceEdit = new QLineEdit(this);
m_replaceEdit->setPlaceholderText(tr("Ersetzen durch…"));
m_replaceEdit->setClearButtonEnabled(true);
m_btnReplace = new QPushButton(tr("Ersetzen"), this);
m_btnReplaceAll = new QPushButton(tr("Alle ersetzen"), this);
m_btnReplaceSelection = new QPushButton(tr("In Auswahl ersetzen"), this);
QHBoxLayout *replaceRow = new QHBoxLayout();
replaceRow->addWidget(new QLabel(tr("Ersetzen:"), this));
replaceRow->addWidget(m_replaceEdit, 1);
replaceRow->addWidget(m_btnReplace);
replaceRow->addWidget(m_btnReplaceAll);
replaceRow->addWidget(m_btnReplaceSelection);
m_grid->addLayout(replaceRow, 1, 0);
// ---- Row 2: Optionen ----
m_chkCase = new QCheckBox(tr("Groß-/Kleinschreibung"), this);
m_chkWord = new QCheckBox(tr("Ganzes Wort"), this);
m_chkRegex = new QCheckBox(tr("Regulärer Ausdruck"), this);
QHBoxLayout *optRow = new QHBoxLayout();
optRow->addWidget(m_chkCase);
optRow->addWidget(m_chkWord);
optRow->addWidget(m_chkRegex);
optRow->addStretch();
m_grid->addLayout(optRow, 2, 0);
// ---- Connections ----
connect(m_searchEdit, &QLineEdit::textChanged,
this, &SearchPanel::onSearchTextChanged);
connect(m_searchEdit, &QLineEdit::returnPressed,
this, &SearchPanel::findNext);
connect(m_btnNext, &QPushButton::clicked, this, &SearchPanel::findNext);
connect(m_btnPrev, &QPushButton::clicked, this, &SearchPanel::findPrevious);
connect(m_btnReplace, &QPushButton::clicked, this, &SearchPanel::replaceCurrent);
connect(m_btnReplaceAll, &QPushButton::clicked, this, &SearchPanel::replaceAll);
connect(m_btnReplaceSelection, &QPushButton::clicked, this, &SearchPanel::replaceInSelection);
connect(m_btnClose, &QPushButton::clicked, this, &SearchPanel::onCloseClicked);
connect(m_chkCase, &QCheckBox::toggled, this, &SearchPanel::onOptionChanged);
connect(m_chkWord, &QCheckBox::toggled, this, &SearchPanel::onOptionChanged);
connect(m_chkRegex, &QCheckBox::toggled, this, &SearchPanel::onOptionChanged);
}
// ---------------------------------------------------------------------------
// Public interface
// ---------------------------------------------------------------------------
void SearchPanel::setEditor(CodeEditor *editor)
{
clearHighlights();
m_editor = editor;
}
void SearchPanel::activate()
{
show();
m_searchEdit->setFocus();
m_searchEdit->selectAll();
// Pre-fill with selected text if short enough
if (m_editor)
{
const QString sel = m_editor->textCursor().selectedText();
if (!sel.isEmpty() && !sel.contains('\n') && sel.length() < 200)
{
m_searchEdit->setText(sel);
}
}
updateMatchLabel();
}
// ---------------------------------------------------------------------------
// Find helpers
// ---------------------------------------------------------------------------
QTextDocument::FindFlags SearchPanel::buildFindFlags(bool backwards) const
{
QTextDocument::FindFlags flags;
if (backwards) { flags |= QTextDocument::FindBackward; }
if (m_chkCase->isChecked()) { flags |= QTextDocument::FindCaseSensitively; }
if (m_chkWord->isChecked()) { flags |= QTextDocument::FindWholeWords; }
return flags;
}
bool SearchPanel::performFind(bool backwards)
{
if (!m_editor || m_searchEdit->text().isEmpty())
{
return false;
}
const QTextDocument::FindFlags flags = buildFindFlags(backwards);
bool found = false;
if (m_chkRegex->isChecked())
{
QRegularExpression re(m_searchEdit->text());
if (m_chkCase->isChecked())
{
re.setPatternOptions(QRegularExpression::NoPatternOption);
}
else
{
re.setPatternOptions(QRegularExpression::CaseInsensitiveOption);
}
found = m_editor->find(re, flags);
// Wrap around
if (!found)
{
QTextCursor c = m_editor->textCursor();
c.movePosition(backwards ? QTextCursor::End : QTextCursor::Start);
m_editor->setTextCursor(c);
found = m_editor->find(re, flags);
}
}
else
{
found = m_editor->find(m_searchEdit->text(), flags);
// Wrap around
if (!found)
{
QTextCursor c = m_editor->textCursor();
c.movePosition(backwards ? QTextCursor::End : QTextCursor::Start);
m_editor->setTextCursor(c);
found = m_editor->find(m_searchEdit->text(), flags);
}
}
return found;
}
void SearchPanel::highlightAllMatches()
{
if (!m_editor)
{
return;
}
QList<QTextEdit::ExtraSelection> extras;
const QString needle = m_searchEdit->text();
if (needle.isEmpty())
{
m_editor->setExtraSelections(extras);
return;
}
QTextCharFormat fmt;
fmt.setBackground(QColor("#3a3a00"));
fmt.setForeground(QColor("#ffff80"));
QTextDocument *doc = m_editor->document();
QTextCursor cursor(doc);
const QTextDocument::FindFlags flags = buildFindFlags(false);
while (true)
{
if (m_chkRegex->isChecked())
{
QRegularExpression re(needle);
if (!m_chkCase->isChecked())
{
re.setPatternOptions(QRegularExpression::CaseInsensitiveOption);
}
cursor = doc->find(re, cursor, flags);
}
else
{
cursor = doc->find(needle, cursor, flags);
}
if (cursor.isNull())
{
break;
}
QTextEdit::ExtraSelection sel;
sel.cursor = cursor;
sel.format = fmt;
extras.append(sel);
}
m_editor->setExtraSelections(extras);
}
void SearchPanel::clearHighlights()
{
if (m_editor)
{
m_editor->setExtraSelections({});
}
}
void SearchPanel::updateMatchLabel()
{
if (!m_editor || m_searchEdit->text().isEmpty())
{
m_matchLabel->setText(QString());
return;
}
// Count total matches
int count = 0;
QTextDocument *doc = m_editor->document();
QTextCursor cursor(doc);
const QTextDocument::FindFlags flags = buildFindFlags(false);
const QString needle = m_searchEdit->text();
while (true)
{
if (m_chkRegex->isChecked())
{
QRegularExpression re(needle);
if (!m_chkCase->isChecked())
{
re.setPatternOptions(QRegularExpression::CaseInsensitiveOption);
}
cursor = doc->find(re, cursor, flags);
}
else
{
cursor = doc->find(needle, cursor, flags);
}
if (cursor.isNull())
{
break;
}
++count;
}
if (count == 0)
{
m_matchLabel->setText(tr("Kein Treffer"));
m_matchLabel->setStyleSheet("color: #cc4444;");
}
else
{
m_matchLabel->setText(tr("%1 Treffer").arg(count));
m_matchLabel->setStyleSheet(QString());
}
}
// ---------------------------------------------------------------------------
// Public slots
// ---------------------------------------------------------------------------
void SearchPanel::findNext()
{
performFind(false);
}
void SearchPanel::findPrevious()
{
performFind(true);
}
void SearchPanel::replaceCurrent()
{
if (!m_editor)
{
return;
}
QTextCursor cursor = m_editor->textCursor();
// If current selection matches the search term, replace it
// Otherwise just find the next occurrence first
const bool hasMatch = !cursor.selectedText().isEmpty();
if (!hasMatch)
{
performFind(false);
return;
}
cursor.insertText(m_replaceEdit->text());
// Move to next match
performFind(false);
updateMatchLabel();
highlightAllMatches();
}
void SearchPanel::replaceAll()
{
if (!m_editor || m_searchEdit->text().isEmpty())
{
return;
}
QTextDocument *doc = m_editor->document();
QTextCursor cursor(doc);
cursor.beginEditBlock();
int count = 0;
const QTextDocument::FindFlags flags = buildFindFlags(false);
const QString needle = m_searchEdit->text();
const QString replacement = m_replaceEdit->text();
while (true)
{
if (m_chkRegex->isChecked())
{
QRegularExpression re(needle);
if (!m_chkCase->isChecked())
{
re.setPatternOptions(QRegularExpression::CaseInsensitiveOption);
}
cursor = doc->find(re, cursor, flags);
}
else
{
cursor = doc->find(needle, cursor, flags);
}
if (cursor.isNull())
{
break;
}
cursor.insertText(replacement);
++count;
}
cursor.endEditBlock();
updateMatchLabel();
clearHighlights();
QMessageBox::information(this, tr("Alle ersetzen"),
tr("%1 Ersetzung(en) durchgeführt.").arg(count));
}
void SearchPanel::replaceInSelection()
{
if (!m_editor || m_searchEdit->text().isEmpty())
{
return;
}
QTextCursor selCursor = m_editor->textCursor();
if (!selCursor.hasSelection())
{
QMessageBox::information(this, tr("In Auswahl ersetzen"),
tr("Es ist kein Text ausgewählt."));
return;
}
// Work only within the selected region
const int selStart = selCursor.selectionStart();
const int selEnd = selCursor.selectionEnd();
QTextDocument *doc = m_editor->document();
QTextCursor cursor(doc);
cursor.setPosition(selStart);
cursor.beginEditBlock();
int count = 0;
int offset = 0; // Replacement may be longer/shorter than search term
const QTextDocument::FindFlags flags = buildFindFlags(false);
const QString needle = m_searchEdit->text();
const QString replacement = m_replaceEdit->text();
while (true)
{
if (m_chkRegex->isChecked())
{
QRegularExpression re(needle);
if (!m_chkCase->isChecked())
{
re.setPatternOptions(QRegularExpression::CaseInsensitiveOption);
}
cursor = doc->find(re, cursor, flags);
}
else
{
cursor = doc->find(needle, cursor, flags);
}
if (cursor.isNull())
{
break;
}
// Stop if we've left the original selection
if (cursor.selectionEnd() > selEnd + offset)
{
break;
}
offset += replacement.length() - cursor.selectedText().length();
cursor.insertText(replacement);
++count;
}
cursor.endEditBlock();
updateMatchLabel();
clearHighlights();
QMessageBox::information(this, tr("In Auswahl ersetzen"),
tr("%1 Ersetzung(en) in der Auswahl durchgeführt.").arg(count));
}
// ---------------------------------------------------------------------------
// Private slots
// ---------------------------------------------------------------------------
void SearchPanel::onSearchTextChanged(const QString &/*text*/)
{
highlightAllMatches();
updateMatchLabel();
}
void SearchPanel::onOptionChanged()
{
highlightAllMatches();
updateMatchLabel();
}
void SearchPanel::onCloseClicked()
{
clearHighlights();
m_matchLabel->setText(QString());
hide();
if (m_editor)
{
m_editor->setFocus();
}
}

View File

@@ -0,0 +1,79 @@
#pragma once
#include <QWidget>
#include <QLineEdit>
#include <QPushButton>
#include <QCheckBox>
#include <QLabel>
#include <QGridLayout>
#include <QHBoxLayout>
#include <QTextDocument>
class CodeEditor;
// ---------------------------------------------------------------------------
// SearchPanel Collapsible find/replace bar that operates on a CodeEditor.
//
// Capabilities:
// • Nächsten / Vorherigen Treffer suchen
// • Einzeln ersetzen
// • Alle ersetzen
// • Nur in Auswahl ersetzen
// • Optionen: Groß-/Kleinschreibung, Ganzes Wort, Reguläre Ausdrücke
// ---------------------------------------------------------------------------
class SearchPanel : public QWidget
{
Q_OBJECT
public:
explicit SearchPanel(QWidget *parent = nullptr);
// Must be called whenever the active editor changes
void setEditor(CodeEditor *editor);
// Toggle visibility and focus the search field
void activate();
public slots:
void findNext();
void findPrevious();
void replaceCurrent();
void replaceAll();
void replaceInSelection();
private slots:
void onSearchTextChanged(const QString &text);
void onOptionChanged(); // Für Checkbox-Signale (bool-Parameter wird ignoriert)
void onCloseClicked();
private:
void setupUi();
QTextDocument::FindFlags buildFindFlags(bool backwards = false) const;
bool performFind(bool backwards = false);
void highlightAllMatches();
void clearHighlights();
void updateMatchLabel();
CodeEditor *m_editor = nullptr;
// Search row
QLineEdit *m_searchEdit = nullptr;
QPushButton *m_btnPrev = nullptr;
QPushButton *m_btnNext = nullptr;
QLabel *m_matchLabel = nullptr;
QPushButton *m_btnClose = nullptr;
// Replace row
QLineEdit *m_replaceEdit = nullptr;
QPushButton *m_btnReplace = nullptr;
QPushButton *m_btnReplaceAll = nullptr;
QPushButton *m_btnReplaceSelection = nullptr;
// Options row
QCheckBox *m_chkCase = nullptr;
QCheckBox *m_chkWord = nullptr;
QCheckBox *m_chkRegex = nullptr;
QGridLayout *m_grid = nullptr;
};

View File

@@ -0,0 +1,222 @@
#include "SignatureHelper.h"
#include "SignatureTooltip.h"
#include "CodeEditor.h"
#include <QTextCursor>
#include <QTextBlock>
#include <QFile>
#include <QJsonDocument>
#include <QJsonArray>
#include <QJsonObject>
#include <QRect>
#include <QRegularExpression>
SignatureHelper::SignatureHelper(CodeEditor *editor)
: QObject(editor)
, m_editor(editor)
{
// Tooltip als Kind des Viewports — bleibt im Fenster
m_tooltip = new SignatureTooltip(editor->window());
loadDatabase();
connect(m_editor, &CodeEditor::cursorPositionChanged,
this, &SignatureHelper::onCursorPositionChanged);
}
// ---------------------------------------------------------------------------
// Datenbank laden
// ---------------------------------------------------------------------------
void SignatureHelper::loadDatabase()
{
QFile f(":/php_functions.json");
if (!f.open(QIODevice::ReadOnly))
{
return;
}
const QJsonDocument doc = QJsonDocument::fromJson(f.readAll());
if (!doc.isArray())
{
return;
}
for (const QJsonValue &val : doc.array())
{
const QJsonObject obj = val.toObject();
const QString name = obj["name"].toString();
if (name.isEmpty())
{
continue;
}
FunctionInfo info;
info.signature = obj["signature"].toString();
info.description = obj["desc"].toString();
m_functions.insert(name.toLower(), info);
}
}
// ---------------------------------------------------------------------------
// Cursor-Bewegung auswerten
// ---------------------------------------------------------------------------
void SignatureHelper::onCursorPositionChanged()
{
const QTextCursor cursor = m_editor->textCursor();
const QString block = cursor.block().text();
const int col = cursor.columnNumber();
const QString leftText = block.left(col);
const QString funcName = extractFunctionName(leftText);
if (funcName.isEmpty())
{
m_tooltip->hide();
return;
}
// Klammern zählen — wenn alle geschlossen, Tooltip ausblenden
if (countOpenParens(leftText) <= 0)
{
m_tooltip->hide();
return;
}
const QString key = funcName.toLower();
if (!m_functions.contains(key))
{
m_tooltip->hide();
return;
}
const FunctionInfo &info = m_functions[key];
// Position unter dem Cursor berechnen
const QRect cursorRect = m_editor->cursorRect(cursor);
const QPoint globalPos = m_editor->viewport()->mapToGlobal(
QPoint(cursorRect.left(), cursorRect.bottom() + 4)
);
m_tooltip->showSignature(info.signature, info.description, globalPos);
}
// ---------------------------------------------------------------------------
// Funktionsnamen links vor der öffnenden Klammer extrahieren
// ---------------------------------------------------------------------------
QString SignatureHelper::extractFunctionName(const QString &text) const
{
// Wir suchen das letzte '(' das zu einem Funktionsnamen gehört.
// Dabei müssen wir verschachtelte Klammern korrekt behandeln.
int depth = 0;
int openPos = -1;
for (int i = text.length() - 1; i >= 0; --i)
{
const QChar ch = text[i];
if (ch == ')')
{
++depth;
}
else if (ch == '(')
{
if (depth == 0)
{
openPos = i;
break;
}
--depth;
}
}
if (openPos <= 0)
{
return QString();
}
// Funktionsnamen direkt links von '(' lesen
int end = openPos - 1;
// Leerzeichen überspringen
while (end >= 0 && text[end].isSpace())
{
--end;
}
if (end < 0)
{
return QString();
}
// Bezeichner-Zeichen sammeln (Buchstaben, Ziffern, _, :, \)
int start = end;
while (start > 0 &&
(text[start - 1].isLetterOrNumber() ||
text[start - 1] == '_' ||
text[start - 1] == ':' ||
text[start - 1] == '\\'))
{
--start;
}
const QString name = text.mid(start, end - start + 1);
// Schlüsselwörter und leere Namen ausschließen
static const QStringList keywords = {
"if", "else", "elseif", "while", "for", "foreach",
"switch", "match", "catch", "function", "fn"
};
if (name.isEmpty() || keywords.contains(name.toLower()))
{
return QString();
}
// Nur den letzten Teil nach :: oder -> nehmen
const int colonPos = name.lastIndexOf("::");
const int arrowPos = name.lastIndexOf("->");
const int backslashPos = name.lastIndexOf("\\");
const int splitPos = qMax(backslashPos, qMax(colonPos, arrowPos));
if (splitPos >= 0)
{
return name.mid(splitPos + (name[splitPos] == ':' ? 2 : (name[splitPos] == '\\' ? 1 : 2)));
}
return name;
}
// ---------------------------------------------------------------------------
// Offene Klammern zählen
// ---------------------------------------------------------------------------
int SignatureHelper::countOpenParens(const QString &text) const
{
int depth = 0;
bool inString = false;
QChar stringChar;
for (int i = 0; i < text.length(); ++i)
{
const QChar ch = text[i];
// Einfache String-Erkennung (kein vollständiger PHP-Parser)
if (!inString && (ch == '\'' || ch == '"'))
{
inString = true;
stringChar = ch;
continue;
}
if (inString)
{
if (ch == stringChar && (i == 0 || text[i - 1] != '\\'))
{
inString = false;
}
continue;
}
if (ch == '(') { ++depth; }
else if (ch == ')') { --depth; }
}
return depth;
}

View File

@@ -0,0 +1,51 @@
#pragma once
#include <QObject>
#include <QString>
#include <QHash>
class CodeEditor;
class SignatureTooltip;
// ---------------------------------------------------------------------------
// SignatureHelper Lädt die PHP-Funktionsdatenbank und zeigt beim Tippen
// automatisch die passende Funktionssignatur als Tooltip.
//
// Logik:
// • Bei jedem Tastendruck: Text links vom Cursor analysieren
// • Wenn "funktionsname(" erkannt wird → Tooltip anzeigen
// • Wenn ")" die öffnende Klammer schließt → Tooltip verstecken
// • Wenn Cursor sich weg bewegt → Tooltip verstecken
// ---------------------------------------------------------------------------
class SignatureHelper : public QObject
{
Q_OBJECT
public:
explicit SignatureHelper(CodeEditor *editor);
private slots:
void onCursorPositionChanged();
private:
struct FunctionInfo
{
QString signature;
QString description;
};
void loadDatabase();
void loadProjectFunctions();
// Extrahiert den Funktionsnamen direkt links vor dem letzten '('
// Gibt leeren String zurück wenn kein Kontext gefunden
QString extractFunctionName(const QString &textUpToCursor) const;
// Zählt offene Klammern — bei 0 ist der Aufruf abgeschlossen
int countOpenParens(const QString &textUpToCursor) const;
CodeEditor *m_editor = nullptr;
SignatureTooltip *m_tooltip = nullptr;
QHash<QString, FunctionInfo> m_functions; // name → info
};

View File

@@ -0,0 +1,91 @@
#include "SignatureTooltip.h"
#include <QApplication>
#include <QScreen>
SignatureTooltip::SignatureTooltip(QWidget *parent)
: QFrame(parent, Qt::ToolTip | Qt::FramelessWindowHint)
{
setFrameShape(QFrame::StyledPanel);
setFrameShadow(QFrame::Raised);
setAttribute(Qt::WA_ShowWithoutActivating);
// Dezentes Styling passend zu Hell- und Dunkeltheme
setStyleSheet(
"SignatureTooltip {"
" background: palette(toolTipBase);"
" border: 1px solid palette(mid);"
" border-radius: 4px;"
" padding: 4px;"
"}"
);
m_layout = new QVBoxLayout(this);
m_layout->setContentsMargins(8, 6, 8, 6);
m_layout->setSpacing(3);
// Signatur — Monospace, deutlich hervorgehoben
m_sigLabel = new QLabel(this);
m_sigLabel->setTextFormat(Qt::PlainText);
m_sigLabel->setWordWrap(false);
QFont sigFont = m_sigLabel->font();
sigFont.setFamily("Monospace");
sigFont.setStyleHint(QFont::Monospace);
sigFont.setPointSize(sigFont.pointSize());
m_sigLabel->setFont(sigFont);
m_sigLabel->setStyleSheet("color: palette(toolTipText); font-weight: bold;");
m_layout->addWidget(m_sigLabel);
// Beschreibung — kleiner, gedimmt
m_descLabel = new QLabel(this);
m_descLabel->setTextFormat(Qt::PlainText);
m_descLabel->setWordWrap(false);
m_descLabel->setStyleSheet("color: palette(mid);");
QFont descFont = m_descLabel->font();
descFont.setPointSize(qMax(descFont.pointSize() - 1, 8));
m_descLabel->setFont(descFont);
m_layout->addWidget(m_descLabel);
hide();
}
void SignatureTooltip::showSignature(const QString &signature,
const QString &description,
const QPoint &globalPos)
{
m_sigLabel->setText(signature);
if (description.isEmpty())
{
m_descLabel->hide();
}
else
{
m_descLabel->setText(description);
m_descLabel->show();
}
adjustSize();
// Position so wählen dass das Popup nicht aus dem Bildschirm ragt
QPoint pos = globalPos;
const QRect screen = QApplication::primaryScreen()->availableGeometry();
if (pos.x() + width() > screen.right())
{
pos.setX(screen.right() - width() - 4);
}
if (pos.y() + height() > screen.bottom())
{
pos.setY(globalPos.y() - height() - 24);
}
move(pos);
show();
raise();
}
void SignatureTooltip::hide()
{
QFrame::hide();
}

View File

@@ -0,0 +1,29 @@
#pragma once
#include <QFrame>
#include <QLabel>
#include <QString>
#include <QVBoxLayout>
// ---------------------------------------------------------------------------
// SignatureTooltip Schwebendes Popup das die Signatur einer Funktion zeigt.
// Erscheint unter dem Cursor, verschwindet automatisch wenn der Nutzer
// die Klammer schließt oder den Kontext verlässt.
// ---------------------------------------------------------------------------
class SignatureTooltip : public QFrame
{
Q_OBJECT
public:
explicit SignatureTooltip(QWidget *parent = nullptr);
void showSignature(const QString &signature,
const QString &description,
const QPoint &globalPos);
void hide();
private:
QVBoxLayout *m_layout = nullptr;
QLabel *m_sigLabel = nullptr;
QLabel *m_descLabel = nullptr;
};

View File

@@ -0,0 +1,256 @@
#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 reines Anzeige-Fenster — dieselbe Fensterart wie
// SignatureTooltip, die nachweislich nie den Fokus an sich reißt.
// Wichtig: Ein echtes Elternfenster (editor->window()) angeben, sonst
// behandelt der Fenstermanager das Popup als eigenständiges Fenster.
// Das führte dazu, dass bei mehreren offenen Dateien/Tabs verwaiste
// Popups entstehen konnten, die den Fokus übernehmen und die
// Texteingabe blockieren.
m_popup = new QListWidget(editor->window());
m_popup->setWindowFlags(Qt::ToolTip | Qt::FramelessWindowHint);
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();
}
// ---------------------------------------------------------------------------
// Editor hat den Fokus verloren (z. B. Tab-Wechsel) — Popup schließen
// ---------------------------------------------------------------------------
void VariableCompleter::notifyFocusLost()
{
hidePopup();
}
// ---------------------------------------------------------------------------
// 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,43 @@
#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);
// Muss aufgerufen werden, wenn der Editor den Fokus verliert
// (z. B. beim Wechsel des Tabs) — schließt ein evtl. offenes Popup,
// damit es nicht als "Geisterfenster" stehen bleibt.
void notifyFocusLost();
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;
};