diff --git a/Libraries/LibLine/Editor.cpp b/Libraries/LibLine/Editor.cpp index 4a76f80ba6..bd30a751d0 100644 --- a/Libraries/LibLine/Editor.cpp +++ b/Libraries/LibLine/Editor.cpp @@ -89,6 +89,8 @@ void Editor::insert(const u32 cp) auto str = builder.build(); m_pending_chars.append(str.characters(), str.length()); + readjust_anchored_styles(m_cursor, ModificationKind::Insertion); + if (m_cursor == m_buffer.size()) { m_buffer.append(cp); m_cursor = m_buffer.size(); @@ -126,6 +128,9 @@ static size_t codepoint_length_in_utf8(u32 codepoint) void Editor::stylize(const Span& span, const Style& style) { + if (style.is_empty()) + return; + auto start = span.beginning(); auto end = span.end(); @@ -153,22 +158,25 @@ void Editor::stylize(const Span& span, const Style& style) end = end_codepoint_offset; } - auto starting_map = m_spans_starting.get(start).value_or({}); + auto& spans_starting = style.is_anchored() ? m_anchored_spans_starting : m_spans_starting; + auto& spans_ending = style.is_anchored() ? m_anchored_spans_ending : m_spans_ending; + + auto starting_map = spans_starting.get(start).value_or({}); if (!starting_map.contains(end)) m_refresh_needed = true; starting_map.set(end, style); - m_spans_starting.set(start, starting_map); + spans_starting.set(start, starting_map); - auto ending_map = m_spans_ending.get(end).value_or({}); + auto ending_map = spans_ending.get(end).value_or({}); if (!ending_map.contains(start)) m_refresh_needed = true; ending_map.set(start, style); - m_spans_ending.set(end, ending_map); + spans_ending.set(end, ending_map); } String Editor::get_line(const String& prompt) @@ -179,6 +187,7 @@ String Editor::get_line(const String& prompt) set_prompt(prompt); reset(); set_origin(); + strip_styles(true); m_history_cursor = m_history.size(); for (;;) { @@ -396,7 +405,7 @@ String Editor::get_line(const String& prompt) fflush(stdout); continue; } - m_buffer.remove(m_cursor); + remove_at_index(m_cursor); m_refresh_needed = true; m_search_offset = 0; m_state = InputState::ExpectTerminator; @@ -436,6 +445,8 @@ String Editor::get_line(const String& prompt) // reverse tab can count as regular tab here m_times_tab_pressed++; + int token_start = m_cursor - 1 - m_last_shown_suggestion_display_length; + // ask for completions only on the first tab // and scan for the largest common prefix to display // further tabs simply show the cached completions @@ -506,12 +517,13 @@ String Editor::get_line(const String& prompt) } for (size_t i = m_next_suggestion_invariant_offset; i < shown_length; ++i) - m_buffer.remove(actual_offset); + remove_at_index(actual_offset); m_cursor = actual_offset; m_inline_search_cursor = m_cursor; m_refresh_needed = true; } m_last_shown_suggestion = m_suggestions[m_next_suggestion_index]; + m_last_shown_suggestion.token_start_index = token_start - m_next_suggestion_invariant_offset - m_last_shown_suggestion.trailing_trivia.length(); m_last_shown_suggestion_display_length = m_last_shown_suggestion.text.length(); m_last_shown_suggestion_was_complete = true; if (m_times_tab_pressed == 1) { @@ -526,6 +538,7 @@ String Editor::get_line(const String& prompt) // add in the trivia of the last selected suggestion insert(m_last_shown_suggestion.trailing_trivia); m_last_shown_suggestion_display_length += m_last_shown_suggestion.trailing_trivia.length(); + stylize({ m_last_shown_suggestion.token_start_index, m_cursor, Span::Mode::CodepointOriented }, m_last_shown_suggestion.style); } } else { m_last_shown_suggestion_display_length = 0; @@ -609,7 +622,7 @@ String Editor::get_line(const String& prompt) } if (m_last_shown_suggestion_was_complete && index == current_suggestion_index) { - vt_apply_style({}); + vt_apply_style(Style::reset_style()); fflush(stdout); } @@ -645,6 +658,8 @@ String Editor::get_line(const String& prompt) } if (m_times_tab_pressed) { + // Apply the style of the last suggestion + stylize({ m_last_shown_suggestion.token_start_index, m_cursor, Span::Mode::CodepointOriented }, m_last_shown_suggestion.style); // we probably have some suggestions drawn // let's clean them up if (m_lines_used_for_last_suggestions) { @@ -670,7 +685,7 @@ String Editor::get_line(const String& prompt) fflush(stdout); return; } - m_buffer.remove(m_cursor - 1); + remove_at_index(m_cursor - 1); --m_cursor; m_inline_search_cursor = m_cursor; // we will have to redraw :( @@ -696,7 +711,7 @@ String Editor::get_line(const String& prompt) } if (codepoint == m_termios.c_cc[VKILL]) { for (size_t i = 0; i < m_cursor; ++i) - m_buffer.remove(0); + remove_at_index(0); m_cursor = 0; m_refresh_needed = true; continue; @@ -972,19 +987,45 @@ void Editor::refresh_display() 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()) { + + auto anchored_ends = m_anchored_spans_ending.get(i).value_or(empty_styles); + auto anchored_starts = m_anchored_spans_starting.get(i).value_or(empty_styles); + + if (ends.size() || anchored_ends.size()) { + Style style; + + for (auto& applicable_style : ends) + style.unify_with(applicable_style.value); + + for (auto& applicable_style : anchored_ends) + style.unify_with(applicable_style.value); + + // Disable any style that should be turned off + vt_apply_style(style, false); + // go back to defaults - vt_apply_style(find_applicable_style(i)); + style = find_applicable_style(i); + vt_apply_style(style, true); } - if (starts.size()) { + if (starts.size() || anchored_starts.size()) { + Style style; + + for (auto& applicable_style : starts) + style.unify_with(applicable_style.value); + + for (auto& applicable_style : anchored_starts) + style.unify_with(applicable_style.value); + // set new options - vt_apply_style(starts.begin()->value); // apply some random style that starts here + vt_apply_style(style, true); } builder.clear(); builder.append(Utf32View { &m_buffer[i], 1 }); fputs(builder.to_string().characters(), stdout); } - vt_apply_style({}); // don't bleed to EOL + + vt_apply_style(Style::reset_style()); // don't bleed to EOL + m_pending_chars.clear(); m_refresh_needed = false; m_cached_buffer_size = m_buffer.size(); @@ -997,6 +1038,19 @@ void Editor::refresh_display() fflush(stdout); } +void Editor::strip_styles(bool strip_anchored) +{ + m_spans_starting.clear(); + m_spans_ending.clear(); + + if (strip_anchored) { + m_anchored_spans_starting.clear(); + m_anchored_spans_ending.clear(); + } + + m_refresh_needed = true; +} + void Editor::reposition_cursor() { m_drawn_cursor = m_cursor; @@ -1034,21 +1088,34 @@ void Editor::vt_move_relative(int x, int y) 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; + // walk through our styles and merge all that fit in the offset + Style style; + auto unify = [&](auto& entry) { + if (entry.key >= offset) + return; for (auto& style_value : entry.value) { if (style_value.key <= offset) - continue; - return style_value.value; + return; + style.unify_with(style_value.value); } + }; + + for (auto& entry : m_spans_starting) { + unify(entry); } - return {}; + + for (auto& entry : m_anchored_spans_starting) { + unify(entry); + } + + return style; } String Style::Background::to_vt_escape() const { + if (is_default()) + return ""; + if (m_is_rgb) { return String::format("\033[48;2;%d;%d;%dm", m_rgb_color[0], m_rgb_color[1], m_rgb_color[2]); } else { @@ -1058,6 +1125,9 @@ String Style::Background::to_vt_escape() const String Style::Foreground::to_vt_escape() const { + if (is_default()) + return ""; + if (m_is_rgb) { return String::format("\033[38;2;%d;%d;%dm", m_rgb_color[0], m_rgb_color[1], m_rgb_color[2]); } else { @@ -1065,15 +1135,94 @@ String Style::Foreground::to_vt_escape() const } } -void Editor::vt_apply_style(const Style& style) +String Style::Hyperlink::to_vt_escape(bool starting) const { - printf( - "\033[%d;%d;%dm%s%s", - style.bold() ? 1 : 22, - style.underline() ? 4 : 24, - style.italic() ? 3 : 23, - style.background().to_vt_escape().characters(), - style.foreground().to_vt_escape().characters()); + if (is_empty()) + return ""; + + return String::format("\033]8;;%s\033\\", starting ? m_link.characters() : ""); +} + +void Style::unify_with(const Style& other, bool prefer_other) +{ + // unify colors + if (prefer_other || m_background.is_default()) + m_background = other.background(); + + if (prefer_other || m_foreground.is_default()) + m_foreground = other.foreground(); + + // unify graphic renditions + if (other.bold()) + set(Bold); + + if (other.italic()) + set(Italic); + + if (other.underline()) + set(Underline); + + // unify links + if (prefer_other || m_hyperlink.is_empty()) + m_hyperlink = other.hyperlink(); +} + +String Style::to_string() const +{ + StringBuilder builder; + builder.append("Style { "); + + if (!m_foreground.is_default()) { + builder.append("Foreground("); + if (m_foreground.m_is_rgb) { + builder.join(", ", m_foreground.m_rgb_color); + } else { + builder.appendf("(XtermColor) %d", m_foreground.m_xterm_color); + } + builder.append("), "); + } + + if (!m_background.is_default()) { + builder.append("Background("); + if (m_background.m_is_rgb) { + builder.join(' ', m_background.m_rgb_color); + } else { + builder.appendf("(XtermColor) %d", m_background.m_xterm_color); + } + builder.append("), "); + } + + if (bold()) + builder.append("Bold, "); + + if (underline()) + builder.append("Underline, "); + + if (italic()) + builder.append("Italic, "); + + if (!m_hyperlink.is_empty()) + builder.appendf("Hyperlink(\"%s\"), ", m_hyperlink.m_link.characters()); + + builder.append("}"); + + return builder.build(); +} + +void Editor::vt_apply_style(const Style& style, bool is_starting) +{ + if (is_starting) { + printf( + "\033[%d;%d;%dm%s%s%s", + style.bold() ? 1 : 22, + style.underline() ? 4 : 24, + style.italic() ? 3 : 23, + style.background().to_vt_escape().characters(), + style.foreground().to_vt_escape().characters(), + style.hyperlink().to_vt_escape(true).characters()); + } else { + printf("%s", style.hyperlink().to_vt_escape(false).characters()); + } } void Editor::vt_clear_lines(size_t count_above, size_t count_below) @@ -1250,4 +1399,49 @@ bool Editor::should_break_token(Vector& buffer, size_t index) return true; }; +void Editor::remove_at_index(size_t index) +{ + // see if we have any anchored styles, and reposition them if needed + readjust_anchored_styles(index, ModificationKind::Removal); + m_buffer.remove(index); +} + +void Editor::readjust_anchored_styles(size_t hint_index, ModificationKind modification) +{ + struct Anchor { + Span old_span; + Span new_span; + Style style; + }; + Vector anchors_to_relocate; + auto index_shift = modification == ModificationKind::Insertion ? 1 : -1; + + for (auto& start_entry : m_anchored_spans_starting) { + for (auto& end_entry : start_entry.value) { + if (start_entry.key >= hint_index) { + if (start_entry.key == hint_index && end_entry.key == hint_index + 1 && modification == ModificationKind::Removal) { + // remove the anchor, as all its text was wiped + continue; + } + // shift everything + anchors_to_relocate.append({ { start_entry.key, end_entry.key, Span::Mode::CodepointOriented }, { start_entry.key + index_shift, end_entry.key + index_shift, Span::Mode::CodepointOriented }, end_entry.value }); + continue; + } + if (end_entry.key > hint_index) { + // shift just the end + anchors_to_relocate.append({ { start_entry.key, end_entry.key, Span::Mode::CodepointOriented }, { start_entry.key, end_entry.key + index_shift, Span::Mode::CodepointOriented }, end_entry.value }); + continue; + } + anchors_to_relocate.append({ { start_entry.key, end_entry.key, Span::Mode::CodepointOriented }, { start_entry.key, end_entry.key, Span::Mode::CodepointOriented }, end_entry.value }); + } + } + + m_anchored_spans_ending.clear(); + m_anchored_spans_starting.clear(); + // pass over the relocations and update the stale entries + for (auto& relocation : anchors_to_relocate) { + stylize(relocation.new_span, relocation.style); + } +} + } diff --git a/Libraries/LibLine/Editor.h b/Libraries/LibLine/Editor.h index fcb1883c06..cb8d91c584 100644 --- a/Libraries/LibLine/Editor.h +++ b/Libraries/LibLine/Editor.h @@ -50,11 +50,19 @@ struct CompletionSuggestion { CompletionSuggestion(const String& completion) : text(completion) , trailing_trivia("") + , style() { } CompletionSuggestion(const StringView& completion, const StringView& trailing_trivia) : text(completion) , trailing_trivia(trailing_trivia) + , style() + { + } + CompletionSuggestion(const StringView& completion, const StringView& trailing_trivia, Style style) + : text(completion) + , trailing_trivia(trailing_trivia) + , style(style) { } @@ -65,6 +73,8 @@ struct CompletionSuggestion { String text; String trailing_trivia; + Style style; + size_t token_start_index { 0 }; }; struct Configuration { @@ -157,12 +167,8 @@ public: void insert(const String&); void insert(const u32); void stylize(const Span&, const Style&); - void strip_styles() - { - m_spans_starting.clear(); - m_spans_ending.clear(); - m_refresh_needed = true; - } + void strip_styles(bool strip_anchored = false); + void suggest(size_t invariant_offset = 0, size_t index = 0) const { m_next_suggestion_index = index; @@ -194,8 +200,15 @@ private: void vt_clear_lines(size_t count_above, size_t count_below = 0); void vt_move_relative(int x, int y); void vt_move_absolute(u32 x, u32 y); - void vt_apply_style(const Style&); + void vt_apply_style(const Style&, bool is_starting = true); Vector vt_dsr(); + void remove_at_index(size_t); + + enum class ModificationKind { + Insertion, + Removal, + }; + void readjust_anchored_styles(size_t hint_index, ModificationKind); Style find_applicable_style(size_t offset) const; @@ -343,6 +356,9 @@ private: HashMap> m_spans_starting; HashMap> m_spans_ending; + HashMap> m_anchored_spans_starting; + HashMap> m_anchored_spans_ending; + bool m_initialized { false }; bool m_refresh_needed { false }; diff --git a/Libraries/LibLine/Style.h b/Libraries/LibLine/Style.h index f18f727f78..bd0c06c3df 100644 --- a/Libraries/LibLine/Style.h +++ b/Libraries/LibLine/Style.h @@ -25,6 +25,7 @@ */ #pragma once +#include #include #include #include @@ -43,8 +44,11 @@ public: Magenta, Cyan, White, + Unchanged, }; + struct AnchoredTag { + }; struct UnderlineTag { }; struct BoldTag { @@ -63,8 +67,13 @@ public: { } - XtermColor m_xterm_color { XtermColor::Default }; - Vector m_rgb_color; + bool is_default() const + { + return !m_is_rgb && m_xterm_color == XtermColor::Unchanged; + } + + XtermColor m_xterm_color { XtermColor::Unchanged }; + Vector m_rgb_color; bool m_is_rgb { false }; }; @@ -93,9 +102,27 @@ public: String to_vt_escape() const; }; + struct Hyperlink { + explicit Hyperlink(const StringView& link) + : m_link(link) + { + m_has_link = true; + } + + Hyperlink() { } + + String to_vt_escape(bool starting) const; + + bool is_empty() const { return !m_has_link; } + + String m_link; + bool m_has_link { false }; + }; + static constexpr UnderlineTag Underline {}; static constexpr BoldTag Bold {}; static constexpr ItalicTag Italic {}; + static constexpr AnchoredTag Anchored {}; // prepare for the horror of templates template @@ -103,26 +130,54 @@ public: : Style(rest...) { set(style_arg); + m_is_empty = false; } Style() { } + static Style reset_style() + { + return { Foreground(XtermColor::Default), Background(XtermColor::Default), Hyperlink("") }; + } + + Style unified_with(const Style& other, bool prefer_other = true) const + { + Style style = *this; + style.unify_with(other, prefer_other); + return style; + } + + void unify_with(const Style&, bool prefer_other = false); + bool underline() const { return m_underline; } bool bold() const { return m_bold; } bool italic() const { return m_italic; } Background background() const { return m_background; } Foreground foreground() const { return m_foreground; } + Hyperlink hyperlink() const { return m_hyperlink; } 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; } void set(const Foreground& fg) { m_foreground = fg; } + void set(const Hyperlink& link) { m_hyperlink = link; } + void set(const AnchoredTag&) { m_is_anchored = true; } + + bool is_anchored() const { return m_is_anchored; } + bool is_empty() const { return m_is_empty; } + + String to_string() const; private: bool m_underline { false }; bool m_bold { false }; bool m_italic { false }; - Background m_background { XtermColor::Default }; - Foreground m_foreground { XtermColor::Default }; + Background m_background { XtermColor::Unchanged }; + Foreground m_foreground { XtermColor::Unchanged }; + Hyperlink m_hyperlink; + + bool m_is_anchored { false }; + + bool m_is_empty { true }; }; }