Browse Source

Lua: Add basic autocomplete in the console

pull/6773/head
Gleb Mazovetskiy 2 years ago
parent
commit
b30b712cbb
  1. 3
      Source/CMakeLists.txt
  2. 15
      Source/engine/render/text_render.cpp
  3. 3
      Source/engine/render/text_render.hpp
  4. 112
      Source/lua/autocomplete.cpp
  5. 44
      Source/lua/autocomplete.hpp
  6. 5
      Source/lua/lua.hpp
  7. 16
      Source/lua/repl.cpp
  8. 3
      Source/lua/repl.hpp
  9. 122
      Source/panels/console.cpp

3
Source/CMakeLists.txt

@ -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
@ -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)

15
Source/engine/render/text_render.cpp

@ -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;
@ -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;
}
}
};

3
Source/engine/render/text_render.hpp

@ -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;
};
/**

112
Source/lua/autocomplete.cpp

@ -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
Source/lua/autocomplete.hpp

@ -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
Source/lua/lua.hpp

@ -3,10 +3,7 @@
#include <string_view>
#include <expected.hpp>
namespace sol {
class state;
} // namespace sol
#include <sol/forward.hpp>
namespace devilution {

16
Source/lua/repl.cpp

@ -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.
@ -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);

3
Source/lua/repl.hpp

@ -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

122
Source/panels/console.cpp

@ -22,6 +22,7 @@
#include "engine/render/text_render.hpp"
#include "engine/size.hpp"
#include "engine/surface.hpp"
#include "lua/autocomplete.hpp"
#include "lua/repl.hpp"
#include "utils/algorithm/container.hpp"
#include "utils/sdl_geometry.h"
@ -54,6 +55,10 @@ TextInputState ConsoleInputState {
};
bool InputTextChanged = false;
std::string WrappedInputText { Prompt };
std::vector<LuaAutocompleteSuggestion> AutocompleteSuggestions;
int AutocompleteSuggestionsMaxWidth = -1;
int AutocompleteSuggestionFocusIndex = -1;
constexpr size_t MaxSuggestions = 12;
struct ConsoleLine {
enum Type : uint8_t {
@ -103,9 +108,12 @@ constexpr UiFlags InputTextUiFlags = TextUiFlags | UiFlags::ColorDialogWhite;
constexpr UiFlags OutputTextUiFlags = TextUiFlags | UiFlags::ColorDialogWhite;
constexpr UiFlags WarningTextUiFlags = TextUiFlags | UiFlags::ColorDialogYellow;
constexpr UiFlags ErrorTextUiFlags = TextUiFlags | UiFlags::ColorDialogRed;
constexpr UiFlags AutocompleteSuggestionsTextUiFlags = TextUiFlags | UiFlags::ColorDialogWhite;
constexpr UiFlags AutocompleteSuggestionsFocusedTextUiFlags = TextUiFlags | UiFlags::ColorDialogYellow;
constexpr int TextSpacing = 0;
constexpr GameFontTables TextFontSize = GetFontSizeFromUiFlags(InputTextUiFlags);
constexpr GameFontTables AutocompleteSuggestionsTextFontSize = GetFontSizeFromUiFlags(AutocompleteSuggestionsTextUiFlags);
// Scroll offset from the bottom (in pages), to be applied on next render.
int PendingScrollPages;
@ -156,11 +164,53 @@ void SendInput()
HistoryIndex = -1;
}
void DrawInputText(const Surface &inputTextSurface, std::string_view originalInputText, std::string_view wrappedInputText)
void DrawAutocompleteSuggestions(const Surface &out, const std::vector<LuaAutocompleteSuggestion> &suggestions, Point position)
{
if (AutocompleteSuggestionsMaxWidth == -1) {
int maxWidth = 0;
for (const LuaAutocompleteSuggestion &suggestion : suggestions) {
maxWidth = std::max(maxWidth, GetLineWidth(suggestion.displayText, AutocompleteSuggestionsTextFontSize, TextSpacing));
}
AutocompleteSuggestionsMaxWidth = maxWidth;
}
const int outerWidth = AutocompleteSuggestionsMaxWidth + TextPaddingX * 2;
if (position.x + outerWidth > out.w()) {
position.x -= AutocompleteSuggestionsMaxWidth;
}
const int height = static_cast<int>(suggestions.size()) * LineHeight + TextPaddingYBottom + TextPaddingYTop;
position.y -= height;
FillRect(out, position.x, position.y, outerWidth, height, PAL16_BLUE + 14);
size_t i = 0;
Point textPosition { position.x + TextPaddingX, position.y + TextPaddingYTop };
for (const LuaAutocompleteSuggestion &suggestion : suggestions) {
if (static_cast<int>(i) == AutocompleteSuggestionFocusIndex) {
const int extraTop = i == 0 ? TextPaddingYTop : 0;
const int extraHeight = extraTop + TextPaddingYBottom;
FillRect(out, position.x, textPosition.y - extraTop, outerWidth, LineHeight + extraHeight, PAL16_BLUE + 8);
}
DrawString(out, suggestion.displayText, textPosition,
TextRenderOptions {
.flags = AutocompleteSuggestionsTextUiFlags,
.spacing = TextSpacing,
});
textPosition.y += LineHeight;
++i;
}
}
void DrawInputText(const Surface &out,
Rectangle rect, std::string_view originalInputText, std::string_view wrappedInputText)
{
int lineY = 0;
int numRendered = -static_cast<int>(Prompt.size());
bool prevIsOriginalNewline = false;
const Surface inputTextSurface = out.subregion(rect.position.x, rect.position.y, rect.size.width, rect.size.height);
std::optional<Point> renderedCursorPositionOut;
for (const std::string_view line : SplitByChar(wrappedInputText, '\n')) {
const int lineCursorPosition = static_cast<int>(ConsoleInputCursor.position) - numRendered;
const bool isCursorOnPrevLine = lineCursorPosition == 0 && !prevIsOriginalNewline && numRendered > 0;
@ -171,7 +221,7 @@ void DrawInputText(const Surface &inputTextSurface, std::string_view originalInp
.spacing = TextSpacing,
.cursorPosition = isCursorOnPrevLine ? -1 : lineCursorPosition,
.highlightRange = { static_cast<int>(ConsoleInputCursor.selection.begin) - numRendered, static_cast<int>(ConsoleInputCursor.selection.end) - numRendered },
});
.renderedCursorPositionOut = &renderedCursorPositionOut });
lineY += LineHeight;
numRendered += static_cast<int>(line.size());
prevIsOriginalNewline = static_cast<size_t>(numRendered) < originalInputText.size()
@ -179,6 +229,13 @@ void DrawInputText(const Surface &inputTextSurface, std::string_view originalInp
if (prevIsOriginalNewline)
++numRendered;
}
if (!AutocompleteSuggestions.empty() && renderedCursorPositionOut.has_value()) {
Point position = *renderedCursorPositionOut;
position.x += rect.position.x;
position.y += rect.position.y;
DrawAutocompleteSuggestions(out, AutocompleteSuggestions, position);
}
}
void DrawConsoleLines(const Surface &out)
@ -357,6 +414,15 @@ void OpenConsole()
FirstRender = true;
}
void AcceptSuggestion()
{
const LuaAutocompleteSuggestion &suggestion = AutocompleteSuggestions[AutocompleteSuggestionFocusIndex];
ConsoleInputState.type(suggestion.completionText);
if (suggestion.cursorAdjust == -1) {
ConsoleInputState.moveCursorLeft(/*word=*/false);
}
}
bool ConsoleHandleEvent(const SDL_Event &event)
{
if (!IsConsoleVisible) {
@ -377,20 +443,46 @@ bool ConsoleHandleEvent(const SDL_Event &event)
case SDL_KEYDOWN:
switch (event.key.keysym.sym) {
case SDLK_ESCAPE:
CloseConsole();
if (!AutocompleteSuggestions.empty()) {
AutocompleteSuggestions.clear();
AutocompleteSuggestionFocusIndex = -1;
} else {
CloseConsole();
}
return true;
case SDLK_UP:
isShift ? PrevOutput() : PrevInput();
if (AutocompleteSuggestionFocusIndex != -1) {
AutocompleteSuggestionFocusIndex = std::max(
0, AutocompleteSuggestionFocusIndex - 1);
} else {
isShift ? PrevOutput() : PrevInput();
}
return true;
case SDLK_DOWN:
isShift ? NextOutput() : NextInput();
if (AutocompleteSuggestionFocusIndex != -1) {
AutocompleteSuggestionFocusIndex = std::min(
static_cast<int>(AutocompleteSuggestions.size()) - 1,
AutocompleteSuggestionFocusIndex + 1);
} else {
isShift ? NextOutput() : NextInput();
}
return true;
case SDLK_TAB:
if (AutocompleteSuggestionFocusIndex != -1) {
AcceptSuggestion();
InputTextChanged = true;
}
return true;
case SDLK_RETURN:
case SDLK_KP_ENTER:
if (isShift) {
ConsoleInputState.type("\n");
} else {
SendInput();
if (AutocompleteSuggestionFocusIndex != -1) {
AcceptSuggestion();
} else {
SendInput();
}
}
InputTextChanged = true;
return true;
@ -445,6 +537,9 @@ void DrawConsole(const Surface &out)
const std::string_view originalInputText = ConsoleInputState.value();
if (InputTextChanged) {
WrappedInputText = WordWrapString(StrCat(Prompt, originalInputText), OuterRect.size.width - 2 * TextPaddingX, TextFontSize, TextSpacing);
GetLuaAutocompleteSuggestions(originalInputText.substr(0, ConsoleInputCursor.position), GetLuaReplEnvironment(), /*maxSuggestions=*/MaxSuggestions, AutocompleteSuggestions);
AutocompleteSuggestionsMaxWidth = -1;
AutocompleteSuggestionFocusIndex = AutocompleteSuggestions.empty() ? -1 : 0;
InputTextChanged = false;
}
@ -481,13 +576,14 @@ void DrawConsole(const Surface &out)
DrawHorizontalLine(out, InputRect.position - Displacement { 0, 1 }, InputRect.size.width, BorderColor);
DrawInputText(
out.subregion(
inputTextRect.position.x,
inputTextRect.position.y,
// Extra space for the cursor on the right:
inputTextRect.size.width + TextPaddingX,
// Extra space for letters like g.
inputTextRect.size.height + TextPaddingYBottom),
out,
Rectangle(
inputTextRect.position,
Size {
// Extra space for the cursor on the right:
inputTextRect.size.width + TextPaddingX,
// Extra space for letters like g.
inputTextRect.size.height + TextPaddingYBottom }),
originalInputText,
WrappedInputText);

Loading…
Cancel
Save