]> git.lizzy.rs Git - minetest.git/blob - src/chat.cpp
Improve chat history (#12975)
[minetest.git] / src / chat.cpp
1 /*
2 Minetest
3 Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
4
5 This program is free software; you can redistribute it and/or modify
6 it under the terms of the GNU Lesser General Public License as published by
7 the Free Software Foundation; either version 2.1 of the License, or
8 (at your option) any later version.
9
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 GNU Lesser General Public License for more details.
14
15 You should have received a copy of the GNU Lesser General Public License along
16 with this program; if not, write to the Free Software Foundation, Inc.,
17 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18 */
19
20 #include "chat.h"
21
22 #include <algorithm>
23 #include <cctype>
24 #include <sstream>
25
26 #include "config.h"
27 #include "debug.h"
28 #include "util/strfnd.h"
29 #include "util/string.h"
30 #include "util/numeric.h"
31
32 ChatBuffer::ChatBuffer(u32 scrollback):
33         m_scrollback(scrollback)
34 {
35         if (m_scrollback == 0)
36                 m_scrollback = 1;
37         m_empty_formatted_line.first = true;
38
39         m_cache_clickable_chat_weblinks = false;
40         // Curses mode cannot access g_settings here
41         if (g_settings != nullptr) {
42                 m_cache_clickable_chat_weblinks = g_settings->getBool("clickable_chat_weblinks");
43                 if (m_cache_clickable_chat_weblinks) {
44                         std::string colorval = g_settings->get("chat_weblink_color");
45                         parseColorString(colorval, m_cache_chat_weblink_color, false, 255);
46                         m_cache_chat_weblink_color.setAlpha(255);
47                 }
48         }
49 }
50
51 void ChatBuffer::addLine(const std::wstring &name, const std::wstring &text)
52 {
53         m_lines_modified = true;
54
55         ChatLine line(name, text);
56         m_unformatted.push_back(line);
57
58         if (m_rows > 0) {
59                 // m_formatted is valid and must be kept valid
60                 bool scrolled_at_bottom = (m_scroll == getBottomScrollPos());
61                 u32 num_added = formatChatLine(line, m_cols, m_formatted);
62                 if (scrolled_at_bottom)
63                         m_scroll += num_added;
64         }
65
66         // Limit number of lines by m_scrollback
67         if (m_unformatted.size() > m_scrollback) {
68                 deleteOldest(m_unformatted.size() - m_scrollback);
69         }
70 }
71
72 void ChatBuffer::clear()
73 {
74         m_unformatted.clear();
75         m_formatted.clear();
76         m_scroll = 0;
77         m_lines_modified = true;
78 }
79
80 u32 ChatBuffer::getLineCount() const
81 {
82         return m_unformatted.size();
83 }
84
85 const ChatLine& ChatBuffer::getLine(u32 index) const
86 {
87         assert(index < getLineCount()); // pre-condition
88         return m_unformatted[index];
89 }
90
91 void ChatBuffer::step(f32 dtime)
92 {
93         for (ChatLine &line : m_unformatted) {
94                 line.age += dtime;
95         }
96 }
97
98 void ChatBuffer::deleteOldest(u32 count)
99 {
100         bool at_bottom = (m_scroll == getBottomScrollPos());
101
102         u32 del_unformatted = 0;
103         u32 del_formatted = 0;
104
105         while (count > 0 && del_unformatted < m_unformatted.size()) {
106                 ++del_unformatted;
107
108                 // keep m_formatted in sync
109                 if (del_formatted < m_formatted.size()) {
110                         sanity_check(m_formatted[del_formatted].first);
111                         ++del_formatted;
112                         while (del_formatted < m_formatted.size() &&
113                                         !m_formatted[del_formatted].first)
114                                 ++del_formatted;
115                 }
116
117                 --count;
118         }
119
120         m_unformatted.erase(m_unformatted.begin(), m_unformatted.begin() + del_unformatted);
121         m_formatted.erase(m_formatted.begin(), m_formatted.begin() + del_formatted);
122
123         if (del_unformatted > 0)
124                 m_lines_modified = true;
125
126         if (at_bottom)
127                 m_scroll = getBottomScrollPos();
128         else
129                 scrollAbsolute(m_scroll - del_formatted);
130 }
131
132 void ChatBuffer::deleteByAge(f32 maxAge)
133 {
134         u32 count = 0;
135         while (count < m_unformatted.size() && m_unformatted[count].age > maxAge)
136                 ++count;
137         deleteOldest(count);
138 }
139
140 u32 ChatBuffer::getRows() const
141 {
142         return m_rows;
143 }
144
145 void ChatBuffer::reformat(u32 cols, u32 rows)
146 {
147         if (cols == 0 || rows == 0)
148         {
149                 // Clear formatted buffer
150                 m_cols = 0;
151                 m_rows = 0;
152                 m_scroll = 0;
153                 m_formatted.clear();
154         }
155         else if (cols != m_cols || rows != m_rows)
156         {
157                 // TODO: Avoid reformatting ALL lines (even invisible ones)
158                 // each time the console size changes.
159
160                 // Find out the scroll position in *unformatted* lines
161                 u32 restore_scroll_unformatted = 0;
162                 u32 restore_scroll_formatted = 0;
163                 bool at_bottom = (m_scroll == getBottomScrollPos());
164                 if (!at_bottom)
165                 {
166                         for (s32 i = 0; i < m_scroll; ++i)
167                         {
168                                 if (m_formatted[i].first)
169                                         ++restore_scroll_unformatted;
170                         }
171                 }
172
173                 // If number of columns change, reformat everything
174                 if (cols != m_cols)
175                 {
176                         m_formatted.clear();
177                         for (u32 i = 0; i < m_unformatted.size(); ++i)
178                         {
179                                 if (i == restore_scroll_unformatted)
180                                         restore_scroll_formatted = m_formatted.size();
181                                 formatChatLine(m_unformatted[i], cols, m_formatted);
182                         }
183                 }
184
185                 // Update the console size
186                 m_cols = cols;
187                 m_rows = rows;
188
189                 // Restore the scroll position
190                 if (at_bottom)
191                 {
192                         scrollBottom();
193                 }
194                 else
195                 {
196                         scrollAbsolute(restore_scroll_formatted);
197                 }
198         }
199 }
200
201 const ChatFormattedLine& ChatBuffer::getFormattedLine(u32 row) const
202 {
203         s32 index = m_scroll + (s32) row;
204         if (index >= 0 && index < (s32) m_formatted.size())
205                 return m_formatted[index];
206
207         return m_empty_formatted_line;
208 }
209
210 void ChatBuffer::scroll(s32 rows)
211 {
212         scrollAbsolute(m_scroll + rows);
213 }
214
215 void ChatBuffer::scrollAbsolute(s32 scroll)
216 {
217         s32 top = getTopScrollPos();
218         s32 bottom = getBottomScrollPos();
219
220         m_scroll = scroll;
221         if (m_scroll < top)
222                 m_scroll = top;
223         if (m_scroll > bottom)
224                 m_scroll = bottom;
225 }
226
227 void ChatBuffer::scrollBottom()
228 {
229         m_scroll = getBottomScrollPos();
230 }
231
232 u32 ChatBuffer::formatChatLine(const ChatLine& line, u32 cols,
233                 std::vector<ChatFormattedLine>& destination) const
234 {
235         u32 num_added = 0;
236         std::vector<ChatFormattedFragment> next_frags;
237         ChatFormattedLine next_line;
238         ChatFormattedFragment temp_frag;
239         u32 out_column = 0;
240         u32 in_pos = 0;
241         u32 hanging_indentation = 0;
242
243         // Format the sender name and produce fragments
244         if (!line.name.empty()) {
245                 temp_frag.text = L"<";
246                 temp_frag.column = 0;
247                 //temp_frag.bold = 0;
248                 next_frags.push_back(temp_frag);
249                 temp_frag.text = line.name;
250                 temp_frag.column = 0;
251                 //temp_frag.bold = 1;
252                 next_frags.push_back(temp_frag);
253                 temp_frag.text = L"> ";
254                 temp_frag.column = 0;
255                 //temp_frag.bold = 0;
256                 next_frags.push_back(temp_frag);
257         }
258
259         std::wstring name_sanitized = line.name.c_str();
260
261         // Choose an indentation level
262         if (line.name.empty()) {
263                 // Server messages
264                 hanging_indentation = 0;
265         } else if (name_sanitized.size() + 3 <= cols/2) {
266                 // Names shorter than about half the console width
267                 hanging_indentation = line.name.size() + 3;
268         } else {
269                 // Very long names
270                 hanging_indentation = 2;
271         }
272         //EnrichedString line_text(line.text);
273
274         next_line.first = true;
275         // Set/use forced newline after the last frag in each line
276         bool mark_newline = false;
277
278         // Produce fragments and layout them into lines
279         while (!next_frags.empty() || in_pos < line.text.size()) {
280                 mark_newline = false; // now using this to USE line-end frag
281
282                 // Layout fragments into lines
283                 while (!next_frags.empty()) {
284                         ChatFormattedFragment& frag = next_frags[0];
285
286                         // Force newline after this frag, if marked
287                         if (frag.column == INT_MAX)
288                                 mark_newline = true;
289
290                         if (frag.text.size() <= cols - out_column) {
291                                 // Fragment fits into current line
292                                 frag.column = out_column;
293                                 next_line.fragments.push_back(frag);
294                                 out_column += frag.text.size();
295                                 next_frags.erase(next_frags.begin());
296                         } else {
297                                 // Fragment does not fit into current line
298                                 // So split it up
299                                 temp_frag.text = frag.text.substr(0, cols - out_column);
300                                 temp_frag.column = out_column;
301                                 temp_frag.weblink = frag.weblink;
302
303                                 next_line.fragments.push_back(temp_frag);
304                                 frag.text = frag.text.substr(cols - out_column);
305                                 frag.column = 0;
306                                 out_column = cols;
307                         }
308
309                         if (out_column == cols || mark_newline) {
310                                 // End the current line
311                                 destination.push_back(next_line);
312                                 num_added++;
313                                 next_line.fragments.clear();
314                                 next_line.first = false;
315
316                                 out_column = hanging_indentation;
317                                 mark_newline = false;
318                         }
319                 }
320
321                 // Produce fragment(s) for next formatted line
322                 if (!(in_pos < line.text.size()))
323                         continue;
324
325                 const std::wstring &linestring = line.text.getString();
326                 u32 remaining_in_output = cols - out_column;
327                 size_t http_pos = std::wstring::npos;
328                 mark_newline = false;  // now using this to SET line-end frag
329
330                 // Construct all frags for next output line
331                 while (!mark_newline) {
332                         // Determine a fragment length <= the minimum of
333                         // remaining_in_{in,out}put. Try to end the fragment
334                         // on a word boundary.
335                         u32 frag_length = 0, space_pos = 0;
336                         u32 remaining_in_input = line.text.size() - in_pos;
337
338                         if (m_cache_clickable_chat_weblinks) {
339                                 // Note: unsigned(-1) on fail
340                                 http_pos = linestring.find(L"https://", in_pos);
341                                 if (http_pos == std::wstring::npos)
342                                         http_pos = linestring.find(L"http://", in_pos);
343                                 if (http_pos != std::wstring::npos)
344                                         http_pos -= in_pos;
345                         }
346
347                         while (frag_length < remaining_in_input &&
348                                         frag_length < remaining_in_output) {
349                                 if (iswspace(linestring[in_pos + frag_length]))
350                                         space_pos = frag_length;
351                                 ++frag_length;
352                         }
353
354                         if (http_pos >= remaining_in_output) {
355                                 // Http not in range, grab until space or EOL, halt as normal.
356                                 // Note this works because (http_pos = npos) is unsigned(-1)
357
358                                 mark_newline = true;
359                         } else if (http_pos == 0) {
360                                 // At http, grab ALL until FIRST whitespace or end marker. loop.
361                                 // If at end of string, next loop will be empty string to mark end of weblink.
362
363                                 frag_length = 6;  // Frag is at least "http://"
364
365                                 // Chars to mark end of weblink
366                                 // TODO? replace this with a safer (slower) regex whitelist?
367                                 static const std::wstring delim_chars = L"\'\";";
368                                 wchar_t tempchar = linestring[in_pos+frag_length];
369                                 while (frag_length < remaining_in_input &&
370                                                 !iswspace(tempchar) &&
371                                                 delim_chars.find(tempchar) == std::wstring::npos) {
372                                         ++frag_length;
373                                         tempchar = linestring[in_pos+frag_length];
374                                 }
375
376                                 space_pos = frag_length - 1;
377                                 // This frag may need to be force-split. That's ok, urls aren't "words"
378                                 if (frag_length >= remaining_in_output) {
379                                         mark_newline = true;
380                                 }
381                         } else {
382                                 // Http in range, grab until http, loop
383
384                                 space_pos = http_pos - 1;
385                                 frag_length = http_pos;
386                         }
387
388                         // Include trailing space in current frag
389                         if (space_pos != 0 && frag_length < remaining_in_input)
390                                 frag_length = space_pos + 1;
391
392                         temp_frag.text = line.text.substr(in_pos, frag_length);
393                         // A hack so this frag remembers mark_newline for the layout phase
394                         temp_frag.column = mark_newline ? INT_MAX : 0;
395
396                         if (http_pos == 0) {
397                                 // Discard color stuff from the source frag
398                                 temp_frag.text = EnrichedString(temp_frag.text.getString());
399                                 temp_frag.text.setDefaultColor(m_cache_chat_weblink_color);
400                                 // Set weblink in the frag meta
401                                 temp_frag.weblink = wide_to_utf8(temp_frag.text.getString());
402                         } else {
403                                 temp_frag.weblink.clear();
404                         }
405                         next_frags.push_back(temp_frag);
406                         in_pos += frag_length;
407                         remaining_in_output -= std::min(frag_length, remaining_in_output);
408                 }
409         }
410
411         // End the last line
412         if (num_added == 0 || !next_line.fragments.empty()) {
413                 destination.push_back(next_line);
414                 num_added++;
415         }
416
417         return num_added;
418 }
419
420 s32 ChatBuffer::getTopScrollPos() const
421 {
422         s32 formatted_count = (s32) m_formatted.size();
423         s32 rows = (s32) m_rows;
424         if (rows == 0)
425                 return 0;
426
427         if (formatted_count <= rows)
428                 return formatted_count - rows;
429
430         return 0;
431 }
432
433 s32 ChatBuffer::getBottomScrollPos() const
434 {
435         s32 formatted_count = (s32) m_formatted.size();
436         s32 rows = (s32) m_rows;
437         if (rows == 0)
438                 return 0;
439
440         return formatted_count - rows;
441 }
442
443 void ChatBuffer::resize(u32 scrollback)
444 {
445         m_scrollback = scrollback;
446         if (m_unformatted.size() > m_scrollback)
447                 deleteOldest(m_unformatted.size() - m_scrollback);
448 }
449
450
451 ChatPrompt::ChatPrompt(const std::wstring &prompt, u32 history_limit):
452         m_prompt(prompt),
453         m_history_limit(history_limit)
454 {
455 }
456
457 const std::wstring &ChatPrompt::getLineRef() const
458 {
459         return m_history_index >= m_history.size() ? m_line : m_history[m_history_index].line;
460 }
461
462 std::wstring &ChatPrompt::makeLineRef()
463 {
464         if (m_history_index >= m_history.size()) {
465                 return m_line;
466         } else {
467                 if (!m_history[m_history_index].saved)
468                         m_history[m_history_index].saved = m_history[m_history_index].line;
469                 return m_history[m_history_index].line;
470         }
471 }
472
473 bool ChatPrompt::HistoryEntry::operator==(const ChatPrompt::HistoryEntry &other)
474 {
475         if (line != other.line)
476                 return false;
477         if (saved == other.saved)
478                 return true;
479         if ((!saved || saved == line) && (!other.saved || other.saved == other.line))
480                 return true;
481         return false;
482 }
483
484 void ChatPrompt::input(wchar_t ch)
485 {
486         makeLineRef().insert(m_cursor, 1, ch);
487         m_cursor++;
488         clampView();
489         m_nick_completion_start = 0;
490         m_nick_completion_end = 0;
491 }
492
493 void ChatPrompt::input(const std::wstring &str)
494 {
495         makeLineRef().insert(m_cursor, str);
496         m_cursor += str.size();
497         clampView();
498         m_nick_completion_start = 0;
499         m_nick_completion_end = 0;
500 }
501
502 void ChatPrompt::addToHistory(const std::wstring &line)
503 {
504         std::wstring old_line = getLine();
505         if (m_history_index < m_history.size()) {
506                 auto entry = m_history.begin() + m_history_index;
507                 if (entry->saved && entry->line == line) {
508                         entry->line = *entry->saved;
509                         entry->saved = nullopt;
510                         // Remove potential duplicates
511                         auto dup_before = std::find(m_history.begin(), entry, *entry);
512                         if (dup_before != entry)
513                                 m_history.erase(dup_before);
514                         else if (std::find(entry + 1, m_history.end(), *entry) != m_history.end())
515                                 m_history.erase(entry);
516                 }
517         }
518         if (!line.empty() &&
519                         (m_history.size() == 0 || m_history.back().line != line)) {
520                 HistoryEntry entry(line);
521                 // Remove all duplicates
522                 m_history.erase(std::remove(m_history.begin(), m_history.end(), entry),
523                                 m_history.end());
524                 // Push unique line
525                 m_history.push_back(std::move(entry));
526         }
527         if (m_history.size() > m_history_limit)
528                 m_history.erase(m_history.begin());
529         m_history_index = m_history.size();
530         m_line = std::move(old_line);
531 }
532
533 void ChatPrompt::clear()
534 {
535         makeLineRef().clear();
536         m_view = 0;
537         m_cursor = 0;
538         m_nick_completion_start = 0;
539         m_nick_completion_end = 0;
540 }
541
542 std::wstring ChatPrompt::replace(const std::wstring &line)
543 {
544         std::wstring old_line = getLine();
545         makeLineRef() = line;
546         m_view = m_cursor = line.size();
547         clampView();
548         m_nick_completion_start = 0;
549         m_nick_completion_end = 0;
550         return old_line;
551 }
552
553 void ChatPrompt::historyPrev()
554 {
555         if (m_history_index != 0) {
556                 --m_history_index;
557                 m_view = m_cursor = getLineRef().size();
558                 clampView();
559                 m_nick_completion_start = 0;
560                 m_nick_completion_end = 0;
561         }
562 }
563
564 void ChatPrompt::historyNext()
565 {
566         if (m_history_index < m_history.size()) {
567                 m_history_index++;
568                 m_view = m_cursor = getLineRef().size();
569                 clampView();
570                 m_nick_completion_start = 0;
571                 m_nick_completion_end = 0;
572         }
573 }
574
575 void ChatPrompt::nickCompletion(const std::list<std::string>& names, bool backwards)
576 {
577         // Two cases:
578         // (a) m_nick_completion_start == m_nick_completion_end == 0
579         //     Then no previous nick completion is active.
580         //     Get the word around the cursor and replace with any nick
581         //     that has that word as a prefix.
582         // (b) else, continue a previous nick completion.
583         //     m_nick_completion_start..m_nick_completion_end are the
584         //     interval where the originally used prefix was. Cycle
585         //     through the list of completions of that prefix.
586         const std::wstring &line = getLineRef();
587         u32 prefix_start = m_nick_completion_start;
588         u32 prefix_end = m_nick_completion_end;
589         bool initial = (prefix_end == 0);
590         if (initial)
591         {
592                 // no previous nick completion is active
593                 prefix_start = prefix_end = m_cursor;
594                 while (prefix_start > 0 && !iswspace(line[prefix_start-1]))
595                         --prefix_start;
596                 while (prefix_end < line.size() && !iswspace(line[prefix_end]))
597                         ++prefix_end;
598                 if (prefix_start == prefix_end)
599                         return;
600         }
601         std::wstring prefix = line.substr(prefix_start, prefix_end - prefix_start);
602
603         // find all names that start with the selected prefix
604         std::vector<std::wstring> completions;
605         for (const std::string &name : names) {
606                 std::wstring completion = utf8_to_wide(name);
607                 if (str_starts_with(completion, prefix, true)) {
608                         if (prefix_start == 0)
609                                 completion += L": ";
610                         completions.push_back(completion);
611                 }
612         }
613
614         if (completions.empty())
615                 return;
616
617         // find a replacement string and the word that will be replaced
618         u32 word_end = prefix_end;
619         u32 replacement_index = 0;
620         if (!initial)
621         {
622                 while (word_end < line.size() && !iswspace(line[word_end]))
623                         ++word_end;
624                 std::wstring word = line.substr(prefix_start, word_end - prefix_start);
625
626                 // cycle through completions
627                 for (u32 i = 0; i < completions.size(); ++i)
628                 {
629                         if (str_equal(word, completions[i], true))
630                         {
631                                 if (backwards)
632                                         replacement_index = i + completions.size() - 1;
633                                 else
634                                         replacement_index = i + 1;
635                                 replacement_index %= completions.size();
636                                 break;
637                         }
638                 }
639         }
640         std::wstring replacement = completions[replacement_index];
641         if (word_end < line.size() && iswspace(line[word_end]))
642                 ++word_end;
643
644         // replace existing word with replacement word,
645         // place the cursor at the end and record the completion prefix
646         makeLineRef().replace(prefix_start, word_end - prefix_start, replacement);
647         m_cursor = prefix_start + replacement.size();
648         clampView();
649         m_nick_completion_start = prefix_start;
650         m_nick_completion_end = prefix_end;
651 }
652
653 void ChatPrompt::reformat(u32 cols)
654 {
655         if (cols <= m_prompt.size())
656         {
657                 m_cols = 0;
658                 m_view = m_cursor;
659         }
660         else
661         {
662                 s32 length = getLineRef().size();
663                 bool was_at_end = (m_view + m_cols >= length + 1);
664                 m_cols = cols - m_prompt.size();
665                 if (was_at_end)
666                         m_view = length;
667                 clampView();
668         }
669 }
670
671 std::wstring ChatPrompt::getVisiblePortion() const
672 {
673         return m_prompt + getLineRef().substr(m_view, m_cols);
674 }
675
676 s32 ChatPrompt::getVisibleCursorPosition() const
677 {
678         return m_cursor - m_view + m_prompt.size();
679 }
680
681 void ChatPrompt::cursorOperation(CursorOp op, CursorOpDir dir, CursorOpScope scope)
682 {
683         s32 old_cursor = m_cursor;
684         s32 new_cursor = m_cursor;
685
686         const std::wstring &line = getLineRef();
687         s32 length = line.size();
688         s32 increment = (dir == CURSOROP_DIR_RIGHT) ? 1 : -1;
689
690         switch (scope) {
691         case CURSOROP_SCOPE_CHARACTER:
692                 new_cursor += increment;
693                 break;
694         case CURSOROP_SCOPE_WORD:
695                 if (dir == CURSOROP_DIR_RIGHT) {
696                         // skip one word to the right
697                         while (new_cursor < length && iswspace(line[new_cursor]))
698                                 new_cursor++;
699                         while (new_cursor < length && !iswspace(line[new_cursor]))
700                                 new_cursor++;
701                         while (new_cursor < length && iswspace(line[new_cursor]))
702                                 new_cursor++;
703                 } else {
704                         // skip one word to the left
705                         while (new_cursor >= 1 && iswspace(line[new_cursor - 1]))
706                                 new_cursor--;
707                         while (new_cursor >= 1 && !iswspace(line[new_cursor - 1]))
708                                 new_cursor--;
709                 }
710                 break;
711         case CURSOROP_SCOPE_LINE:
712                 new_cursor += increment * length;
713                 break;
714         case CURSOROP_SCOPE_SELECTION:
715                 break;
716         }
717
718         new_cursor = MYMAX(MYMIN(new_cursor, length), 0);
719
720         switch (op) {
721         case CURSOROP_MOVE:
722                 m_cursor = new_cursor;
723                 m_cursor_len = 0;
724                 break;
725         case CURSOROP_DELETE:
726                 if (m_cursor_len > 0) { // Delete selected text first
727                         makeLineRef().erase(m_cursor, m_cursor_len);
728                 } else {
729                         m_cursor = MYMIN(new_cursor, old_cursor);
730                         makeLineRef().erase(m_cursor, abs(new_cursor - old_cursor));
731                 }
732                 m_cursor_len = 0;
733                 break;
734         case CURSOROP_SELECT:
735                 if (scope == CURSOROP_SCOPE_LINE) {
736                         m_cursor = 0;
737                         m_cursor_len = length;
738                 } else {
739                         m_cursor = MYMIN(new_cursor, old_cursor);
740                         m_cursor_len += abs(new_cursor - old_cursor);
741                         m_cursor_len = MYMIN(m_cursor_len, length - m_cursor);
742                 }
743                 break;
744         }
745
746         clampView();
747
748         m_nick_completion_start = 0;
749         m_nick_completion_end = 0;
750 }
751
752 void ChatPrompt::clampView()
753 {
754         s32 length = getLineRef().size();
755         if (length + 1 <= m_cols)
756         {
757                 m_view = 0;
758         }
759         else
760         {
761                 m_view = MYMIN(m_view, length + 1 - m_cols);
762                 m_view = MYMIN(m_view, m_cursor);
763                 m_view = MYMAX(m_view, m_cursor - m_cols + 1);
764                 m_view = MYMAX(m_view, 0);
765         }
766 }
767
768
769
770 ChatBackend::ChatBackend():
771         m_console_buffer(500),
772         m_recent_buffer(6),
773         m_prompt(L"]", 500)
774 {
775 }
776
777 void ChatBackend::addMessage(const std::wstring &name, std::wstring text)
778 {
779         // Note: A message may consist of multiple lines, for example the MOTD.
780         text = translate_string(text);
781         WStrfnd fnd(text);
782         while (!fnd.at_end())
783         {
784                 std::wstring line = fnd.next(L"\n");
785                 m_console_buffer.addLine(name, line);
786                 m_recent_buffer.addLine(name, line);
787         }
788 }
789
790 void ChatBackend::addUnparsedMessage(std::wstring message)
791 {
792         // TODO: Remove the need to parse chat messages client-side, by sending
793         // separate name and text fields in TOCLIENT_CHAT_MESSAGE.
794
795         if (message.size() >= 2 && message[0] == L'<')
796         {
797                 std::size_t closing = message.find_first_of(L'>', 1);
798                 if (closing != std::wstring::npos &&
799                                 closing + 2 <= message.size() &&
800                                 message[closing+1] == L' ')
801                 {
802                         std::wstring name = message.substr(1, closing - 1);
803                         std::wstring text = message.substr(closing + 2);
804                         addMessage(name, text);
805                         return;
806                 }
807         }
808
809         // Unable to parse, probably a server message.
810         addMessage(L"", message);
811 }
812
813 ChatBuffer& ChatBackend::getConsoleBuffer()
814 {
815         return m_console_buffer;
816 }
817
818 ChatBuffer& ChatBackend::getRecentBuffer()
819 {
820         return m_recent_buffer;
821 }
822
823 EnrichedString ChatBackend::getRecentChat() const
824 {
825         EnrichedString result;
826         for (u32 i = 0; i < m_recent_buffer.getLineCount(); ++i) {
827                 const ChatLine& line = m_recent_buffer.getLine(i);
828                 if (i != 0)
829                         result += L"\n";
830                 if (!line.name.empty()) {
831                         result += L"<";
832                         result += line.name;
833                         result += L"> ";
834                 }
835                 result += line.text;
836         }
837         return result;
838 }
839
840 ChatPrompt& ChatBackend::getPrompt()
841 {
842         return m_prompt;
843 }
844
845 void ChatBackend::reformat(u32 cols, u32 rows)
846 {
847         m_console_buffer.reformat(cols, rows);
848
849         // no need to reformat m_recent_buffer, its formatted lines
850         // are not used
851
852         m_prompt.reformat(cols);
853 }
854
855 void ChatBackend::clearRecentChat()
856 {
857         m_recent_buffer.clear();
858 }
859
860
861 void ChatBackend::applySettings()
862 {
863         u32 recent_lines = g_settings->getU32("recent_chat_messages");
864         recent_lines = rangelim(recent_lines, 2, 20);
865         m_recent_buffer.resize(recent_lines);
866 }
867
868 void ChatBackend::step(float dtime)
869 {
870         m_recent_buffer.step(dtime);
871         m_recent_buffer.deleteByAge(60.0);
872
873         // no need to age messages in anything but m_recent_buffer
874 }
875
876 void ChatBackend::scroll(s32 rows)
877 {
878         m_console_buffer.scroll(rows);
879 }
880
881 void ChatBackend::scrollPageDown()
882 {
883         m_console_buffer.scroll(m_console_buffer.getRows());
884 }
885
886 void ChatBackend::scrollPageUp()
887 {
888         m_console_buffer.scroll(-(s32)m_console_buffer.getRows());
889 }