- 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}
|
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
install(FILES LICENSE
|
||||||
|
DESTINATION ${CMAKE_INSTALL_DATADIR}/licenses/barecode
|
||||||
|
)
|
||||||
|
|
||||||
# ---- Linux (FreeDesktop) --------------------------------------------------
|
# ---- Linux (FreeDesktop) --------------------------------------------------
|
||||||
if(PLATFORM_LINUX)
|
if(PLATFORM_LINUX)
|
||||||
foreach(SIZE 16 32 48 64 128 256 512)
|
foreach(SIZE 16 32 48 64 128 256 512)
|
||||||
|
|||||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
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
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
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 = mBearbeiten->addAction(tr("In &Dateien suchen…"), this, &MainWindow::onShowFileSearch);
|
||||||
m_actFileSearch->setShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_F);
|
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 ----
|
// ---- Ansicht ----
|
||||||
QMenu *mAnsicht = menuBar()->addMenu(tr("&Ansicht"));
|
QMenu *mAnsicht = menuBar()->addMenu(tr("&Ansicht"));
|
||||||
|
|
||||||
@@ -281,6 +289,16 @@ void MainWindow::onAbout()
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Slots – Projekt
|
// Slots – Projekt
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
void MainWindow::onShowDeadCode()
|
||||||
|
{
|
||||||
|
m_editor->showDeadCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::onShowFunctionList()
|
||||||
|
{
|
||||||
|
m_editor->showFunctionList();
|
||||||
|
}
|
||||||
|
|
||||||
void MainWindow::onShowFileSearch()
|
void MainWindow::onShowFileSearch()
|
||||||
{
|
{
|
||||||
m_editor->showFileSearchPanel();
|
m_editor->showFileSearchPanel();
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ private slots:
|
|||||||
void onRedo();
|
void onRedo();
|
||||||
void onShowSearch();
|
void onShowSearch();
|
||||||
void onShowFileSearch();
|
void onShowFileSearch();
|
||||||
|
void onShowFunctionList();
|
||||||
|
void onShowDeadCode();
|
||||||
// Ansicht
|
// Ansicht
|
||||||
void onToggleDarkMode(bool checked);
|
void onToggleDarkMode(bool checked);
|
||||||
// Hilfe
|
// Hilfe
|
||||||
@@ -82,6 +84,8 @@ private:
|
|||||||
QAction *m_actRedo = nullptr;
|
QAction *m_actRedo = nullptr;
|
||||||
QAction *m_actSearch = nullptr;
|
QAction *m_actSearch = nullptr;
|
||||||
QAction *m_actFileSearch = nullptr;
|
QAction *m_actFileSearch = nullptr;
|
||||||
|
QAction *m_actFuncList = nullptr;
|
||||||
|
QAction *m_actDeadCode = nullptr;
|
||||||
QAction *m_actDarkMode = nullptr;
|
QAction *m_actDarkMode = nullptr;
|
||||||
QAction *m_actAbout = nullptr;
|
QAction *m_actAbout = nullptr;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,6 +17,18 @@ set(EDITOR_SOURCES
|
|||||||
SignatureHelper.h
|
SignatureHelper.h
|
||||||
SignatureTooltip.cpp
|
SignatureTooltip.cpp
|
||||||
SignatureTooltip.h
|
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})
|
add_library(BareCode_Editor STATIC ${EDITOR_SOURCES})
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
#include "LineNumberArea.h"
|
#include "LineNumberArea.h"
|
||||||
#include "ColorIndicator.h"
|
#include "ColorIndicator.h"
|
||||||
#include "SignatureHelper.h"
|
#include "SignatureHelper.h"
|
||||||
|
#include "FunctionIndex.h"
|
||||||
|
|
||||||
#include "core/Settings.h"
|
#include "core/Settings.h"
|
||||||
#include "highlighter/HighlighterFactory.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)
|
void CodeEditor::mousePressEvent(QMouseEvent *event)
|
||||||
{
|
{
|
||||||
// Zuerst prüfen ob ein Farbquadrat geklickt wurde
|
// Zuerst prüfen ob ein Farbquadrat geklickt wurde
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ class Settings;
|
|||||||
class SyntaxHighlighter;
|
class SyntaxHighlighter;
|
||||||
class ColorIndicator;
|
class ColorIndicator;
|
||||||
class SignatureHelper;
|
class SignatureHelper;
|
||||||
|
class FunctionIndex;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// CodeEditor – Core editing widget.
|
// CodeEditor – Core editing widget.
|
||||||
@@ -48,14 +49,18 @@ public:
|
|||||||
QRectF blockBoundingGeometryPublic(const QTextBlock &b) const { return blockBoundingGeometry(b); }
|
QRectF blockBoundingGeometryPublic(const QTextBlock &b) const { return blockBoundingGeometry(b); }
|
||||||
QPointF contentOffsetPublic() const { return contentOffset(); }
|
QPointF contentOffsetPublic() const { return contentOffset(); }
|
||||||
|
|
||||||
|
void setFunctionIndex(FunctionIndex *index);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void fileSaved(const QString &filePath);
|
void fileSaved(const QString &filePath);
|
||||||
|
void navigateToRequested(const QString &filePath, int line);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void resizeEvent(QResizeEvent *event) override;
|
void resizeEvent(QResizeEvent *event) override;
|
||||||
void keyPressEvent(QKeyEvent *event) override;
|
void keyPressEvent(QKeyEvent *event) override;
|
||||||
void paintEvent(QPaintEvent *event) override;
|
void paintEvent(QPaintEvent *event) override;
|
||||||
void mousePressEvent(QMouseEvent *event) override;
|
void mousePressEvent(QMouseEvent *event) override;
|
||||||
|
void mouseDoubleClickEvent(QMouseEvent *event) override;
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void updateLineNumberAreaWidth(int newBlockCount);
|
void updateLineNumberAreaWidth(int newBlockCount);
|
||||||
@@ -72,5 +77,6 @@ private:
|
|||||||
SyntaxHighlighter *m_highlighter = nullptr;
|
SyntaxHighlighter *m_highlighter = nullptr;
|
||||||
ColorIndicator *m_colorIndicator = nullptr;
|
ColorIndicator *m_colorIndicator = nullptr;
|
||||||
SignatureHelper *m_signatureHelper = nullptr;
|
SignatureHelper *m_signatureHelper = nullptr;
|
||||||
|
FunctionIndex *m_functionIndex = nullptr;
|
||||||
QString m_filePath;
|
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 "CodeEditor.h"
|
||||||
#include "SearchPanel.h"
|
#include "SearchPanel.h"
|
||||||
#include "FileSearchPanel.h"
|
#include "FileSearchPanel.h"
|
||||||
|
#include "FunctionListDialog.h"
|
||||||
|
#include "DeadCodeDialog.h"
|
||||||
|
#include "FunctionIndex.h"
|
||||||
|
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
@@ -32,6 +35,9 @@ void EditorPanel::setupUi()
|
|||||||
|
|
||||||
m_searchPanel = new SearchPanel(this);
|
m_searchPanel = new SearchPanel(this);
|
||||||
m_fileSearch = new FileSearchPanel(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_tabWidget, 1);
|
||||||
m_layout->addWidget(m_searchPanel, 0);
|
m_layout->addWidget(m_searchPanel, 0);
|
||||||
@@ -45,6 +51,12 @@ void EditorPanel::setupUi()
|
|||||||
|
|
||||||
connect(m_fileSearch, &FileSearchPanel::fileLineRequested,
|
connect(m_fileSearch, &FileSearchPanel::fileLineRequested,
|
||||||
this, &EditorPanel::goToLine);
|
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_tabWidget->setTabToolTip(index, filePath);
|
||||||
m_openTabs.insert(filePath, tab);
|
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)
|
// Änderungsindikator im Tab-Titel (● = ungespeichert)
|
||||||
connect(tab->editor()->document(), &QTextDocument::modificationChanged,
|
connect(tab->editor()->document(), &QTextDocument::modificationChanged,
|
||||||
this, [this, tab](bool modified)
|
this, [this, tab](bool modified)
|
||||||
@@ -102,6 +121,13 @@ void EditorPanel::openFile(const QString &filePath)
|
|||||||
m_tabWidget->setTabToolTip(idx, savedPath);
|
m_tabWidget->setTabToolTip(idx, savedPath);
|
||||||
}
|
}
|
||||||
emit currentFileSaved(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();
|
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)
|
void EditorPanel::setSearchRoot(const QString &path)
|
||||||
{
|
{
|
||||||
m_fileSearch->setSearchRoot(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)
|
void EditorPanel::goToLine(const QString &filePath, int line)
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ class EditorTab;
|
|||||||
class Settings;
|
class Settings;
|
||||||
class SearchPanel;
|
class SearchPanel;
|
||||||
class FileSearchPanel;
|
class FileSearchPanel;
|
||||||
|
class FunctionListDialog;
|
||||||
|
class DeadCodeDialog;
|
||||||
|
class FunctionIndex;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// EditorPanel – Rechtes Panel: Tab-Leiste + Editoren + Such/Ersetzen-Panel.
|
// EditorPanel – Rechtes Panel: Tab-Leiste + Editoren + Such/Ersetzen-Panel.
|
||||||
@@ -29,6 +32,8 @@ public slots:
|
|||||||
void setSearchRoot(const QString &path);
|
void setSearchRoot(const QString &path);
|
||||||
void showSearchPanel();
|
void showSearchPanel();
|
||||||
void showFileSearchPanel();
|
void showFileSearchPanel();
|
||||||
|
void showFunctionList();
|
||||||
|
void showDeadCode();
|
||||||
void goToLine(const QString &filePath, int line);
|
void goToLine(const QString &filePath, int line);
|
||||||
|
|
||||||
// Session
|
// Session
|
||||||
@@ -55,6 +60,9 @@ private:
|
|||||||
QTabWidget *m_tabWidget = nullptr;
|
QTabWidget *m_tabWidget = nullptr;
|
||||||
SearchPanel *m_searchPanel = nullptr;
|
SearchPanel *m_searchPanel = nullptr;
|
||||||
FileSearchPanel *m_fileSearch = nullptr;
|
FileSearchPanel *m_fileSearch = nullptr;
|
||||||
|
FunctionListDialog *m_funcDialog = nullptr;
|
||||||
|
DeadCodeDialog *m_deadCode = nullptr;
|
||||||
|
FunctionIndex *m_funcIndex = nullptr;
|
||||||
|
|
||||||
QHash<QString, EditorTab *> m_openTabs;
|
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