Skip to content

Commit

Permalink
Lua: Add basic autocomplete in the console
Browse files Browse the repository at this point in the history
  • Loading branch information
glebm committed Nov 3, 2023
1 parent e10f468 commit b30b712
Show file tree
Hide file tree
Showing 9 changed files with 292 additions and 31 deletions.
3 changes: 2 additions & 1 deletion Source/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ set(libdevilutionx_SRCS
levels/town.cpp
levels/trigs.cpp

lua/autocomplete.cpp
lua/lua.cpp
lua/repl.cpp
lua/modules/audio.cpp
Expand Down Expand Up @@ -295,7 +296,7 @@ if(DISCORD_INTEGRATION)
target_link_libraries(libdevilutionx PRIVATE discord discord_game_sdk)
endif()

target_link_libraries(libdevilutionx PRIVATE ${LUA_LIBRARIES} sol2::sol2)
target_link_libraries(libdevilutionx PUBLIC ${LUA_LIBRARIES} sol2::sol2)

if(SCREEN_READER_INTEGRATION AND WIN32)
target_compile_definitions(libdevilutionx PRIVATE Tolk)
Expand Down
15 changes: 10 additions & 5 deletions Source/engine/render/text_render.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,7 @@ int GetLineStartX(UiFlags flags, const Rectangle &rect, int lineWidth)

uint32_t DoDrawString(const Surface &out, std::string_view text, Rectangle rect, Point &characterPosition,
int lineWidth, int rightMargin, int bottomMargin, GameFontTables size, text_color color, bool outline,
const TextRenderOptions &opts)
TextRenderOptions &opts)
{
CurrentFont currentFont;

Expand All @@ -410,12 +410,17 @@ uint32_t DoDrawString(const Surface &out, std::string_view text, Rectangle rect,
size_t cpLen;

const auto maybeDrawCursor = [&]() {
if (opts.cursorPosition == static_cast<int>(text.size() - remaining.size()) && GetAnimationFrame(2, 500) != 0) {
if (opts.cursorPosition == static_cast<int>(text.size() - remaining.size())) {
Point position = characterPosition;
MaybeWrap(position, 2, rightMargin, position.x, opts.lineHeight);
OptionalClxSpriteList baseFont = LoadFont(size, color, 0);
if (baseFont)
DrawFont(out, position, (*baseFont)['|'], color, outline);
if (GetAnimationFrame(2, 500) != 0) {
OptionalClxSpriteList baseFont = LoadFont(size, color, 0);
if (baseFont)
DrawFont(out, position, (*baseFont)['|'], color, outline);
}
if (opts.renderedCursorPositionOut != nullptr) {
*opts.renderedCursorPositionOut = position;
}
}
};

Expand Down
3 changes: 3 additions & 0 deletions Source/engine/render/text_render.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,9 @@ struct TextRenderOptions {
} highlightRange = { 0, 0 };

uint8_t highlightColor = PAL8_RED + 6;

/** @brief If a cursor is rendered, the surface coordinates are saved here. */
std::optional<Point> *renderedCursorPositionOut = nullptr;
};

/**
Expand Down
112 changes: 112 additions & 0 deletions Source/lua/autocomplete.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
#ifdef _DEBUG
#include "lua/autocomplete.hpp"

#include <algorithm>
#include <string>
#include <string_view>
#include <unordered_set>
#include <vector>

#include <sol/sol.hpp>

#include "utils/algorithm/container.hpp"

namespace devilution {

namespace {

std::string_view GetLastToken(std::string_view text)
{
if (text.empty())
return {};
size_t i = text.size();
while (i > 0 && text[i - 1] != ' ')
--i;
return text.substr(i);
}

bool IsCallable(const sol::object &value)
{
if (value.get_type() == sol::type::function)
return true;
if (!value.is<sol::table>())
return false;
const auto table = value.as<sol::table>();
const auto metatable = table.get<std::optional<sol::object>>(sol::metatable_key);
if (!metatable || !metatable->is<sol::table>())
return false;
const auto callFn = metatable->as<sol::table>().get<std::optional<sol::object>>(sol::meta_function::call);
return callFn.has_value();
}

void SuggestionsFromTable(const sol::table &table, std::string_view prefix,
size_t maxSuggestions, std::unordered_set<LuaAutocompleteSuggestion> &out)
{
for (const auto &[key, value] : table) {
if (key.get_type() == sol::type::string) {
std::string keyStr = key.as<std::string>();
if (!keyStr.starts_with(prefix) || keyStr.size() == prefix.size())
continue;
if (keyStr.starts_with("__") && !prefix.starts_with("__"))
continue;
// sol-internal keys -- we don't have fonts for these so skip them.
if (keyStr.find("") != std::string::npos
|| keyStr.find("") != std::string::npos
|| keyStr.find("🔩") != std::string::npos)
continue;
std::string completionText = keyStr.substr(prefix.size());
LuaAutocompleteSuggestion suggestion { std::move(keyStr), std::move(completionText) };
if (IsCallable(value)) {
suggestion.completionText.append("()");
suggestion.cursorAdjust = -1;
}
out.insert(std::move(suggestion));
if (out.size() == maxSuggestions)
break;
}
}
const auto fallback = table.get<std::optional<sol::object>>(sol::metatable_key);
if (fallback.has_value() && fallback->get_type() == sol::type::table) {
SuggestionsFromTable(fallback->as<sol::table>(), prefix, maxSuggestions, out);
}
}

} // namespace

void GetLuaAutocompleteSuggestions(std::string_view text, const sol::environment &lua,
size_t maxSuggestions, std::vector<LuaAutocompleteSuggestion> &out)
{
out.clear();
if (text.empty())
return;
std::string_view token = GetLastToken(text);
const size_t dotPos = token.rfind('.');
const std::string_view prefix = token.substr(dotPos + 1);
token.remove_suffix(token.size() - (dotPos == std::string_view::npos ? 0 : dotPos));

std::unordered_set<LuaAutocompleteSuggestion> suggestions;
const auto addSuggestions = [&](const sol::table &table) {
SuggestionsFromTable(table, prefix, maxSuggestions, suggestions);
};

if (token.empty()) {
addSuggestions(lua);
const auto fallback = lua.get<std::optional<sol::object>>("_G");
if (fallback.has_value() && fallback->get_type() == sol::type::table) {
addSuggestions(fallback->as<sol::table>());
}
} else {
const auto obj = lua.traverse_get<std::optional<sol::object>>(token);
if (!obj.has_value())
return;
if (obj->get_type() == sol::type::table) {
addSuggestions(obj->as<sol::table>());
}
}

out.insert(out.end(), suggestions.begin(), suggestions.end());
c_sort(out);
}

} // namespace devilution
#endif // _DEBUG
44 changes: 44 additions & 0 deletions Source/lua/autocomplete.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#pragma once
#ifdef _DEBUG
#include <cstddef>
#include <string>
#include <string_view>
#include <vector>

#include <sol/forward.hpp>

namespace devilution {

struct LuaAutocompleteSuggestion {
std::string displayText;
std::string completionText;
int cursorAdjust = 0;

bool operator==(const LuaAutocompleteSuggestion &other) const
{
return displayText == other.displayText;
}

bool operator<(const LuaAutocompleteSuggestion &other) const
{
return displayText < other.displayText;
}
};

void GetLuaAutocompleteSuggestions(
std::string_view text, const sol::environment &lua,
size_t maxSuggestions, std::vector<LuaAutocompleteSuggestion> &out);

} // namespace devilution

namespace std {
template <>
struct hash<devilution::LuaAutocompleteSuggestion> {
size_t operator()(const devilution::LuaAutocompleteSuggestion &suggestion) const
{
return hash<std::string>()(suggestion.displayText);
}
};
} // namespace std

#endif // _DEBUG
5 changes: 1 addition & 4 deletions Source/lua/lua.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@
#include <string_view>

#include <expected.hpp>

namespace sol {
class state;
} // namespace sol
#include <sol/forward.hpp>

namespace devilution {

Expand Down
16 changes: 8 additions & 8 deletions Source/lua/repl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,6 @@ void CreateReplEnvironment()
lua_setwarnf(replEnv->lua_state(), LuaConsoleWarn, /*ud=*/nullptr);
}

sol::environment &ReplEnvironment()
{
if (!replEnv.has_value())
CreateReplEnvironment();
return *replEnv;
}

sol::protected_function_result TryRunLuaAsExpressionThenStatement(std::string_view code)
{
// Try to compile as an expression first. This also how the `lua` repl is implemented.
Expand All @@ -81,12 +74,19 @@ sol::protected_function_result TryRunLuaAsExpressionThenStatement(std::string_vi
}
}
sol::stack_aligned_protected_function fn(lua.lua_state(), -1);
sol::set_environment(ReplEnvironment(), fn);
sol::set_environment(GetLuaReplEnvironment(), fn);
return fn();
}

} // namespace

sol::environment &GetLuaReplEnvironment()
{
if (!replEnv.has_value())
CreateReplEnvironment();
return *replEnv;
}

tl::expected<std::string, std::string> RunLuaReplLine(std::string_view code)
{
const sol::protected_function_result result = TryRunLuaAsExpressionThenStatement(code);
Expand Down
3 changes: 3 additions & 0 deletions Source/lua/repl.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@
#include <string_view>

#include <expected.hpp>
#include <sol/forward.hpp>

namespace devilution {

tl::expected<std::string, std::string> RunLuaReplLine(std::string_view code);

sol::environment &GetLuaReplEnvironment();

} // namespace devilution
#endif // _DEBUG
Loading

0 comments on commit b30b712

Please sign in to comment.