I've written a function that can be used at runtime to suggest how the provided incomplete Lua expression can be completed so that it would make sense (that is, it would evaluate to a non-nil value). For example, this can be used with an application console that can evaluate Lua expressions at runtime, so that it would allow you to autocomplete expressions such as "network.database[index].ro" with a Tab press to "network.database[index].rowCount" or "network.database[index].rows" (provided that network.database[index]
is a table that contains values at the rowCount
and rows
indices).
I am interested in all kinds of suggestions of how can this be improved. Did I handle all the special cases? Any C++ style suggestions?
extern "C"
{
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"
}
#include <set>
#include <string>
#include <vector>
#include <boost/algorithm/string/predicate.hpp>
// =================================================================================
// The 'buildAutoCompleteCandidates' returns a collection of options that suggest how
// the provided string could be completed so that it would evaluate to a valid value.
//
// For example, when there's a console that can be used to evaluate Lua expressions
// at the application's runtime, this can be used to provide the console with the autocomplete
// feature, similar to a <Tab> press in bash etc.
//
// For instance, the expression 'world.entities[1].p' can perhaps be autocompleted
// as 'world.entities[1].posX', 'world.entities[1].posY', or 'world.entities[1].pivot',
// and so on.
//
// NOTES:
// 1/ The expression to autocomplete is evaluated at runtime.
// 2/ The autocomplete search traverses metatables' __index tables as needed.
// 3/ The metatables' non-table __index values (such as __index functions) stop the search.
// 4/ Endless metatable loops are detected and stop the search.
// 5/ The Lua stack remains in its original state when the call is over.
// 6/ Global variable names can be autocompleted as well.
// =================================================================================
std::vector<std::string> buildAutoCompleteCandidates (lua_State* L, const std::string& str)
{
std::string strBase;
std::string strSep;
std::string strKeyStart;
// str: "world.entities[1].p"
// --> strBase: "world.entities[1]"
// --> strSep: "."
// --> strKeyStart: "p"
auto lastSepPos = str.find_last_of(".:");
if (lastSepPos == std::string::npos)
{
strKeyStart = str;
}
else
{
strBase = str.substr(0, lastSepPos);
strKeyStart = str.substr(lastSepPos+1);
strSep = str[lastSepPos];
}
std::vector<std::string> result;
if (strBase.empty())
{
// For Lua 5.1 replace the line with:
// lua_pushvalue(L, LUA_GLOBALSINDEX)
lua_pushglobaltable(L);
}
else
{
// Evaluate the expresssion contained in strBase
// and leave the result at the topmost stack position.
luaL_loadstring(L, ("return " + strBase).c_str());
lua_pcall(L, 0, 1, 0);
if (!lua_istable(L, -1))
{
lua_pop(L, 1);
return result;
}
}
// We now have the base table whose indices we need
// to iterate over at the topmost (-1) stack position.
std::set<const void*> visited;
while (true)
{
// Iterate over the table:
for (lua_pushnil(L); lua_next(L, -2); lua_pop(L, 1))
{
// For the ':' separator, it only makes sense
// to propose autocompletion with 'function' values:
if (strSep == ":" && !lua_isfunction(L, -1))
{
continue;
}
// Consider only the string indices:
if (lua_type(L, -2) == LUA_TSTRING)
{
std::string key = lua_tostring(L, -2);
if (boost::starts_with(key, strKeyStart))
{
result.push_back(strBase + strSep + key);
}
}
}
// Follow one step up in the metatable chain:
visited.insert(lua_topointer(L, -1));
if (lua_getmetatable(L, -1) != 0)
{
lua_remove(L, -2);
lua_getfield(L, -1, "__index");
lua_remove(L, -2);
if (!lua_istable(L, -1))
{
// Non-table '__index' values cannot
// provide us with a list of all indices.
lua_pop(L, 1);
break;
}
// Detect and break metatable cycles.
const void* index = lua_topointer(L, -1);
if (visited.find(index) != visited.end())
{
lua_pop(L, 1);
break;
}
}
else
{
lua_pop(L, 1);
break;
}
}
return result;
}