701 lines
22 KiB
C++
701 lines
22 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 "asdebugger.h"
|
|
#include "define.h"
|
|
|
|
#include <QApplication>
|
|
#include <QFileInfo>
|
|
#include <QProcess>
|
|
#include <QRegularExpression>
|
|
#include <QTextStream>
|
|
#include <QThread>
|
|
|
|
asDebugger::asDebugger(QObject *parent) : QObject(parent) {
|
|
m_action = CONTINUE;
|
|
m_lastFunction = nullptr;
|
|
m_engine = nullptr;
|
|
}
|
|
|
|
asDebugger::~asDebugger() { setEngine(nullptr); }
|
|
|
|
void asDebugger::registerToStringCallback(const asITypeInfo *ti,
|
|
ToStringCallback callback) {
|
|
if (!m_toStringCallbacks.contains(ti))
|
|
m_toStringCallbacks.insert(ti, callback);
|
|
}
|
|
|
|
void asDebugger::takeCommands(asIScriptContext *ctx) {
|
|
emit onPullVariables(globalVariables(ctx), localVariables(ctx));
|
|
emit onPullCallStack(retriveCallstack(ctx));
|
|
|
|
while (m_action == DebugAction::PAUSE) {
|
|
qApp->processEvents();
|
|
}
|
|
|
|
switch (m_action) {
|
|
case ABORT:
|
|
ctx->Abort();
|
|
return;
|
|
case PAUSE:
|
|
Q_ASSERT(false);
|
|
break;
|
|
case STEP_OVER:
|
|
m_lastCommandAtStackLevel = ctx ? ctx->GetCallstackSize() : 1;
|
|
break;
|
|
case STEP_OUT:
|
|
m_lastCommandAtStackLevel = ctx ? ctx->GetCallstackSize() : 0;
|
|
break;
|
|
case CONTINUE:
|
|
case STEP_INTO:
|
|
break;
|
|
}
|
|
|
|
emit onDebugActionExec();
|
|
}
|
|
|
|
void asDebugger::lineCallback(asIScriptContext *ctx) {
|
|
Q_ASSERT(ctx);
|
|
|
|
// prevent UI freezing
|
|
qApp->processEvents();
|
|
|
|
// This should never happen, but it doesn't hurt to validate it
|
|
if (ctx == nullptr)
|
|
return;
|
|
|
|
auto isDbg = reinterpret_cast<asPWORD>(
|
|
ctx->GetUserData(AsUserDataType::UserData_isDbg));
|
|
if (!isDbg) {
|
|
return;
|
|
}
|
|
|
|
const char *file = 0;
|
|
int col = 0;
|
|
int lineNbr = ctx->GetLineNumber(0, &col, &file);
|
|
|
|
// why?
|
|
// LineCallBack will be called each only a sentence,
|
|
// just like a bytecode level debugger
|
|
// for(auto i = 0; i < 5 ; i++) this line will break twice at first
|
|
if (ctx->GetUserData() == nullptr) {
|
|
auto dbgContext = new ContextDbgInfo;
|
|
ctx->SetUserData(dbgContext, AsUserDataType::UserData_ContextDbgInfo);
|
|
} else {
|
|
auto dbgContext =
|
|
reinterpret_cast<ContextDbgInfo *>(ctx->GetUserData());
|
|
if (m_action != CONTINUE) {
|
|
if (dbgContext->line == lineNbr && dbgContext->file == file &&
|
|
dbgContext->stackCount == ctx->GetCallstackSize() &&
|
|
dbgContext->col != col) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// By default we ignore callbacks when the context is not active.
|
|
// An application might override this to for example disconnect the
|
|
// debugger as the execution finished.
|
|
if (ctx->GetState() != asEXECUTION_ACTIVE)
|
|
return;
|
|
|
|
auto dbgContext = reinterpret_cast<ContextDbgInfo *>(ctx->GetUserData());
|
|
Q_ASSERT(dbgContext);
|
|
|
|
auto rc = [](ContextDbgInfo *dbgContext, const QString &file, int lineNbr,
|
|
int col, asUINT stackCount) {
|
|
if (dbgContext->line != lineNbr) {
|
|
dbgContext->file = file;
|
|
dbgContext->line = lineNbr;
|
|
dbgContext->col = col;
|
|
dbgContext->stackCount = stackCount;
|
|
}
|
|
};
|
|
|
|
qApp->processEvents();
|
|
|
|
switch (m_action) {
|
|
case ABORT:
|
|
ctx->Abort();
|
|
return;
|
|
case PAUSE:
|
|
break;
|
|
case CONTINUE:
|
|
if (!checkBreakPoint(ctx)) {
|
|
rc(dbgContext, file, lineNbr, col, ctx->GetCallstackSize());
|
|
return;
|
|
}
|
|
break;
|
|
case STEP_INTO:
|
|
m_action = PAUSE;
|
|
break;
|
|
case STEP_OVER: {
|
|
auto s = ctx->GetCallstackSize();
|
|
if (s > m_lastCommandAtStackLevel) {
|
|
if (!checkBreakPoint(ctx)) {
|
|
rc(dbgContext, file, lineNbr, col, ctx->GetCallstackSize());
|
|
return;
|
|
}
|
|
}
|
|
m_action = PAUSE;
|
|
break;
|
|
}
|
|
case STEP_OUT: {
|
|
auto s = ctx->GetCallstackSize();
|
|
if (s >= m_lastCommandAtStackLevel) {
|
|
if (!checkBreakPoint(ctx)) {
|
|
rc(dbgContext, file, lineNbr, col, ctx->GetCallstackSize());
|
|
return;
|
|
}
|
|
}
|
|
m_action = PAUSE;
|
|
break;
|
|
}
|
|
}
|
|
|
|
rc(dbgContext, file, lineNbr, col, ctx->GetCallstackSize());
|
|
|
|
emit onRunCurrentLine(file, lineNbr);
|
|
|
|
takeCommands(ctx);
|
|
}
|
|
|
|
void asDebugger::addFileBreakPoint(const QString &file, int lineNbr) {
|
|
BreakPoint bp(file, lineNbr, false);
|
|
m_breakPoints.push_back(bp);
|
|
emit breakPointChanged();
|
|
}
|
|
|
|
void asDebugger::removeFileBreakPoint(const QString &file, int lineNbr) {
|
|
auto r = std::remove_if(
|
|
m_breakPoints.begin(), m_breakPoints.end(), [=](const BreakPoint &bp) {
|
|
return bp.name == file && bp.lineNbr == lineNbr && bp.func == false;
|
|
});
|
|
if (r == m_breakPoints.end()) {
|
|
return;
|
|
}
|
|
m_breakPoints.erase(r);
|
|
emit breakPointChanged();
|
|
}
|
|
|
|
void asDebugger::addFuncBreakPoint(const QString &func) {
|
|
// Trim the function name
|
|
QString actual = func.trimmed();
|
|
|
|
BreakPoint bp(actual, 0, true);
|
|
m_breakPoints.push_back(bp);
|
|
emit breakPointChanged();
|
|
}
|
|
|
|
void asDebugger::removeFuncBreakPoint(const QString &func) {
|
|
QString actual = func.trimmed();
|
|
m_breakPoints.erase(std::remove_if(
|
|
m_breakPoints.begin(), m_breakPoints.end(), [=](const BreakPoint &bp) {
|
|
return bp.name == actual && bp.func == true;
|
|
}));
|
|
emit breakPointChanged();
|
|
}
|
|
|
|
void asDebugger::clearBreakPoint() {
|
|
m_breakPoints.clear();
|
|
emit breakPointChanged();
|
|
}
|
|
|
|
const QVector<asDebugger::BreakPoint> &asDebugger::breakPoints() {
|
|
return m_breakPoints;
|
|
}
|
|
|
|
QVector<asDebugger::VariablesInfo>
|
|
asDebugger::localVariables(asIScriptContext *ctx) {
|
|
QVector<VariablesInfo> vars;
|
|
if (ctx == nullptr) {
|
|
return vars;
|
|
}
|
|
|
|
asIScriptFunction *func = ctx->GetFunction();
|
|
if (!func) {
|
|
return vars;
|
|
}
|
|
|
|
for (asUINT n = 0; n < func->GetVarCount(); n++) {
|
|
// Skip temporary variables
|
|
// TODO: Should there be an option to view temporary variables too?
|
|
const char *name;
|
|
func->GetVar(n, &name);
|
|
if (name == 0 || strlen(name) == 0)
|
|
continue;
|
|
|
|
if (ctx->IsVarInScope(n)) {
|
|
int typeId;
|
|
ctx->GetVar(n, 0, 0, &typeId);
|
|
|
|
VariablesInfo var;
|
|
var.name = func->GetVarDecl(n);
|
|
var.value =
|
|
toString(ctx->GetAddressOfVar(n), typeId, ctx->GetEngine());
|
|
vars << var;
|
|
}
|
|
}
|
|
return vars;
|
|
}
|
|
|
|
QVector<asDebugger::VariablesInfo>
|
|
asDebugger::globalVariables(asIScriptContext *ctx) {
|
|
QVector<VariablesInfo> vars;
|
|
if (ctx == nullptr) {
|
|
return vars;
|
|
}
|
|
|
|
// Determine the current module from the function
|
|
asIScriptFunction *func = ctx->GetFunction();
|
|
if (!func) {
|
|
return vars;
|
|
}
|
|
|
|
asIScriptModule *mod = func->GetModule();
|
|
if (!mod) {
|
|
return vars;
|
|
}
|
|
|
|
for (asUINT n = 0; n < mod->GetGlobalVarCount(); n++) {
|
|
int typeId = 0;
|
|
mod->GetGlobalVar(n, 0, 0, &typeId);
|
|
|
|
VariablesInfo var;
|
|
var.name = mod->GetGlobalVarDeclaration(n);
|
|
var.value =
|
|
toString(mod->GetAddressOfGlobalVar(n), typeId, ctx->GetEngine());
|
|
}
|
|
|
|
return vars;
|
|
}
|
|
|
|
void asDebugger::listMemberProperties(asIScriptContext *ctx) {
|
|
if (ctx == nullptr) {
|
|
return;
|
|
}
|
|
|
|
void *ptr = ctx->GetThisPointer();
|
|
if (ptr) {
|
|
QString str;
|
|
QTextStream s(&str);
|
|
|
|
s << QStringLiteral("this = ")
|
|
<< toString(ptr, ctx->GetThisTypeId(), ctx->GetEngine()) << Qt::endl;
|
|
}
|
|
}
|
|
|
|
bool asDebugger::checkBreakPoint(asIScriptContext *ctx) {
|
|
if (ctx == nullptr)
|
|
return false;
|
|
|
|
const char *tmp = 0;
|
|
|
|
int column = 0;
|
|
int lineNbr = ctx->GetLineNumber(0, &column, &tmp);
|
|
|
|
QString file = tmp ? QString(tmp) : QString();
|
|
|
|
// Did we move into a new function?
|
|
asIScriptFunction *func = ctx->GetFunction();
|
|
if (m_lastFunction != func) {
|
|
// Check if any breakpoints need adjusting
|
|
for (QVector<BreakPoint>::size_type n = 0; n < m_breakPoints.size();
|
|
n++) {
|
|
// We need to check for a breakpoint at entering the function
|
|
if (m_breakPoints[n].func) {
|
|
if (m_breakPoints[n].name == func->GetName()) {
|
|
// Entering function. Transforming it into break point.
|
|
|
|
// Transform the function breakpoint into a file breakpoint
|
|
m_breakPoints[n].name = file;
|
|
m_breakPoints[n].lineNbr = lineNbr;
|
|
m_breakPoints[n].func = false;
|
|
m_breakPoints[n].needsAdjusting = false;
|
|
}
|
|
}
|
|
// Check if a given breakpoint fall on a line with code or else
|
|
// adjust it to the next line
|
|
else if (m_breakPoints[n].needsAdjusting &&
|
|
m_breakPoints[n].name == file) {
|
|
int line = func->FindNextLineWithCode(m_breakPoints[n].lineNbr);
|
|
if (line >= 0) {
|
|
m_breakPoints[n].needsAdjusting = false;
|
|
if (line != m_breakPoints[n].lineNbr) {
|
|
// Moving break point to next line with code
|
|
|
|
auto old = m_breakPoints[n];
|
|
// Move the breakpoint to the next line
|
|
m_breakPoints[n].lineNbr = line;
|
|
emit onAdjustBreakPointLine(old, line);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
m_lastFunction = func;
|
|
|
|
// Determine if there is a breakpoint at the current line
|
|
for (QVector<BreakPoint>::size_type n = 0; n < m_breakPoints.size(); n++) {
|
|
auto bpName = m_breakPoints[n].name;
|
|
// Should we break?
|
|
if (!m_breakPoints[n].func && m_breakPoints[n].lineNbr == lineNbr &&
|
|
#ifdef Q_OS_WIN
|
|
bpName.compare(file, Qt::CaseInsensitive) == 0) {
|
|
#else
|
|
bpName == file) {
|
|
#endif
|
|
m_action = PAUSE; // hit and pause script
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
QString asDebugger::toString(void *value, asUINT typeId,
|
|
asIScriptEngine *engine) {
|
|
if (value == nullptr)
|
|
return QStringLiteral("<null>");
|
|
|
|
// If no engine pointer was provided use the default
|
|
if (engine == nullptr)
|
|
engine = m_engine;
|
|
|
|
QString str;
|
|
QTextStream s(&str);
|
|
if (typeId == asTYPEID_VOID)
|
|
return QStringLiteral("<void>");
|
|
else if (typeId == asTYPEID_BOOL)
|
|
return *(bool *)value ? QStringLiteral("true")
|
|
: QStringLiteral("false");
|
|
else if (typeId == asTYPEID_INT8)
|
|
s << (int)*(signed char *)value;
|
|
else if (typeId == asTYPEID_INT16)
|
|
s << (int)*(signed short *)value;
|
|
else if (typeId == asTYPEID_INT32)
|
|
s << *(signed int *)value;
|
|
else if (typeId == asTYPEID_INT64)
|
|
s << *(asINT64 *)value;
|
|
else if (typeId == asTYPEID_UINT8)
|
|
s << (unsigned int)*(unsigned char *)value;
|
|
else if (typeId == asTYPEID_UINT16)
|
|
s << (unsigned int)*(unsigned short *)value;
|
|
else if (typeId == asTYPEID_UINT32)
|
|
s << *(unsigned int *)value;
|
|
else if (typeId == asTYPEID_UINT64)
|
|
s << *(asQWORD *)value;
|
|
else if (typeId == asTYPEID_FLOAT)
|
|
s << *(float *)value;
|
|
else if (typeId == asTYPEID_DOUBLE)
|
|
s << *(double *)value;
|
|
else if ((typeId & asTYPEID_MASK_OBJECT) == 0) {
|
|
// The type is an enum
|
|
s << *(asUINT *)value;
|
|
|
|
// Check if the value matches one of the defined enums
|
|
if (engine) {
|
|
asITypeInfo *t = engine->GetTypeInfoById(typeId);
|
|
for (int n = t->GetEnumValueCount(); n-- > 0;) {
|
|
int enumVal;
|
|
const char *enumName = t->GetEnumValueByIndex(n, &enumVal);
|
|
if (enumVal == *(int *)value) {
|
|
s << QStringLiteral(", ") << enumName;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
} else if (typeId & asTYPEID_SCRIPTOBJECT) {
|
|
// Dereference handles, so we can see what it points to
|
|
if (typeId & asTYPEID_OBJHANDLE)
|
|
value = *(void **)value;
|
|
|
|
asIScriptObject *obj = (asIScriptObject *)value;
|
|
|
|
// Print the address of the object
|
|
// s << QStringLiteral("{") << obj << QStringLiteral("}");
|
|
|
|
s << QStringLiteral("{");
|
|
// Print the members
|
|
if (obj && _expandMembers > 0) {
|
|
asITypeInfo *type = obj->GetObjectType();
|
|
for (asUINT n = 0; n < obj->GetPropertyCount(); n++) {
|
|
if (n == 0)
|
|
s << QStringLiteral(" ");
|
|
else
|
|
s << QStringLiteral(", ");
|
|
|
|
const char *name = nullptr;
|
|
type->GetProperty(n, &name);
|
|
if (name) {
|
|
s << name /*type->GetPropertyDeclaration(n)*/
|
|
<< QStringLiteral(" = ")
|
|
<< toString(obj->GetAddressOfProperty(n),
|
|
obj->GetPropertyTypeId(n), type->GetEngine());
|
|
}
|
|
}
|
|
}
|
|
|
|
s << QStringLiteral("}");
|
|
} else {
|
|
// Dereference handles, so we can see what it points to
|
|
if (typeId & asTYPEID_OBJHANDLE)
|
|
value = *(void **)value;
|
|
|
|
// Print the address for reference types so it will be
|
|
// possible to see when handles point to the same object
|
|
if (engine) {
|
|
asITypeInfo *type = engine->GetTypeInfoById(typeId);
|
|
// if (type->GetFlags() & asOBJ_REF)
|
|
// s << QStringLiteral("{") << value << QStringLiteral("}");
|
|
|
|
if (value) {
|
|
// Check if there is a registered to-string callback
|
|
auto it = m_toStringCallbacks.find(type);
|
|
if (it == m_toStringCallbacks.end()) {
|
|
// If the type is a template instance, there might be a
|
|
// to-string callback for the generic template type
|
|
if (type->GetFlags() & asOBJ_TEMPLATE) {
|
|
asITypeInfo *tmplType =
|
|
engine->GetTypeInfoByName(type->GetName());
|
|
it = m_toStringCallbacks.find(tmplType);
|
|
}
|
|
}
|
|
|
|
if (it != m_toStringCallbacks.end()) {
|
|
if (type->GetFlags() & asOBJ_REF)
|
|
s << QStringLiteral(" ");
|
|
|
|
// Invoke the callback to get the string representation of
|
|
// this type
|
|
s << it.value()(value, this);
|
|
} else {
|
|
// Unknown type: type + address
|
|
s << type->GetName() << '(' << value << ')';
|
|
}
|
|
}
|
|
} else
|
|
s << tr("{no engine}");
|
|
}
|
|
|
|
return str;
|
|
}
|
|
|
|
asDebugger::GCStatistic asDebugger::gcStatistics() {
|
|
if (m_engine == nullptr) {
|
|
return {};
|
|
}
|
|
|
|
GCStatistic sta;
|
|
m_engine->GetGCStatistics(&sta.currentSize, &sta.totalDestroyed,
|
|
&sta.totalDetected, &sta.newObjects,
|
|
&sta.totalNewDestroyed);
|
|
return sta;
|
|
}
|
|
|
|
void asDebugger::runDebugAction(DebugAction action) { m_action = action; }
|
|
|
|
void asDebugger::resetState() { m_action = CONTINUE; }
|
|
|
|
void asDebugger::deleteDbgContextInfo(void *info) {
|
|
if (info) {
|
|
delete reinterpret_cast<ContextDbgInfo *>(info);
|
|
}
|
|
}
|
|
|
|
asDebugger::DebugAction asDebugger::currentState() const { return m_action; }
|
|
|
|
void asDebugger::setEngine(asIScriptEngine *engine) {
|
|
if (m_engine != engine) {
|
|
if (m_engine)
|
|
m_engine->Release();
|
|
m_engine = engine;
|
|
if (m_engine)
|
|
m_engine->AddRef();
|
|
m_action = CONTINUE;
|
|
}
|
|
}
|
|
|
|
asIScriptEngine *asDebugger::getEngine() { return m_engine; }
|
|
|
|
QList<asDebugger::CallStackItem>
|
|
asDebugger::retriveCallstack(asIScriptContext *ctx) {
|
|
if (ctx == nullptr) {
|
|
return {};
|
|
}
|
|
|
|
QList<CallStackItem> callstack;
|
|
|
|
QString str;
|
|
QTextStream s(&str);
|
|
const char *file = nullptr;
|
|
int lineNbr = 0;
|
|
for (asUINT n = 0; n < ctx->GetCallstackSize(); n++) {
|
|
lineNbr = ctx->GetLineNumber(n, 0, &file);
|
|
|
|
CallStackItem item;
|
|
item.file = file;
|
|
item.lineNbr = lineNbr;
|
|
item.decl = ctx->GetFunction(n)->GetDeclaration();
|
|
|
|
callstack << item;
|
|
}
|
|
|
|
return callstack;
|
|
}
|
|
|
|
QString asDebugger::printValue(const QString &expr, asIScriptContext *ctx,
|
|
WatchExpError &error) {
|
|
error = WatchExpError::Error;
|
|
|
|
if (ctx == nullptr) {
|
|
return {};
|
|
}
|
|
|
|
asIScriptEngine *engine = ctx->GetEngine();
|
|
|
|
// Tokenize the input string to get the variable scope and name
|
|
asUINT len = 0;
|
|
QString scope;
|
|
QString name;
|
|
QString str = expr;
|
|
asETokenClass t = engine->ParseToken(str.toUtf8(), 0, &len);
|
|
while (t == asTC_IDENTIFIER ||
|
|
(t == asTC_KEYWORD && len == 2 && str.startsWith("::"))) {
|
|
if (t == asTC_KEYWORD) {
|
|
if (scope.isEmpty() && name.isEmpty())
|
|
scope = "::"; // global scope
|
|
else if (scope == "::" || scope.isEmpty())
|
|
scope = name; // namespace
|
|
else
|
|
scope += "::" + name; // nested namespace
|
|
name.clear();
|
|
} else if (t == asTC_IDENTIFIER)
|
|
name = str.left(len);
|
|
|
|
// Skip the parsed token and get the next one
|
|
str = str.mid(len);
|
|
t = engine->ParseToken(str.toUtf8(), 0, &len);
|
|
}
|
|
|
|
if (name.size()) {
|
|
// Find the variable
|
|
void *ptr = 0;
|
|
int typeId = 0;
|
|
|
|
asIScriptFunction *func = ctx->GetFunction();
|
|
if (!func) {
|
|
return {};
|
|
}
|
|
|
|
// skip local variables if a scope was informed
|
|
if (scope.isEmpty()) {
|
|
// We start from the end, in case the same name is reused in
|
|
// different scopes
|
|
for (asUINT n = func->GetVarCount(); n-- > 0;) {
|
|
const char *varName = 0;
|
|
ctx->GetVar(n, 0, &varName, &typeId);
|
|
if (ctx->IsVarInScope(n) && varName != 0 && name == varName) {
|
|
ptr = ctx->GetAddressOfVar(n);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Look for class members, if we're in a class method
|
|
if (!ptr && func->GetObjectType()) {
|
|
if (name == "this") {
|
|
ptr = ctx->GetThisPointer();
|
|
typeId = ctx->GetThisTypeId();
|
|
} else {
|
|
asITypeInfo *type =
|
|
engine->GetTypeInfoById(ctx->GetThisTypeId());
|
|
for (asUINT n = 0; n < type->GetPropertyCount(); n++) {
|
|
const char *propName = 0;
|
|
int offset = 0;
|
|
bool isReference = 0;
|
|
int compositeOffset = 0;
|
|
bool isCompositeIndirect = false;
|
|
type->GetProperty(n, &propName, &typeId, 0, 0, &offset,
|
|
&isReference, 0, &compositeOffset,
|
|
&isCompositeIndirect);
|
|
if (name == propName) {
|
|
ptr = (void *)(((asBYTE *)ctx->GetThisPointer()) +
|
|
compositeOffset);
|
|
if (isCompositeIndirect)
|
|
ptr = *(void **)ptr;
|
|
ptr = (void *)(((asBYTE *)ptr) + offset);
|
|
if (isReference)
|
|
ptr = *(void **)ptr;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Look for global variables
|
|
if (!ptr) {
|
|
if (scope.isEmpty()) {
|
|
// If no explicit scope was informed then use the namespace of
|
|
// the current function by default
|
|
scope = func->GetNamespace();
|
|
} else if (scope == "::") {
|
|
// The global namespace will be empty
|
|
scope.clear();
|
|
}
|
|
|
|
asIScriptModule *mod = func->GetModule();
|
|
if (mod) {
|
|
for (asUINT n = 0; n < mod->GetGlobalVarCount(); n++) {
|
|
const char *varName = 0, *nameSpace = 0;
|
|
mod->GetGlobalVar(n, &varName, &nameSpace, &typeId);
|
|
|
|
// Check if both name and namespace match
|
|
if (name == varName && scope == nameSpace) {
|
|
ptr = mod->GetAddressOfGlobalVar(n);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (ptr) {
|
|
// TODO: If there is a . after the identifier, check for members
|
|
// TODO: If there is a [ after the identifier try to call the
|
|
// 'opIndex(expr) const' method
|
|
if (!str.isEmpty()) {
|
|
error = WatchExpError::NotEndAfterSymbol;
|
|
return {};
|
|
} else {
|
|
return toString(ptr, typeId, engine);
|
|
}
|
|
} else {
|
|
error = WatchExpError::NoMatchingSymbol;
|
|
return {};
|
|
}
|
|
} else {
|
|
error = WatchExpError::ExpectedIdentifier;
|
|
return {};
|
|
}
|
|
}
|
|
|
|
int asDebugger::expandMembers() const { return _expandMembers; }
|
|
|
|
void asDebugger::setExpandMembers(int newExpandMembers) {
|
|
_expandMembers = qMax(newExpandMembers, 3);
|
|
}
|