Improve chat history (#12975)

This commit is contained in:
Jude Melton-Houghton 2023-01-14 16:14:37 -05:00 committed by GitHub
parent 8fded9d990
commit 2f9f0c0900
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 103 additions and 43 deletions

@ -454,9 +454,36 @@ ChatPrompt::ChatPrompt(const std::wstring &prompt, u32 history_limit):
{ {
} }
const std::wstring &ChatPrompt::getLineRef() const
{
return m_history_index >= m_history.size() ? m_line : m_history[m_history_index].line;
}
std::wstring &ChatPrompt::makeLineRef()
{
if (m_history_index >= m_history.size()) {
return m_line;
} else {
if (!m_history[m_history_index].saved)
m_history[m_history_index].saved = m_history[m_history_index].line;
return m_history[m_history_index].line;
}
}
bool ChatPrompt::HistoryEntry::operator==(const ChatPrompt::HistoryEntry &other)
{
if (line != other.line)
return false;
if (saved == other.saved)
return true;
if ((!saved || saved == line) && (!other.saved || other.saved == other.line))
return true;
return false;
}
void ChatPrompt::input(wchar_t ch) void ChatPrompt::input(wchar_t ch)
{ {
m_line.insert(m_cursor, 1, ch); makeLineRef().insert(m_cursor, 1, ch);
m_cursor++; m_cursor++;
clampView(); clampView();
m_nick_completion_start = 0; m_nick_completion_start = 0;
@ -465,7 +492,7 @@ void ChatPrompt::input(wchar_t ch)
void ChatPrompt::input(const std::wstring &str) void ChatPrompt::input(const std::wstring &str)
{ {
m_line.insert(m_cursor, str); makeLineRef().insert(m_cursor, str);
m_cursor += str.size(); m_cursor += str.size();
clampView(); clampView();
m_nick_completion_start = 0; m_nick_completion_start = 0;
@ -474,22 +501,38 @@ void ChatPrompt::input(const std::wstring &str)
void ChatPrompt::addToHistory(const std::wstring &line) void ChatPrompt::addToHistory(const std::wstring &line)
{ {
std::wstring old_line = getLine();
if (m_history_index < m_history.size()) {
auto entry = m_history.begin() + m_history_index;
if (entry->saved && entry->line == line) {
entry->line = *entry->saved;
entry->saved = nullopt;
// Remove potential duplicates
auto dup_before = std::find(m_history.begin(), entry, *entry);
if (dup_before != entry)
m_history.erase(dup_before);
else if (std::find(entry + 1, m_history.end(), *entry) != m_history.end())
m_history.erase(entry);
}
}
if (!line.empty() && if (!line.empty() &&
(m_history.size() == 0 || m_history.back() != line)) { (m_history.size() == 0 || m_history.back().line != line)) {
HistoryEntry entry(line);
// Remove all duplicates // Remove all duplicates
m_history.erase(std::remove(m_history.begin(), m_history.end(), m_history.erase(std::remove(m_history.begin(), m_history.end(), entry),
line), m_history.end()); m_history.end());
// Push unique line // Push unique line
m_history.push_back(line); m_history.push_back(std::move(entry));
} }
if (m_history.size() > m_history_limit) if (m_history.size() > m_history_limit)
m_history.erase(m_history.begin()); m_history.erase(m_history.begin());
m_history_index = m_history.size(); m_history_index = m_history.size();
m_line = std::move(old_line);
} }
void ChatPrompt::clear() void ChatPrompt::clear()
{ {
m_line.clear(); makeLineRef().clear();
m_view = 0; m_view = 0;
m_cursor = 0; m_cursor = 0;
m_nick_completion_start = 0; m_nick_completion_start = 0;
@ -498,8 +541,8 @@ void ChatPrompt::clear()
std::wstring ChatPrompt::replace(const std::wstring &line) std::wstring ChatPrompt::replace(const std::wstring &line)
{ {
std::wstring old_line = m_line; std::wstring old_line = getLine();
m_line = line; makeLineRef() = line;
m_view = m_cursor = line.size(); m_view = m_cursor = line.size();
clampView(); clampView();
m_nick_completion_start = 0; m_nick_completion_start = 0;
@ -509,24 +552,23 @@ std::wstring ChatPrompt::replace(const std::wstring &line)
void ChatPrompt::historyPrev() void ChatPrompt::historyPrev()
{ {
if (m_history_index != 0) if (m_history_index != 0) {
{
--m_history_index; --m_history_index;
replace(m_history[m_history_index]); m_view = m_cursor = getLineRef().size();
clampView();
m_nick_completion_start = 0;
m_nick_completion_end = 0;
} }
} }
void ChatPrompt::historyNext() void ChatPrompt::historyNext()
{ {
if (m_history_index + 1 >= m_history.size()) if (m_history_index < m_history.size()) {
{ m_history_index++;
m_history_index = m_history.size(); m_view = m_cursor = getLineRef().size();
replace(L""); clampView();
} m_nick_completion_start = 0;
else m_nick_completion_end = 0;
{
++m_history_index;
replace(m_history[m_history_index]);
} }
} }
@ -541,6 +583,7 @@ void ChatPrompt::nickCompletion(const std::list<std::string>& names, bool backwa
// m_nick_completion_start..m_nick_completion_end are the // m_nick_completion_start..m_nick_completion_end are the
// interval where the originally used prefix was. Cycle // interval where the originally used prefix was. Cycle
// through the list of completions of that prefix. // through the list of completions of that prefix.
const std::wstring &line = getLineRef();
u32 prefix_start = m_nick_completion_start; u32 prefix_start = m_nick_completion_start;
u32 prefix_end = m_nick_completion_end; u32 prefix_end = m_nick_completion_end;
bool initial = (prefix_end == 0); bool initial = (prefix_end == 0);
@ -548,14 +591,14 @@ void ChatPrompt::nickCompletion(const std::list<std::string>& names, bool backwa
{ {
// no previous nick completion is active // no previous nick completion is active
prefix_start = prefix_end = m_cursor; prefix_start = prefix_end = m_cursor;
while (prefix_start > 0 && !iswspace(m_line[prefix_start-1])) while (prefix_start > 0 && !iswspace(line[prefix_start-1]))
--prefix_start; --prefix_start;
while (prefix_end < m_line.size() && !iswspace(m_line[prefix_end])) while (prefix_end < line.size() && !iswspace(line[prefix_end]))
++prefix_end; ++prefix_end;
if (prefix_start == prefix_end) if (prefix_start == prefix_end)
return; return;
} }
std::wstring prefix = m_line.substr(prefix_start, prefix_end - prefix_start); std::wstring prefix = line.substr(prefix_start, prefix_end - prefix_start);
// find all names that start with the selected prefix // find all names that start with the selected prefix
std::vector<std::wstring> completions; std::vector<std::wstring> completions;
@ -576,9 +619,9 @@ void ChatPrompt::nickCompletion(const std::list<std::string>& names, bool backwa
u32 replacement_index = 0; u32 replacement_index = 0;
if (!initial) if (!initial)
{ {
while (word_end < m_line.size() && !iswspace(m_line[word_end])) while (word_end < line.size() && !iswspace(line[word_end]))
++word_end; ++word_end;
std::wstring word = m_line.substr(prefix_start, word_end - prefix_start); std::wstring word = line.substr(prefix_start, word_end - prefix_start);
// cycle through completions // cycle through completions
for (u32 i = 0; i < completions.size(); ++i) for (u32 i = 0; i < completions.size(); ++i)
@ -595,12 +638,12 @@ void ChatPrompt::nickCompletion(const std::list<std::string>& names, bool backwa
} }
} }
std::wstring replacement = completions[replacement_index]; std::wstring replacement = completions[replacement_index];
if (word_end < m_line.size() && iswspace(m_line[word_end])) if (word_end < line.size() && iswspace(line[word_end]))
++word_end; ++word_end;
// replace existing word with replacement word, // replace existing word with replacement word,
// place the cursor at the end and record the completion prefix // place the cursor at the end and record the completion prefix
m_line.replace(prefix_start, word_end - prefix_start, replacement); makeLineRef().replace(prefix_start, word_end - prefix_start, replacement);
m_cursor = prefix_start + replacement.size(); m_cursor = prefix_start + replacement.size();
clampView(); clampView();
m_nick_completion_start = prefix_start; m_nick_completion_start = prefix_start;
@ -616,7 +659,7 @@ void ChatPrompt::reformat(u32 cols)
} }
else else
{ {
s32 length = m_line.size(); s32 length = getLineRef().size();
bool was_at_end = (m_view + m_cols >= length + 1); bool was_at_end = (m_view + m_cols >= length + 1);
m_cols = cols - m_prompt.size(); m_cols = cols - m_prompt.size();
if (was_at_end) if (was_at_end)
@ -627,7 +670,7 @@ void ChatPrompt::reformat(u32 cols)
std::wstring ChatPrompt::getVisiblePortion() const std::wstring ChatPrompt::getVisiblePortion() const
{ {
return m_prompt + m_line.substr(m_view, m_cols); return m_prompt + getLineRef().substr(m_view, m_cols);
} }
s32 ChatPrompt::getVisibleCursorPosition() const s32 ChatPrompt::getVisibleCursorPosition() const
@ -640,7 +683,8 @@ void ChatPrompt::cursorOperation(CursorOp op, CursorOpDir dir, CursorOpScope sco
s32 old_cursor = m_cursor; s32 old_cursor = m_cursor;
s32 new_cursor = m_cursor; s32 new_cursor = m_cursor;
s32 length = m_line.size(); const std::wstring &line = getLineRef();
s32 length = line.size();
s32 increment = (dir == CURSOROP_DIR_RIGHT) ? 1 : -1; s32 increment = (dir == CURSOROP_DIR_RIGHT) ? 1 : -1;
switch (scope) { switch (scope) {
@ -650,17 +694,17 @@ void ChatPrompt::cursorOperation(CursorOp op, CursorOpDir dir, CursorOpScope sco
case CURSOROP_SCOPE_WORD: case CURSOROP_SCOPE_WORD:
if (dir == CURSOROP_DIR_RIGHT) { if (dir == CURSOROP_DIR_RIGHT) {
// skip one word to the right // skip one word to the right
while (new_cursor < length && iswspace(m_line[new_cursor])) while (new_cursor < length && iswspace(line[new_cursor]))
new_cursor++; new_cursor++;
while (new_cursor < length && !iswspace(m_line[new_cursor])) while (new_cursor < length && !iswspace(line[new_cursor]))
new_cursor++; new_cursor++;
while (new_cursor < length && iswspace(m_line[new_cursor])) while (new_cursor < length && iswspace(line[new_cursor]))
new_cursor++; new_cursor++;
} else { } else {
// skip one word to the left // skip one word to the left
while (new_cursor >= 1 && iswspace(m_line[new_cursor - 1])) while (new_cursor >= 1 && iswspace(line[new_cursor - 1]))
new_cursor--; new_cursor--;
while (new_cursor >= 1 && !iswspace(m_line[new_cursor - 1])) while (new_cursor >= 1 && !iswspace(line[new_cursor - 1]))
new_cursor--; new_cursor--;
} }
break; break;
@ -680,10 +724,10 @@ void ChatPrompt::cursorOperation(CursorOp op, CursorOpDir dir, CursorOpScope sco
break; break;
case CURSOROP_DELETE: case CURSOROP_DELETE:
if (m_cursor_len > 0) { // Delete selected text first if (m_cursor_len > 0) { // Delete selected text first
m_line.erase(m_cursor, m_cursor_len); makeLineRef().erase(m_cursor, m_cursor_len);
} else { } else {
m_cursor = MYMIN(new_cursor, old_cursor); m_cursor = MYMIN(new_cursor, old_cursor);
m_line.erase(m_cursor, abs(new_cursor - old_cursor)); makeLineRef().erase(m_cursor, abs(new_cursor - old_cursor));
} }
m_cursor_len = 0; m_cursor_len = 0;
break; break;
@ -707,7 +751,7 @@ void ChatPrompt::cursorOperation(CursorOp op, CursorOpDir dir, CursorOpScope sco
void ChatPrompt::clampView() void ChatPrompt::clampView()
{ {
s32 length = m_line.size(); s32 length = getLineRef().size();
if (length + 1 <= m_cols) if (length + 1 <= m_cols)
{ {
m_view = 0; m_view = 0;

@ -25,6 +25,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "irrlichttypes.h" #include "irrlichttypes.h"
#include "util/enriched_string.h" #include "util/enriched_string.h"
#include "util/Optional.h"
#include "settings.h" #include "settings.h"
// Chat console related classes // Chat console related classes
@ -172,10 +173,10 @@ public:
void addToHistory(const std::wstring &line); void addToHistory(const std::wstring &line);
// Get current line // Get current line
std::wstring getLine() const { return m_line; } std::wstring getLine() const { return getLineRef(); }
// Get section of line that is currently selected // Get section of line that is currently selected
std::wstring getSelection() const { return m_line.substr(m_cursor, m_cursor_len); } std::wstring getSelection() const { return getLineRef().substr(m_cursor, m_cursor_len); }
// Clear the current line // Clear the current line
void clear(); void clear();
@ -233,18 +234,33 @@ public:
void cursorOperation(CursorOp op, CursorOpDir dir, CursorOpScope scope); void cursorOperation(CursorOp op, CursorOpDir dir, CursorOpScope scope);
protected: protected:
const std::wstring &getLineRef() const;
std::wstring &makeLineRef();
// set m_view to ensure that 0 <= m_view <= m_cursor < m_view + m_cols // set m_view to ensure that 0 <= m_view <= m_cursor < m_view + m_cols
// if line can be fully shown, set m_view to zero // if line can be fully shown, set m_view to zero
// else, also ensure m_view <= m_line.size() + 1 - m_cols // else, also ensure m_view <= m_line.size() + 1 - m_cols
void clampView(); void clampView();
private: private:
struct HistoryEntry {
std::wstring line;
// If line is edited, saved holds the unedited version.
Optional<std::wstring> saved;
HistoryEntry(const std::wstring &line): line(line) {}
bool operator==(const HistoryEntry &other);
bool operator!=(const HistoryEntry &other) { return !(*this == other); }
};
// Prompt prefix // Prompt prefix
std::wstring m_prompt = L""; std::wstring m_prompt = L"";
// Currently edited line // Non-historical edited line
std::wstring m_line = L""; std::wstring m_line = L"";
// History buffer // History buffer
std::vector<std::wstring> m_history; std::vector<HistoryEntry> m_history;
// History index (0 <= m_history_index <= m_history.size()) // History index (0 <= m_history_index <= m_history.size())
u32 m_history_index = 0; u32 m_history_index = 0;
// Maximum number of history entries // Maximum number of history entries