- Auslisten aller im Projekt definierten Funktionen
- Suche nach toten Funktionen im Projekt - Doppelklick auf Funktion öffnet die Datei mit der Deklaration
This commit is contained in:
@@ -95,6 +95,10 @@ install(TARGETS BareCode
|
||||
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||
)
|
||||
|
||||
install(FILES LICENSE
|
||||
DESTINATION ${CMAKE_INSTALL_DATADIR}/licenses/barecode
|
||||
)
|
||||
|
||||
# ---- Linux (FreeDesktop) --------------------------------------------------
|
||||
if(PLATFORM_LINUX)
|
||||
foreach(SIZE 16 32 48 64 128 256 512)
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Dany Thinnes – Projekt Hirnfrei
|
||||
Copyright (c) 2025 Dany Thinnes / Projekt Hirnfrei
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
36
PKGBUILD
Normal file
36
PKGBUILD
Normal file
@@ -0,0 +1,36 @@
|
||||
# Maintainer: Dany Thinnes <dany@projekt-hirnfrei.de>
|
||||
# Projekt Hirnfrei - https://www.projekt-hirnfrei.de
|
||||
|
||||
pkgname=barecode
|
||||
pkgver=1.1.0
|
||||
pkgrel=1
|
||||
pkgdesc="Schlanker modularer Code-Editor für HTML, PHP, CSS und C++"
|
||||
arch=('x86_64' 'aarch64')
|
||||
url="https://www.projekt-hirnfrei.de"
|
||||
license=('MIT')
|
||||
depends=('qt6-base')
|
||||
makedepends=('cmake' 'ninja')
|
||||
provides=('barecode')
|
||||
conflicts=('barecode-git')
|
||||
source=("$pkgname-$pkgver.tar.gz::https://git.projekt-hirnfrei.de/diabolus/BareCode/archive/v$pkgver.tar.gz")
|
||||
sha256sums=('SKIP') # Nach erstem Download ersetzen: sha256sum barecode-1.1.0.tar.gz
|
||||
|
||||
build() {
|
||||
cd "BareCode"
|
||||
cmake -B build \
|
||||
-G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_INSTALL_PREFIX=/usr \
|
||||
-DCMAKE_SKIP_RPATH=ON
|
||||
cmake --build build
|
||||
}
|
||||
|
||||
check() {
|
||||
:
|
||||
}
|
||||
|
||||
package() {
|
||||
cd "BareCode"
|
||||
DESTDIR="$pkgdir" cmake --install build
|
||||
install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE"
|
||||
}
|
||||
43
PKGBUILD-git
Normal file
43
PKGBUILD-git
Normal file
@@ -0,0 +1,43 @@
|
||||
# Maintainer: Dany Thinnes <dany@projekt-hirnfrei.de>
|
||||
# Projekt Hirnfrei - https://www.projekt-hirnfrei.de
|
||||
|
||||
pkgname=barecode-git
|
||||
pkgver=1.1.0.r0.g0000000
|
||||
pkgrel=1
|
||||
pkgdesc="Schlanker modularer Code-Editor für HTML, PHP, CSS und C++ (Git-Version)"
|
||||
arch=('x86_64' 'aarch64')
|
||||
url="https://www.projekt-hirnfrei.de"
|
||||
license=('MIT')
|
||||
depends=('qt6-base')
|
||||
makedepends=('cmake' 'ninja' 'git')
|
||||
provides=('barecode')
|
||||
conflicts=('barecode')
|
||||
source=("$pkgname::git+https://git.projekt-hirnfrei.de/diabolus/BareCode.git")
|
||||
sha256sums=('SKIP')
|
||||
|
||||
pkgver() {
|
||||
cd "$pkgname"
|
||||
git describe --long --tags 2>/dev/null \
|
||||
| sed 's/^v//;s/\([^-]*-g\)/r\1/;s/-/./g' \
|
||||
|| printf "r%s.%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)"
|
||||
}
|
||||
|
||||
build() {
|
||||
cd "$pkgname"
|
||||
cmake -B build \
|
||||
-G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_INSTALL_PREFIX=/usr \
|
||||
-DCMAKE_SKIP_RPATH=ON
|
||||
cmake --build build
|
||||
}
|
||||
|
||||
check() {
|
||||
:
|
||||
}
|
||||
|
||||
package() {
|
||||
cd "$pkgname"
|
||||
DESTDIR="$pkgdir" cmake --install build
|
||||
install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE"
|
||||
}
|
||||
@@ -119,6 +119,14 @@ void MainWindow::setupMenuBar()
|
||||
m_actFileSearch = mBearbeiten->addAction(tr("In &Dateien suchen…"), this, &MainWindow::onShowFileSearch);
|
||||
m_actFileSearch->setShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_F);
|
||||
|
||||
mBearbeiten->addSeparator();
|
||||
|
||||
m_actFuncList = mBearbeiten->addAction(tr("&Projektfunktionen…"), this, &MainWindow::onShowFunctionList);
|
||||
m_actFuncList->setShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_P);
|
||||
|
||||
m_actDeadCode = mBearbeiten->addAction(tr("&Tote Funktionen suchen…"), this, &MainWindow::onShowDeadCode);
|
||||
m_actDeadCode->setShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_T);
|
||||
|
||||
// ---- Ansicht ----
|
||||
QMenu *mAnsicht = menuBar()->addMenu(tr("&Ansicht"));
|
||||
|
||||
@@ -281,6 +289,16 @@ void MainWindow::onAbout()
|
||||
// ---------------------------------------------------------------------------
|
||||
// Slots – Projekt
|
||||
// ---------------------------------------------------------------------------
|
||||
void MainWindow::onShowDeadCode()
|
||||
{
|
||||
m_editor->showDeadCode();
|
||||
}
|
||||
|
||||
void MainWindow::onShowFunctionList()
|
||||
{
|
||||
m_editor->showFunctionList();
|
||||
}
|
||||
|
||||
void MainWindow::onShowFileSearch()
|
||||
{
|
||||
m_editor->showFileSearchPanel();
|
||||
|
||||
@@ -42,6 +42,8 @@ private slots:
|
||||
void onRedo();
|
||||
void onShowSearch();
|
||||
void onShowFileSearch();
|
||||
void onShowFunctionList();
|
||||
void onShowDeadCode();
|
||||
// Ansicht
|
||||
void onToggleDarkMode(bool checked);
|
||||
// Hilfe
|
||||
@@ -82,6 +84,8 @@ private:
|
||||
QAction *m_actRedo = nullptr;
|
||||
QAction *m_actSearch = nullptr;
|
||||
QAction *m_actFileSearch = nullptr;
|
||||
QAction *m_actFuncList = nullptr;
|
||||
QAction *m_actDeadCode = nullptr;
|
||||
QAction *m_actDarkMode = nullptr;
|
||||
QAction *m_actAbout = nullptr;
|
||||
};
|
||||
|
||||
@@ -17,6 +17,18 @@ set(EDITOR_SOURCES
|
||||
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
|
||||
)
|
||||
|
||||
add_library(BareCode_Editor STATIC ${EDITOR_SOURCES})
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "LineNumberArea.h"
|
||||
#include "ColorIndicator.h"
|
||||
#include "SignatureHelper.h"
|
||||
#include "FunctionIndex.h"
|
||||
|
||||
#include "core/Settings.h"
|
||||
#include "highlighter/HighlighterFactory.h"
|
||||
@@ -289,6 +290,44 @@ void CodeEditor::paintEvent(QPaintEvent *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
|
||||
|
||||
@@ -10,6 +10,7 @@ class Settings;
|
||||
class SyntaxHighlighter;
|
||||
class ColorIndicator;
|
||||
class SignatureHelper;
|
||||
class FunctionIndex;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CodeEditor – Core editing widget.
|
||||
@@ -48,14 +49,18 @@ public:
|
||||
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;
|
||||
|
||||
private slots:
|
||||
void updateLineNumberAreaWidth(int newBlockCount);
|
||||
@@ -72,5 +77,6 @@ private:
|
||||
SyntaxHighlighter *m_highlighter = nullptr;
|
||||
ColorIndicator *m_colorIndicator = nullptr;
|
||||
SignatureHelper *m_signatureHelper = nullptr;
|
||||
FunctionIndex *m_functionIndex = nullptr;
|
||||
QString m_filePath;
|
||||
};
|
||||
|
||||
181
src/editor/DeadCodeAnalyzer.cpp
Normal file
181
src/editor/DeadCodeAnalyzer.cpp
Normal 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;
|
||||
}
|
||||
39
src/editor/DeadCodeAnalyzer.h
Normal file
39
src/editor/DeadCodeAnalyzer.h
Normal 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 ¤tFile) const;
|
||||
|
||||
private:
|
||||
FunctionScanner *m_scanner = nullptr;
|
||||
};
|
||||
461
src/editor/DeadCodeDialog.cpp
Normal file
461
src/editor/DeadCodeDialog.cpp
Normal 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 ¤tFile)
|
||||
{
|
||||
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();
|
||||
}
|
||||
62
src/editor/DeadCodeDialog.h
Normal file
62
src/editor/DeadCodeDialog.h
Normal 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 ¤tFile);
|
||||
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;
|
||||
};
|
||||
@@ -3,6 +3,9 @@
|
||||
#include "CodeEditor.h"
|
||||
#include "SearchPanel.h"
|
||||
#include "FileSearchPanel.h"
|
||||
#include "FunctionListDialog.h"
|
||||
#include "DeadCodeDialog.h"
|
||||
#include "FunctionIndex.h"
|
||||
|
||||
#include <QFileInfo>
|
||||
#include <QFile>
|
||||
@@ -32,6 +35,9 @@ void EditorPanel::setupUi()
|
||||
|
||||
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);
|
||||
@@ -45,6 +51,12 @@ void EditorPanel::setupUi()
|
||||
|
||||
connect(m_fileSearch, &FileSearchPanel::fileLineRequested,
|
||||
this, &EditorPanel::goToLine);
|
||||
|
||||
connect(m_funcDialog, &FunctionListDialog::fileLineRequested,
|
||||
this, &EditorPanel::goToLine);
|
||||
|
||||
connect(m_deadCode, &DeadCodeDialog::fileLineRequested,
|
||||
this, &EditorPanel::goToLine);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -79,6 +91,13 @@ void EditorPanel::openFile(const QString &filePath)
|
||||
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)
|
||||
@@ -102,6 +121,13 @@ void EditorPanel::openFile(const QString &filePath)
|
||||
m_tabWidget->setTabToolTip(idx, savedPath);
|
||||
}
|
||||
emit currentFileSaved(savedPath);
|
||||
|
||||
// Index und Funktionsliste aktualisieren
|
||||
m_funcIndex->refresh();
|
||||
if (m_funcDialog->isVisible())
|
||||
{
|
||||
m_funcDialog->refresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -145,9 +171,26 @@ void EditorPanel::showFileSearchPanel()
|
||||
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)
|
||||
|
||||
@@ -10,6 +10,9 @@ class EditorTab;
|
||||
class Settings;
|
||||
class SearchPanel;
|
||||
class FileSearchPanel;
|
||||
class FunctionListDialog;
|
||||
class DeadCodeDialog;
|
||||
class FunctionIndex;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// EditorPanel – Rechtes Panel: Tab-Leiste + Editoren + Such/Ersetzen-Panel.
|
||||
@@ -29,6 +32,8 @@ public slots:
|
||||
void setSearchRoot(const QString &path);
|
||||
void showSearchPanel();
|
||||
void showFileSearchPanel();
|
||||
void showFunctionList();
|
||||
void showDeadCode();
|
||||
void goToLine(const QString &filePath, int line);
|
||||
|
||||
// Session
|
||||
@@ -55,6 +60,9 @@ private:
|
||||
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;
|
||||
};
|
||||
|
||||
83
src/editor/FunctionIndex.cpp
Normal file
83
src/editor/FunctionIndex.cpp
Normal 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;
|
||||
}
|
||||
47
src/editor/FunctionIndex.h
Normal file
47
src/editor/FunctionIndex.h
Normal 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;
|
||||
};
|
||||
52
src/editor/FunctionListDialog.cpp
Normal file
52
src/editor/FunctionListDialog.cpp
Normal 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();
|
||||
}
|
||||
30
src/editor/FunctionListDialog.h
Normal file
30
src/editor/FunctionListDialog.h
Normal 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;
|
||||
};
|
||||
332
src/editor/FunctionListPanel.cpp
Normal file
332
src/editor/FunctionListPanel.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
67
src/editor/FunctionListPanel.h
Normal file
67
src/editor/FunctionListPanel.h
Normal 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;
|
||||
};
|
||||
158
src/editor/FunctionScanner.cpp
Normal file
158
src/editor/FunctionScanner.cpp
Normal 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
|
||||
}
|
||||
47
src/editor/FunctionScanner.h
Normal file
47
src/editor/FunctionScanner.h
Normal 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);
|
||||
};
|
||||
Reference in New Issue
Block a user