diff --git a/Libraries/LibLine/Editor.cpp b/Libraries/LibLine/Editor.cpp index 83e51aed55..7bfa8e3d3b 100644 --- a/Libraries/LibLine/Editor.cpp +++ b/Libraries/LibLine/Editor.cpp @@ -35,6 +35,7 @@ namespace Line { Editor::Editor() { + m_pending_chars = ByteBuffer::create_uninitialized(0); struct winsize ws; if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) < 0) m_num_columns = 80; @@ -67,22 +68,15 @@ void Editor::clear_line() void Editor::insert(const String& string) { - fputs(string.characters(), stdout); - fflush(stdout); - + m_pending_chars.append(string.characters(), string.length()); if (m_cursor == m_buffer.size()) { m_buffer.append(string.characters(), string.length()); m_cursor = m_buffer.size(); return; } - vt_save_cursor(); - vt_clear_to_end_of_line(); - for (size_t i = m_cursor; i < m_buffer.size(); ++i) - fputc(m_buffer[i], stdout); - vt_restore_cursor(); - m_buffer.ensure_capacity(m_buffer.size() + string.length()); + m_chars_inserted_in_the_middle += string.length(); for (size_t i = 0; i < string.length(); ++i) m_buffer.insert(m_cursor + i, string[i]); m_cursor += string.length(); @@ -90,22 +84,15 @@ void Editor::insert(const String& string) void Editor::insert(const char ch) { - putchar(ch); - fflush(stdout); - + m_pending_chars.append(&ch, 1); if (m_cursor == m_buffer.size()) { m_buffer.append(ch); m_cursor = m_buffer.size(); return; } - vt_save_cursor(); - vt_clear_to_end_of_line(); - for (size_t i = m_cursor; i < m_buffer.size(); ++i) - fputc(m_buffer[i], stdout); - vt_restore_cursor(); - m_buffer.insert(m_cursor, ch); + ++m_chars_inserted_in_the_middle; ++m_cursor; } @@ -118,6 +105,26 @@ void Editor::register_character_input_callback(char ch, Function m_key_callbacks.set(ch, make(move(callback))); } +void Editor::stylize(const Span& span, const Style& style) +{ + auto starting_map = m_spans_starting.get(span.beginning()).value_or({}); + + if (!starting_map.contains(span.end())) + m_refresh_needed = true; + + starting_map.set(span.end(), style); + + m_spans_starting.set(span.beginning(), starting_map); + + auto ending_map = m_spans_ending.get(span.end()).value_or({}); + + if (!ending_map.contains(span.beginning())) + m_refresh_needed = true; + ending_map.set(span.beginning(), style); + + m_spans_ending.set(span.end(), ending_map); +} + void Editor::cut_mismatching_chars(String& completion, const String& other, size_t start_compare) { size_t i = start_compare; @@ -174,15 +181,7 @@ String Editor::get_line(const String& prompt) return; } m_buffer.remove(m_cursor); - fputs("\033[3~", stdout); - fflush(stdout); - vt_save_cursor(); - vt_clear_to_end_of_line(); - for (size_t i = m_cursor; i < m_buffer.size(); ++i) - fputc(m_buffer[i], stdout); - vt_restore_cursor(); }; - for (ssize_t i = 0; i < nread; ++i) { char ch = keybuf[i]; if (ch == 0) @@ -379,7 +378,7 @@ String Editor::get_line(const String& prompt) do_backspace(); continue; } - if (ch == 0xc) { // ^L + if (ch == 0xc) { // ^L printf("\033[3J\033[H\033[2J"); // Clear screen. fputs(prompt.characters(), stdout); for (size_t i = 0; i < m_buffer.size(); ++i) @@ -422,9 +421,111 @@ String Editor::get_line(const String& prompt) insert(ch); } + refresh_display(); } } +void Editor::refresh_display() +{ + if (on_display_refresh) + on_display_refresh(*this); + + if (!m_refresh_needed && m_cursor == m_buffer.size()) { + // just write the characters out and continue + // no need to refresh the entire line + char null = 0; + m_pending_chars.append(&null, 1); + fputs((char*)m_pending_chars.data(), stdout); + m_pending_chars.clear(); + fflush(stdout); + return; + } + + // ouch, reflow entire line + // FIXME: handle multiline stuff + vt_move_relative(0, m_pending_chars.size() - m_chars_inserted_in_the_middle); + vt_save_cursor(); + auto current_line = cursor_line(); + vt_clear_lines(current_line - 1, num_lines() - current_line); + vt_move_relative(-num_lines() + 1, -offset_in_line() + m_chars_inserted_in_the_middle); + vt_clear_to_end_of_line(); + HashMap empty_styles {}; + for (size_t i = 0; i < m_buffer.size(); ++i) { + auto ends = m_spans_ending.get(i).value_or(empty_styles); + auto starts = m_spans_starting.get(i).value_or(empty_styles); + if (ends.size()) { + // go back to defaults + vt_apply_style(find_applicable_style(i)); + } + if (starts.size()) { + // set new options + vt_apply_style(starts.begin()->value); // apply some random style that starts here + } + fputc(m_buffer[i], stdout); + } + vt_apply_style({}); // don't bleed to EOL + vt_restore_cursor(); + vt_move_relative(0, m_chars_inserted_in_the_middle); + fflush(stdout); + m_pending_chars.clear(); + m_refresh_needed = false; + m_chars_inserted_in_the_middle = 0; +} + +void Editor::vt_move_relative(int x, int y) +{ + char x_op = 'A', y_op = 'D'; + if (x > 0) + x_op = 'B'; + else + x = -x; + if (y > 0) + y_op = 'C'; + else + y = -y; + + if (x > 0) + printf("\033[%d%c", x, x_op); + if (y > 0) + printf("\033[%d%c", y, y_op); +} + +Style Editor::find_applicable_style(size_t offset) const +{ + // walk through our styles and find one that fits in the offset + for (auto& entry : m_spans_starting) { + if (entry.key > offset) + continue; + for (auto& style_value : entry.value) { + if (style_value.key <= offset) + continue; + return style_value.value; + } + } + return {}; +} + +void Editor::vt_apply_style(const Style& style) +{ + printf( + "\033[%d;%d;%d;%d;%dm", + style.bold() ? 1 : 22, + style.underline() ? 4 : 24, + style.italic() ? 3 : 23, + (int)style.foreground() + 30, + (int)style.background() + 40); +} + +void Editor::vt_clear_lines(size_t count_above, size_t count_below) +{ + // go down count_below lines + if (count_below > 0) + printf("\033[%dB", (int)count_below); + // then clear lines going upwards + for (size_t i = 0; i < count_below + count_above; ++i) + fputs("\033[2K\033[A", stdout); +} + void Editor::vt_save_cursor() { fputs("\033[s", stdout); @@ -442,5 +543,4 @@ void Editor::vt_clear_to_end_of_line() fputs("\033[K", stdout); fflush(stdout); } - } diff --git a/Libraries/LibLine/Editor.h b/Libraries/LibLine/Editor.h index 62804748a8..b0560fdd56 100644 --- a/Libraries/LibLine/Editor.h +++ b/Libraries/LibLine/Editor.h @@ -27,6 +27,7 @@ #pragma once #include +#include #include #include #include @@ -35,6 +36,8 @@ #include #include #include +#include +#include #include #include @@ -76,8 +79,9 @@ public: void register_character_input_callback(char ch, Function callback); - Function(const String&)> on_tab_complete_first_token = nullptr; - Function(const String&)> on_tab_complete_other_token = nullptr; + Function(const String&)> on_tab_complete_first_token; + Function(const String&)> on_tab_complete_other_token; + Function on_display_refresh; // FIXME: we will have to kindly ask our instantiators to set our signal handlers // since we can not do this cleanly ourselves (signal() limitation: cannot give member functions) @@ -92,6 +96,13 @@ public: void insert(const String&); void insert(const char); void cut_mismatching_chars(String& completion, const String& other, size_t start_compare); + void stylize(const Span&, const Style&); + void strip_styles() + { + m_spans_starting.clear(); + m_spans_ending.clear(); + m_refresh_needed = true; + } const struct termios& termios() const { return m_termios; } const struct termios& default_termios() const { return m_default_termios; } @@ -100,9 +111,37 @@ private: void vt_save_cursor(); void vt_restore_cursor(); void vt_clear_to_end_of_line(); + void vt_clear_lines(size_t count_above, size_t count_below = 0); + void vt_move_relative(int x, int y); + void vt_apply_style(const Style&); + + Style find_applicable_style(size_t offset) const; + + void refresh_display(); + + // FIXME: These three will report the wrong value because they do not + // take the length of the prompt into consideration, and it does not + // appear that we can figure that out easily + size_t num_lines() const + { + return (m_buffer.size() + m_num_columns) / m_num_columns; + } + + size_t cursor_line() const + { + return (m_cursor + m_num_columns) / m_num_columns; + } + + size_t offset_in_line() const + { + auto offset = m_cursor % m_num_columns; + return offset; + } Vector m_buffer; + ByteBuffer m_pending_chars; size_t m_cursor { 0 }; + size_t m_chars_inserted_in_the_middle { 0 }; size_t m_times_tab_pressed { 0 }; size_t m_num_columns { 0 }; @@ -126,7 +165,11 @@ private: }; InputState m_state { InputState::Free }; - bool m_initialized = false; + HashMap> m_spans_starting; + HashMap> m_spans_ending; + + bool m_initialized { false }; + bool m_refresh_needed { false }; }; } diff --git a/Libraries/LibLine/Span.h b/Libraries/LibLine/Span.h new file mode 100644 index 0000000000..080ab7d6cf --- /dev/null +++ b/Libraries/LibLine/Span.h @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2020, The SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once +#include + +namespace Line { +class Span { +public: + Span(size_t start, size_t end) + : m_beginning(start) + , m_end(end) + { + } + + size_t beginning() const { return m_beginning; } + size_t end() const { return m_end; } + +private: + size_t m_beginning { 0 }; + size_t m_end { 0 }; +}; +} diff --git a/Libraries/LibLine/Style.h b/Libraries/LibLine/Style.h new file mode 100644 index 0000000000..cbd43f814b --- /dev/null +++ b/Libraries/LibLine/Style.h @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2020, The SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once +#include + +namespace Line { + +class Style { +public: + enum class Color : int { + Default = 9, + Black = 0, + Red, + Green, + Yellow, + Blue, + Magenta, + Cyan, + White, + // TODO: it appears that we do not support these SGR options + BrightBlack = 60, + BrightRed, + BrightGreen, + BrightYellow, + BrightBlue, + BrightMagenta, + BrightCyan, + BrightWhite, + }; + + struct UnderlineTag { + }; + struct BoldTag { + }; + struct ItalicTag { + }; + struct Background { + explicit Background(Color color) + : m_color(color) + { + } + Color m_color; + }; + struct Foreground { + explicit Foreground(Color color) + : m_color(color) + { + } + Color m_color; + }; + + static constexpr UnderlineTag Underline {}; + static constexpr BoldTag Bold {}; + static constexpr ItalicTag Italic {}; + + // prepare for the horror of templates + template + Style(const T& style_arg, Rest... rest) + : Style(rest...) + { + set(style_arg); + } + Style() {} + + bool underline() const { return m_underline; } + bool bold() const { return m_bold; } + bool italic() const { return m_italic; } + Color background() const { return m_background; } + Color foreground() const { return m_foreground; } + + void set(const ItalicTag&) { m_italic = true; } + void set(const BoldTag&) { m_bold = true; } + void set(const UnderlineTag&) { m_underline = true; } + void set(const Background& bg) { m_background = bg.m_color; } + void set(const Foreground& fg) { m_foreground = fg.m_color; } + +private: + bool m_underline { false }; + bool m_bold { false }; + bool m_italic { false }; + Color m_background { Color::Default }; + Color m_foreground { Color::Default }; +}; +}