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