545 lines
18 KiB
C++
545 lines
18 KiB
C++
/*==============================================================================
|
|
** Copyright (C) 2024-2027 WingSummer
|
|
**
|
|
** This program is free software: you can redistribute it and/or modify it under
|
|
** the terms of the GNU Affero General Public License as published by the Free
|
|
** Software Foundation, version 3.
|
|
**
|
|
** This program is distributed in the hope that it will be useful, but WITHOUT
|
|
** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
** FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
|
** details.
|
|
**
|
|
** You should have received a copy of the GNU Affero General Public License
|
|
** along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
** =============================================================================
|
|
*/
|
|
|
|
#include "ascompletion.h"
|
|
|
|
#include "asdatabase.h"
|
|
#include "class/aspreprocesser.h"
|
|
#include "class/qascodeparser.h"
|
|
#include "class/scriptmachine.h"
|
|
#include "model/codecompletionmodel.h"
|
|
#include "wingcodeedit.h"
|
|
|
|
#include <QAbstractItemView>
|
|
#include <QApplication>
|
|
#include <QByteArray>
|
|
#include <QDir>
|
|
#include <QEvent>
|
|
#include <QLibraryInfo>
|
|
#include <QQueue>
|
|
#include <QTextStream>
|
|
#include <QTime>
|
|
#include <QTimer>
|
|
#include <QtDebug>
|
|
|
|
Q_GLOBAL_STATIC_WITH_ARGS(QByteArray, DOT_TRIGGER, ("."))
|
|
Q_GLOBAL_STATIC_WITH_ARGS(QByteArray, DBL_COLON_TRIGGER, ("::"))
|
|
Q_GLOBAL_STATIC_WITH_ARGS(QByteArray, LEFT_PARE_TRIGGER, ("("))
|
|
Q_GLOBAL_STATIC_WITH_ARGS(QByteArray, SEMI_COLON_TRIGGER, (";"))
|
|
|
|
AsCompletion::AsCompletion(WingCodeEdit *p)
|
|
: WingCompleter(p), parser(ScriptMachine::instance().engine()) {
|
|
setTriggerList({*DOT_TRIGGER, *DBL_COLON_TRIGGER,
|
|
// unleash the power of call tips
|
|
*LEFT_PARE_TRIGGER,
|
|
// clear the tips
|
|
*SEMI_COLON_TRIGGER});
|
|
setTriggerAmount(3);
|
|
|
|
connect(this, QOverload<const QModelIndex &>::of(&AsCompletion::activated),
|
|
this, [this](const QModelIndex &index) {
|
|
auto v = index.data(Qt::SelfDataRole).value<CodeInfoTip>();
|
|
if (v.type == CodeInfoTip::Type::Function ||
|
|
v.type == CodeInfoTip::Type::ClsFunction) {
|
|
emit onFunctionTip(v.getTooltip());
|
|
}
|
|
});
|
|
|
|
p->installEventFilter(this);
|
|
}
|
|
|
|
AsCompletion::~AsCompletion() {}
|
|
|
|
void AsCompletion::applyEmptyNsNode(QList<CodeInfoTip> &nodes,
|
|
const QList<CodeInfoTip> &docNodes) {
|
|
static QList<CodeInfoTip> emptyNsNodes;
|
|
|
|
if (emptyNsNodes.isEmpty()) {
|
|
auto &hn = parser.headerNodes();
|
|
for (auto p = hn.constKeyValueBegin(); p != hn.constKeyValueEnd();
|
|
++p) {
|
|
if (Q_UNLIKELY(p->first.isEmpty())) {
|
|
emptyNsNodes.append(p->second);
|
|
} else {
|
|
CodeInfoTip tip;
|
|
tip.type = CodeInfoTip::Type::Group;
|
|
tip.name = p->first;
|
|
emptyNsNodes.append(tip);
|
|
}
|
|
}
|
|
emptyNsNodes.append(parser.keywordNodes());
|
|
}
|
|
|
|
nodes.clear();
|
|
|
|
for (auto &p : docNodes) {
|
|
if (p.nameSpace.isEmpty()) {
|
|
nodes.append(p);
|
|
} else {
|
|
if (p.dontAddGlobal) {
|
|
continue;
|
|
}
|
|
|
|
CodeInfoTip tip;
|
|
tip.type = CodeInfoTip::Type::Group;
|
|
tip.name = p.nameSpace;
|
|
nodes.append(tip);
|
|
}
|
|
}
|
|
|
|
nodes.append(emptyNsNodes);
|
|
}
|
|
|
|
void AsCompletion::applyClassNodes(QList<CodeInfoTip> &nodes) {
|
|
static QList<CodeInfoTip> clsNodes;
|
|
if (clsNodes.isEmpty()) {
|
|
auto &hn = parser.headerNodes();
|
|
for (auto &n : hn) {
|
|
for (auto &item : n) {
|
|
if (item.type == CodeInfoTip::Type::Class) {
|
|
for (auto &c : item.children) {
|
|
if (c.type == CodeInfoTip::Type::Function) {
|
|
if (!c.addinfo.contains(CodeInfoTip::RetType)) {
|
|
continue;
|
|
}
|
|
}
|
|
clsNodes.append(c);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
nodes = clsNodes;
|
|
}
|
|
|
|
int AsCompletion::includeCallBack(const QString &include, bool quotedInclude,
|
|
const QString &from, AsPreprocesser *builder,
|
|
void *userParam) {
|
|
Q_UNUSED(userParam);
|
|
|
|
QFileInfo info(include);
|
|
bool isAbsolute = info.isAbsolute();
|
|
bool hasNoExt = info.suffix().isEmpty();
|
|
QString inc;
|
|
if (quotedInclude) {
|
|
if (isAbsolute) {
|
|
inc = include;
|
|
} else {
|
|
auto pwd = QFileInfo(from).absoluteDir();
|
|
inc = pwd.absoluteFilePath(include);
|
|
}
|
|
} else {
|
|
// absolute include is not allowed in #include<>
|
|
if (isAbsolute) {
|
|
// ignored in code completion
|
|
return asSUCCESS;
|
|
}
|
|
|
|
QDir dir(qApp->applicationDirPath());
|
|
if (!dir.cd(QStringLiteral("aslib"))) {
|
|
// someone crash the software, ignored
|
|
return asSUCCESS;
|
|
}
|
|
inc = dir.absoluteFilePath(include);
|
|
}
|
|
|
|
if (hasNoExt) {
|
|
inc += QStringLiteral(".as");
|
|
}
|
|
|
|
builder->loadSectionFromFile(inc);
|
|
return asSUCCESS;
|
|
}
|
|
|
|
void AsCompletion::clearFunctionTip() { emit onFunctionTip({}); }
|
|
|
|
QString AsCompletion::wordSeperators() const {
|
|
static QString eow(QStringLiteral("~!@#$%^&*()_+{}|\"<>?,/;'[]\\-="));
|
|
return eow;
|
|
}
|
|
|
|
bool AsCompletion::processTrigger(const QString &trigger,
|
|
const QString &content) {
|
|
if (content.isEmpty()) {
|
|
return false;
|
|
}
|
|
|
|
if (trigger == *SEMI_COLON_TRIGGER) {
|
|
clearFunctionTip();
|
|
return false;
|
|
}
|
|
|
|
auto len = content.length();
|
|
auto code = content.toUtf8();
|
|
|
|
QList<CodeInfoTip> nodes;
|
|
|
|
if (!trigger.isEmpty() && trigger != *DOT_TRIGGER) {
|
|
clearFunctionTip();
|
|
}
|
|
|
|
auto p = code.data();
|
|
auto end = p + len;
|
|
|
|
struct Token {
|
|
qsizetype pos;
|
|
asETokenClass type;
|
|
QByteArray content;
|
|
};
|
|
|
|
auto engine = ScriptMachine::instance().engine();
|
|
|
|
// parse the tokens
|
|
QVector<Token> tokens;
|
|
qsizetype pos = 0;
|
|
for (; p < end;) {
|
|
asUINT tokenLen = 0;
|
|
auto tt = engine->ParseToken(p, len, &tokenLen);
|
|
if (tt == asTC_WHITESPACE) {
|
|
p += tokenLen;
|
|
pos += tokenLen;
|
|
continue;
|
|
}
|
|
Token token;
|
|
token.pos = pos;
|
|
token.type = tt;
|
|
token.content = QByteArray(p, tokenLen);
|
|
tokens << token;
|
|
p += tokenLen;
|
|
pos += tokenLen;
|
|
}
|
|
|
|
auto getNamespace = [](const QVector<Token> &tokens) -> QString {
|
|
auto rbegin = tokens.rbegin();
|
|
auto rend = tokens.rend();
|
|
|
|
QStringList nss;
|
|
bool semiFlag = true;
|
|
auto p = rbegin;
|
|
for (; p != rend; ++p) {
|
|
if (semiFlag) {
|
|
if (p->type != asTC_IDENTIFIER) {
|
|
break;
|
|
} else {
|
|
nss.prepend(p->content);
|
|
semiFlag = false;
|
|
}
|
|
} else {
|
|
if (p->content != *DBL_COLON_TRIGGER) {
|
|
break;
|
|
}
|
|
semiFlag = true;
|
|
}
|
|
}
|
|
return nss.join(QStringLiteral("::"));
|
|
};
|
|
|
|
QByteArray fn;
|
|
if (tokens.isEmpty()) {
|
|
popup()->hide();
|
|
return false;
|
|
}
|
|
|
|
auto etoken = tokens.back();
|
|
// it can not be any trigger, so take the last as prefix
|
|
QString prefix = etoken.content;
|
|
if (etoken.type == asTC_VALUE || etoken.type == asTC_COMMENT ||
|
|
etoken.type == asTC_UNKNOWN) {
|
|
popup()->hide();
|
|
return false;
|
|
}
|
|
|
|
if (trigger.isEmpty() && popup()->isVisible()) {
|
|
setCompletionPrefix(prefix);
|
|
return true;
|
|
}
|
|
|
|
QList<CodeInfoTip> docNodes = parseDocument();
|
|
|
|
// if trigger is empty, it's making editing
|
|
if (trigger.isEmpty()) {
|
|
tokens.removeLast();
|
|
if (tokens.isEmpty()) {
|
|
applyEmptyNsNode(nodes, docNodes);
|
|
} else {
|
|
etoken = tokens.back(); // checking later
|
|
}
|
|
} else {
|
|
prefix.clear();
|
|
}
|
|
|
|
if (nodes.isEmpty()) {
|
|
if (etoken.type == asTC_KEYWORD) {
|
|
// only support these
|
|
if (etoken.content == *DBL_COLON_TRIGGER) {
|
|
processTrigger(*DBL_COLON_TRIGGER, content.left(etoken.pos));
|
|
setCompletionPrefix(prefix);
|
|
return true;
|
|
} else if (etoken.content == *DOT_TRIGGER) {
|
|
processTrigger(*DOT_TRIGGER, content.left(etoken.pos));
|
|
setCompletionPrefix(prefix);
|
|
return true;
|
|
} else {
|
|
applyEmptyNsNode(nodes, docNodes);
|
|
}
|
|
} else if (etoken.type != asTC_IDENTIFIER) {
|
|
popup()->hide();
|
|
return false;
|
|
}
|
|
|
|
if (trigger == *DOT_TRIGGER) {
|
|
// member type guessing ? basic match is enough. (>n<)
|
|
auto isBasicType = [](const QString &type) {
|
|
static QStringList basicType{
|
|
"int", "int8", "int16", "int32", "int64",
|
|
"uint", "uint8", "uint16", "uint32", "uint64",
|
|
"float", "double", "byte"};
|
|
|
|
return basicType.contains(type);
|
|
};
|
|
|
|
auto clsNodes = parser.headerNodes();
|
|
|
|
// filter the type we can use to auto-complete in docNodes
|
|
for (auto &item : docNodes) {
|
|
if (item.type == CodeInfoTip::Type::Class) {
|
|
auto name = item.nameSpace;
|
|
if (name.isEmpty()) {
|
|
name = item.name;
|
|
}
|
|
clsNodes.insert(name, item.children);
|
|
}
|
|
// a typedef can only be used to define an alias
|
|
// for primitive types, so NO NEED for auto-completing
|
|
}
|
|
|
|
tokens.removeLast();
|
|
auto ns = getNamespace(tokens);
|
|
for (auto &item : docNodes) {
|
|
if (etoken.content == item.name && ns == item.nameSpace) {
|
|
auto retType = item.addinfo.value(CodeInfoTip::RetType);
|
|
|
|
// auto type inference is not supported.
|
|
// PRs will be welcomed !!!
|
|
if (isBasicType(retType)) {
|
|
popup()->hide();
|
|
return false;
|
|
}
|
|
|
|
nodes.append(clsNodes.value(retType));
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (nodes.isEmpty()) {
|
|
applyClassNodes(nodes);
|
|
}
|
|
} else if (etoken.content.length() >= triggerAmount()) {
|
|
// completion for a.b.c or a::b.c or a::b::c.d or ::a::b.c
|
|
if (trigger == *DBL_COLON_TRIGGER) {
|
|
auto ns = getNamespace(tokens);
|
|
auto idx = tokens.length() - ns.length() * 2;
|
|
if (idx >= 0) {
|
|
if (tokens.at(idx).content == *DOT_TRIGGER) {
|
|
popup()->hide();
|
|
return false;
|
|
}
|
|
}
|
|
nodes = parser.headerNodes().value(ns) +
|
|
parser.enumsNodes().value(ns);
|
|
for (auto &n : docNodes) {
|
|
if (n.nameSpace == ns) {
|
|
nodes.append(n);
|
|
}
|
|
}
|
|
|
|
if (nodes.isEmpty()) {
|
|
return true;
|
|
}
|
|
} else if (trigger == *LEFT_PARE_TRIGGER) {
|
|
// the first is function name, an identifier
|
|
fn = etoken.content;
|
|
tokens.removeLast();
|
|
if (!tokens.isEmpty()) {
|
|
if (tokens.back().content == *DBL_COLON_TRIGGER) {
|
|
tokens.removeLast();
|
|
}
|
|
}
|
|
auto ns = getNamespace(tokens);
|
|
auto idx = tokens.length() - ns.length() * 2 + 1;
|
|
if (idx >= 0 && idx < tokens.length()) {
|
|
if (tokens.at(idx).content == *DOT_TRIGGER) {
|
|
popup()->hide();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
nodes = parser.headerNodes().value(ns);
|
|
|
|
if (nodes.isEmpty()) {
|
|
applyEmptyNsNode(nodes, docNodes);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
setModel(new CodeCompletionModel(nodes, this));
|
|
setCompletionPrefix(prefix);
|
|
return true;
|
|
}
|
|
|
|
QList<CodeInfoTip> AsCompletion::parseDocument() {
|
|
auto editor = qobject_cast<WingCodeEdit *>(widget());
|
|
if (editor == nullptr) {
|
|
return {};
|
|
}
|
|
|
|
auto code = editor->toPlainText();
|
|
auto engine = ScriptMachine::instance().engine();
|
|
|
|
// first preprocess the code
|
|
AsPreprocesser prepc(engine);
|
|
prepc.setIncludeCallback(&AsCompletion::includeCallBack, this);
|
|
|
|
auto r = prepc.loadSectionFromMemory(QStringLiteral("ASCOMPLETION"),
|
|
code.toUtf8());
|
|
if (r <= 0) {
|
|
return {};
|
|
}
|
|
|
|
auto data = prepc.scriptData();
|
|
QList<CodeInfoTip> ret;
|
|
|
|
for (auto &d : data) {
|
|
qsizetype offset = -1;
|
|
if (d.section == QStringLiteral("ASCOMPLETION")) {
|
|
offset = editor->textCursor().position();
|
|
}
|
|
ret.append(parseScriptData(offset, d.script));
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
QList<CodeInfoTip> AsCompletion::parseScriptData(qsizetype offset,
|
|
const QByteArray &code) {
|
|
QList<CodeInfoTip> ret;
|
|
|
|
auto engine = ScriptMachine::instance().engine();
|
|
QAsCodeParser parser(engine);
|
|
auto syms = parser.parseAndIntell(offset, code);
|
|
|
|
for (auto &sym : syms) {
|
|
CodeInfoTip tip;
|
|
tip.name = sym.name;
|
|
tip.nameSpace = QString::fromUtf8(sym.scope.join("::"));
|
|
|
|
switch (sym.symtype) {
|
|
case QAsCodeParser::SymbolType::Function:
|
|
case QAsCodeParser::SymbolType::FnDef:
|
|
tip.type = CodeInfoTip::Type::Function;
|
|
tip.addinfo.insert(CodeInfoTip::RetType,
|
|
QString::fromUtf8(sym.type));
|
|
tip.addinfo.insert(CodeInfoTip::Args,
|
|
QString::fromUtf8(sym.additonalInfo));
|
|
for (auto &var : sym.children) {
|
|
CodeInfoTip va;
|
|
va.dontAddGlobal = true;
|
|
va.name = var.name;
|
|
va.nameSpace = QString::fromUtf8(var.scope.join("::"));
|
|
va.addinfo.insert(CodeInfoTip::RetType, var.type);
|
|
va.type = CodeInfoTip::Type::Variable;
|
|
ret.append(va);
|
|
}
|
|
break;
|
|
case QAsCodeParser::SymbolType::Enum:
|
|
tip.type = CodeInfoTip::Type::Enum;
|
|
for (auto &e : sym.children) {
|
|
CodeInfoTip en;
|
|
en.dontAddGlobal = true;
|
|
en.name = e.name;
|
|
en.nameSpace = QString::fromUtf8(e.scope.join("::"));
|
|
en.type = CodeInfoTip::Type::Enumerater;
|
|
if (!e.additonalInfo.isEmpty()) {
|
|
en.addinfo.insert(CodeInfoTip::Comment,
|
|
en.name + QStringLiteral(" = ") +
|
|
e.additonalInfo);
|
|
}
|
|
ret.append(en);
|
|
}
|
|
break;
|
|
case QAsCodeParser::SymbolType::TypeDef:
|
|
tip.type = CodeInfoTip::Type::TypeDef;
|
|
break;
|
|
case QAsCodeParser::SymbolType::Variable:
|
|
tip.addinfo.insert(CodeInfoTip::RetType, sym.type);
|
|
tip.type = CodeInfoTip::Type::Variable;
|
|
break;
|
|
case QAsCodeParser::SymbolType::Class:
|
|
case QAsCodeParser::SymbolType::Interface:
|
|
for (auto &mem : sym.children) {
|
|
if (mem.vis != QAsCodeParser::Visiblity::Public) {
|
|
continue;
|
|
}
|
|
CodeInfoTip ctip;
|
|
ctip.name = mem.name;
|
|
ctip.nameSpace = QString::fromUtf8(mem.scope.join("::"));
|
|
if (mem.symtype == QAsCodeParser::SymbolType::Function) {
|
|
ctip.type = CodeInfoTip::Type::Function;
|
|
ctip.addinfo.insert(CodeInfoTip::RetType,
|
|
QString::fromUtf8(mem.type));
|
|
ctip.addinfo.insert(CodeInfoTip::Args,
|
|
QString::fromUtf8(mem.additonalInfo));
|
|
for (auto &var : mem.children) {
|
|
CodeInfoTip va;
|
|
va.dontAddGlobal = true;
|
|
va.name = var.name;
|
|
va.nameSpace = QString::fromUtf8(var.scope.join("::"));
|
|
va.addinfo.insert(CodeInfoTip::RetType, var.type);
|
|
va.type = CodeInfoTip::Type::Variable;
|
|
tip.children.append(va);
|
|
}
|
|
tip.children.append(ctip);
|
|
} else if (mem.symtype == QAsCodeParser::SymbolType::Variable) {
|
|
ctip.addinfo.insert(CodeInfoTip::RetType, mem.type);
|
|
ctip.type = CodeInfoTip::Type::Variable;
|
|
tip.children.append(ctip);
|
|
}
|
|
}
|
|
tip.type = CodeInfoTip::Type::Class;
|
|
break;
|
|
case QAsCodeParser::SymbolType::Invalid:
|
|
case QAsCodeParser::SymbolType::Import:
|
|
continue;
|
|
}
|
|
|
|
ret.append(tip);
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
bool AsCompletion::eventFilter(QObject *watched, QEvent *event) {
|
|
if (event->type() == QEvent::KeyPress) {
|
|
auto e = static_cast<QKeyEvent *>(event);
|
|
if (e->key() == Qt::Key_Escape) {
|
|
clearFunctionTip();
|
|
}
|
|
}
|
|
return WingCompleter::eventFilter(watched, event);
|
|
}
|