From c14b338a923fc1a407b217975df83231fb4389e5 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Thu, 12 Mar 2026 06:38:10 -0700 Subject: [PATCH] feat: add Tab autocomplete for slash commands in chat input MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pressing Tab while typing a slash command cycles through all matching commands (e.g. /em → /emote, /emote → /emote again). Unambiguous matches append a trailing space. Repeated Tab presses cycle forward through all matches. History navigation (Up/Down) resets the autocomplete session. --- include/ui/game_screen.hpp | 5 +++ src/ui/game_screen.cpp | 65 +++++++++++++++++++++++++++++++++++++- 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index b7cf2f53..b07a2d2d 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -54,6 +54,11 @@ private: std::vector chatSentHistory_; int chatHistoryIdx_ = -1; // -1 = not browsing history + // Tab-completion state for slash commands + std::string chatTabPrefix_; // prefix captured on first Tab press + std::vector chatTabMatches_; // matching command list + int chatTabMatchIdx_ = -1; // active match index (-1 = inactive) + // Chat tabs int activeChatTab_ = 0; struct ChatTab { diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index b4b2f299..a119f1e8 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -1770,8 +1770,70 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) { self->chatInputMoveCursorToEnd = false; } + // Tab: slash-command autocomplete + if (data->EventFlag == ImGuiInputTextFlags_CallbackCompletion) { + if (data->BufTextLen > 0 && data->Buf[0] == '/') { + // Split buffer into command word and trailing args + std::string fullBuf(data->Buf, data->BufTextLen); + size_t spacePos = fullBuf.find(' '); + std::string word = (spacePos != std::string::npos) ? fullBuf.substr(0, spacePos) : fullBuf; + std::string rest = (spacePos != std::string::npos) ? fullBuf.substr(spacePos) : ""; + + // Normalize to lowercase for matching + std::string lowerWord = word; + for (auto& ch : lowerWord) ch = static_cast(std::tolower(static_cast(ch))); + + static const std::vector kCmds = { + "/afk", "/away", "/cast", "/chathelp", "/clear", + "/dance", "/do", "/dnd", "/e", "/emote", + "/follow", "/g", "/guild", "/guildinfo", + "/gmticket", "/grouploot", "/i", "/instance", + "/invite", "/j", "/join", "/kick", + "/l", "/leave", "/local", "/me", + "/p", "/party", "/r", "/raid", + "/raidwarning", "/random", "/reply", "/roll", + "/s", "/say", "/setloot", "/shout", + "/stopattack", "/stopfollow", "/t", "/time", + "/trade", "/uninvite", "/w", "/whisper", + "/who", "/wts", "/wtb", "/y", "/yell", "/zone" + }; + + // New session if prefix changed + if (self->chatTabMatchIdx_ < 0 || self->chatTabPrefix_ != lowerWord) { + self->chatTabPrefix_ = lowerWord; + self->chatTabMatches_.clear(); + for (const auto& cmd : kCmds) { + if (cmd.size() >= lowerWord.size() && + cmd.compare(0, lowerWord.size(), lowerWord) == 0) + self->chatTabMatches_.push_back(cmd); + } + self->chatTabMatchIdx_ = 0; + } else { + // Cycle forward through matches + ++self->chatTabMatchIdx_; + if (self->chatTabMatchIdx_ >= static_cast(self->chatTabMatches_.size())) + self->chatTabMatchIdx_ = 0; + } + + if (!self->chatTabMatches_.empty()) { + std::string match = self->chatTabMatches_[self->chatTabMatchIdx_]; + // Append trailing space when match is unambiguous + if (self->chatTabMatches_.size() == 1 && rest.empty()) + match += ' '; + std::string newBuf = match + rest; + data->DeleteChars(0, data->BufTextLen); + data->InsertChars(0, newBuf.c_str()); + } + } + return 0; + } + // Up/Down arrow: cycle through sent message history if (data->EventFlag == ImGuiInputTextFlags_CallbackHistory) { + // Any history navigation resets autocomplete + self->chatTabMatchIdx_ = -1; + self->chatTabMatches_.clear(); + const int histSize = static_cast(self->chatSentHistory_.size()); if (histSize == 0) return 0; @@ -1802,7 +1864,8 @@ void GameScreen::renderChatWindow(game::GameHandler& gameHandler) { ImGuiInputTextFlags inputFlags = ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_CallbackAlways | - ImGuiInputTextFlags_CallbackHistory; + ImGuiInputTextFlags_CallbackHistory | + ImGuiInputTextFlags_CallbackCompletion; if (ImGui::InputText("##ChatInput", chatInputBuffer, sizeof(chatInputBuffer), inputFlags, inputCallback, this)) { sendChatMessage(gameHandler); // Close chat input on send so movement keys work immediately.