Browse Source

Improve chat history (#12975)

Jude Melton-Houghton 1 year ago
parent
commit
2f9f0c0900
2 changed files with 103 additions and 43 deletions
  1. 83 39
      src/chat.cpp
  2. 20 4
      src/chat.h

+ 83 - 39
src/chat.cpp

@@ -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)
 {
-	m_line.insert(m_cursor, 1, ch);
+	makeLineRef().insert(m_cursor, 1, ch);
 	m_cursor++;
 	clampView();
 	m_nick_completion_start = 0;
@@ -465,7 +492,7 @@ void ChatPrompt::input(wchar_t ch)
 
 void ChatPrompt::input(const std::wstring &str)
 {
-	m_line.insert(m_cursor, str);
+	makeLineRef().insert(m_cursor, str);
 	m_cursor += str.size();
 	clampView();
 	m_nick_completion_start = 0;
@@ -474,22 +501,38 @@ void ChatPrompt::input(const std::wstring &str)
 
 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() &&
-			(m_history.size() == 0 || m_history.back() != line)) {
+			(m_history.size() == 0 || m_history.back().line != line)) {
+		HistoryEntry entry(line);
 		// Remove all duplicates
-		m_history.erase(std::remove(m_history.begin(), m_history.end(),
-			line), m_history.end());
+		m_history.erase(std::remove(m_history.begin(), m_history.end(), entry),
+				m_history.end());
 		// Push unique line
-		m_history.push_back(line);
+		m_history.push_back(std::move(entry));
 	}
 	if (m_history.size() > m_history_limit)
 		m_history.erase(m_history.begin());
 	m_history_index = m_history.size();
+	m_line = std::move(old_line);
 }
 
 void ChatPrompt::clear()
 {
-	m_line.clear();
+	makeLineRef().clear();
 	m_view = 0;
 	m_cursor = 0;
 	m_nick_completion_start = 0;
@@ -498,8 +541,8 @@ void ChatPrompt::clear()
 
 std::wstring ChatPrompt::replace(const std::wstring &line)
 {
-	std::wstring old_line = m_line;
-	m_line =  line;
+	std::wstring old_line = getLine();
+	makeLineRef() = line;
 	m_view = m_cursor = line.size();
 	clampView();
 	m_nick_completion_start = 0;
@@ -509,24 +552,23 @@ std::wstring ChatPrompt::replace(const std::wstring &line)
 
 void ChatPrompt::historyPrev()
 {
-	if (m_history_index != 0)
-	{
+	if (m_history_index != 0) {
 		--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()
 {
-	if (m_history_index + 1 >= m_history.size())
-	{
-		m_history_index = m_history.size();
-		replace(L"");
-	}
-	else
-	{
-		++m_history_index;
-		replace(m_history[m_history_index]);
+	if (m_history_index < m_history.size()) {
+		m_history_index++;
+		m_view = m_cursor = getLineRef().size();
+		clampView();
+		m_nick_completion_start = 0;
+		m_nick_completion_end = 0;
 	}
 }
 
@@ -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
 	//     interval where the originally used prefix was. Cycle
 	//     through the list of completions of that prefix.
+	const std::wstring &line = getLineRef();
 	u32 prefix_start = m_nick_completion_start;
 	u32 prefix_end = m_nick_completion_end;
 	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
 		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;
-		while (prefix_end < m_line.size() && !iswspace(m_line[prefix_end]))
+		while (prefix_end < line.size() && !iswspace(line[prefix_end]))
 			++prefix_end;
 		if (prefix_start == prefix_end)
 			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
 	std::vector<std::wstring> completions;
@@ -576,9 +619,9 @@ void ChatPrompt::nickCompletion(const std::list<std::string>& names, bool backwa
 	u32 replacement_index = 0;
 	if (!initial)
 	{
-		while (word_end < m_line.size() && !iswspace(m_line[word_end]))
+		while (word_end < line.size() && !iswspace(line[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
 		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];
-	if (word_end < m_line.size() && iswspace(m_line[word_end]))
+	if (word_end < line.size() && iswspace(line[word_end]))
 		++word_end;
 
 	// replace existing word with replacement word,
 	// 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();
 	clampView();
 	m_nick_completion_start = prefix_start;
@@ -616,7 +659,7 @@ void ChatPrompt::reformat(u32 cols)
 	}
 	else
 	{
-		s32 length = m_line.size();
+		s32 length = getLineRef().size();
 		bool was_at_end = (m_view + m_cols >= length + 1);
 		m_cols = cols - m_prompt.size();
 		if (was_at_end)
@@ -627,7 +670,7 @@ void ChatPrompt::reformat(u32 cols)
 
 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
@@ -640,7 +683,8 @@ void ChatPrompt::cursorOperation(CursorOp op, CursorOpDir dir, CursorOpScope sco
 	s32 old_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;
 
 	switch (scope) {
@@ -650,17 +694,17 @@ void ChatPrompt::cursorOperation(CursorOp op, CursorOpDir dir, CursorOpScope sco
 	case CURSOROP_SCOPE_WORD:
 		if (dir == CURSOROP_DIR_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++;
-			while (new_cursor < length && !iswspace(m_line[new_cursor]))
+			while (new_cursor < length && !iswspace(line[new_cursor]))
 				new_cursor++;
-			while (new_cursor < length && iswspace(m_line[new_cursor]))
+			while (new_cursor < length && iswspace(line[new_cursor]))
 				new_cursor++;
 		} else {
 			// 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--;
-			while (new_cursor >= 1 && !iswspace(m_line[new_cursor - 1]))
+			while (new_cursor >= 1 && !iswspace(line[new_cursor - 1]))
 				new_cursor--;
 		}
 		break;
@@ -680,10 +724,10 @@ void ChatPrompt::cursorOperation(CursorOp op, CursorOpDir dir, CursorOpScope sco
 		break;
 	case CURSOROP_DELETE:
 		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 {
 			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;
 		break;
@@ -707,7 +751,7 @@ void ChatPrompt::cursorOperation(CursorOp op, CursorOpDir dir, CursorOpScope sco
 
 void ChatPrompt::clampView()
 {
-	s32 length = m_line.size();
+	s32 length = getLineRef().size();
 	if (length + 1 <= m_cols)
 	{
 		m_view = 0;

+ 20 - 4
src/chat.h

@@ -25,6 +25,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 
 #include "irrlichttypes.h"
 #include "util/enriched_string.h"
+#include "util/Optional.h"
 #include "settings.h"
 
 // Chat console related classes
@@ -172,10 +173,10 @@ public:
 	void addToHistory(const std::wstring &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
-	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
 	void clear();
@@ -233,18 +234,33 @@ public:
 	void cursorOperation(CursorOp op, CursorOpDir dir, CursorOpScope scope);
 
 protected:
+	const std::wstring &getLineRef() const;
+
+	std::wstring &makeLineRef();
+
 	// 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
 	// else, also ensure m_view <= m_line.size() + 1 - m_cols
 	void clampView();
 
 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
 	std::wstring m_prompt = L"";
-	// Currently edited line
+	// Non-historical edited line
 	std::wstring m_line = L"";
 	// History buffer
-	std::vector<std::wstring> m_history;
+	std::vector<HistoryEntry> m_history;
 	// History index (0 <= m_history_index <= m_history.size())
 	u32 m_history_index = 0;
 	// Maximum number of history entries