diff options
Diffstat (limited to 'Userland/Libraries/LibLine/Editor.cpp')
-rw-r--r-- | Userland/Libraries/LibLine/Editor.cpp | 205 |
1 files changed, 174 insertions, 31 deletions
diff --git a/Userland/Libraries/LibLine/Editor.cpp b/Userland/Libraries/LibLine/Editor.cpp index 631f0fd3ad..1396a3bd41 100644 --- a/Userland/Libraries/LibLine/Editor.cpp +++ b/Userland/Libraries/LibLine/Editor.cpp @@ -12,6 +12,7 @@ #include <AK/GenericLexer.h> #include <AK/JsonObject.h> #include <AK/MemoryStream.h> +#include <AK/RedBlackTree.h> #include <AK/ScopeGuard.h> #include <AK/ScopedValueRollback.h> #include <AK/StringBuilder.h> @@ -475,6 +476,27 @@ void Editor::stylize(Span const& span, Style const& style) end = offsets.end; } + if (auto maybe_mask = style.mask(); maybe_mask.has_value()) { + auto it = m_current_masks.find_smallest_not_below_iterator(span.beginning()); + Optional<Style::Mask> last_encountered_entry; + if (!it.is_end()) { + // Delete all overlapping old masks. + while (true) { + auto next_it = m_current_masks.find_largest_not_above_iterator(span.end()); + if (next_it.is_end()) + break; + if (it->has_value()) + last_encountered_entry = *it; + m_current_masks.remove(next_it.key()); + } + } + m_current_masks.insert(span.beginning(), move(maybe_mask)); + m_current_masks.insert(span.end(), {}); + if (last_encountered_entry.has_value()) + m_current_masks.insert(span.end() + 1, move(last_encountered_entry)); + style.unset_mask(); + } + auto& spans_starting = style.is_anchored() ? m_current_spans.m_anchored_spans_starting : m_current_spans.m_spans_starting; auto& spans_ending = style.is_anchored() ? m_current_spans.m_anchored_spans_ending : m_current_spans.m_spans_ending; @@ -1272,7 +1294,7 @@ void Editor::recalculate_origin() } void Editor::cleanup() { - auto current_buffer_metrics = actual_rendered_string_metrics(buffer_view()); + auto current_buffer_metrics = actual_rendered_string_metrics(buffer_view(), m_current_masks); auto new_lines = current_prompt_metrics().lines_with_addition(current_buffer_metrics, m_num_columns); auto shown_lines = num_lines(); if (new_lines < shown_lines) @@ -1335,7 +1357,7 @@ void Editor::refresh_display() if (m_cached_prompt_valid && !m_refresh_needed && m_pending_chars.size() == 0) { // Probably just moving around. reposition_cursor(output_stream); - m_cached_buffer_metrics = actual_rendered_string_metrics(buffer_view()); + m_cached_buffer_metrics = actual_rendered_string_metrics(buffer_view(), m_current_masks); m_drawn_end_of_line_offset = m_buffer.size(); return; } @@ -1351,7 +1373,7 @@ void Editor::refresh_display() m_pending_chars.clear(); m_drawn_cursor = m_cursor; m_drawn_end_of_line_offset = m_buffer.size(); - m_cached_buffer_metrics = actual_rendered_string_metrics(buffer_view()); + m_cached_buffer_metrics = actual_rendered_string_metrics(buffer_view(), m_current_masks); m_drawn_spans = m_current_spans; return; } @@ -1395,24 +1417,47 @@ void Editor::refresh_display() }; auto print_character_at = [&](size_t i) { - StringBuilder builder; - auto c = m_buffer[i]; - bool should_print_masked = is_ascii_control(c) && c != '\n'; - bool should_print_caret = c < 64 && should_print_masked; - if (should_print_caret) - builder.appendff("^{:c}", c + 64); - else if (should_print_masked) - builder.appendff("\\x{:0>2x}", c); - else - builder.append(Utf32View { &c, 1 }); + Variant<u32, Utf8View> c { Utf8View {} }; + if (auto it = m_current_masks.find_largest_not_above_iterator(i); !it.is_end() && it->has_value()) { + auto offset = i - it.key(); + if (it->value().mode == Style::Mask::Mode::ReplaceEntireSelection) { + auto& mask = it->value().replacement_view; + auto replacement = mask.begin().peek(offset); + if (!replacement.has_value()) + return; + c = replacement.value(); + ++it; + u32 next_offset = it.is_end() ? m_drawn_end_of_line_offset : it.key(); + if (i + 1 == next_offset) + c = mask.unicode_substring_view(offset, mask.length() - offset); + } else { + c = it->value().replacement_view; + } + } else { + c = m_buffer[i]; + } + auto print_single_character = [&](auto c) { + StringBuilder builder; + bool should_print_masked = is_ascii_control(c) && c != '\n'; + bool should_print_caret = c < 64 && should_print_masked; + if (should_print_caret) + builder.appendff("^{:c}", c + 64); + else if (should_print_masked) + builder.appendff("\\x{:0>2x}", c); + else + builder.append(Utf32View { &c, 1 }); - if (should_print_masked) - output_stream.write("\033[7m"sv.bytes()); + if (should_print_masked) + output_stream.write("\033[7m"sv.bytes()); - output_stream.write(builder.string_view().bytes()); + output_stream.write(builder.string_view().bytes()); - if (should_print_masked) - output_stream.write("\033[27m"sv.bytes()); + if (should_print_masked) + output_stream.write("\033[27m"sv.bytes()); + }; + c.visit( + [&](u32 c) { print_single_character(c); }, + [&](auto& view) { for (auto c : view) print_single_character(c); }); }; // If there have been no changes to previous sections of the line (style or text) @@ -1429,7 +1474,7 @@ void Editor::refresh_display() VT::apply_style(Style::reset_style(), output_stream); m_pending_chars.clear(); m_refresh_needed = false; - m_cached_buffer_metrics = actual_rendered_string_metrics(buffer_view()); + m_cached_buffer_metrics = actual_rendered_string_metrics(buffer_view(), m_current_masks); m_chars_touched_in_the_middle = 0; m_drawn_cursor = m_cursor; m_drawn_end_of_line_offset = m_buffer.size(); @@ -1477,7 +1522,7 @@ void Editor::refresh_display() m_pending_chars.clear(); m_refresh_needed = false; - m_cached_buffer_metrics = actual_rendered_string_metrics(buffer_view()); + m_cached_buffer_metrics = actual_rendered_string_metrics(buffer_view(), m_current_masks); m_chars_touched_in_the_middle = 0; m_drawn_spans = m_current_spans; m_drawn_end_of_line_offset = m_buffer.size(); @@ -1490,6 +1535,8 @@ void Editor::strip_styles(bool strip_anchored) { m_current_spans.m_spans_starting.clear(); m_current_spans.m_spans_ending.clear(); + m_current_masks.clear(); + m_cached_buffer_metrics = actual_rendered_string_metrics(buffer_view(), {}); if (strip_anchored) { m_current_spans.m_anchored_spans_starting.clear(); @@ -1662,6 +1709,14 @@ String Style::to_string() const if (!m_hyperlink.is_empty()) builder.appendff("Hyperlink(\"{}\"), ", m_hyperlink.m_link); + if (!m_mask.has_value()) { + builder.appendff("Mask(\"{}\", {}), ", + m_mask->replacement, + m_mask->mode == Mask::Mode::ReplaceEntireSelection + ? "ReplaceEntireSelection" + : "ReplaceEachCodePointInSelection"); + } + builder.append("}"); return builder.build(); @@ -1715,20 +1770,77 @@ void VT::clear_to_end_of_line(OutputStream& stream) stream.write("\033[K"sv.bytes()); } -StringMetrics Editor::actual_rendered_string_metrics(StringView string) +enum VTState { + Free = 1, + Escape = 3, + Bracket = 5, + BracketArgsSemi = 7, + Title = 9, +}; +static VTState actual_rendered_string_length_step(StringMetrics& metrics, size_t index, StringMetrics::LineMetrics& current_line, u32 c, u32 next_c, VTState state, Optional<Style::Mask> const& mask); + +enum class MaskedSelectionDecision { + Skip, + Continue, +}; +static MaskedSelectionDecision resolve_masked_selection(Optional<Style::Mask>& mask, size_t& i, auto& mask_it, auto& view, auto& state, auto& metrics, auto& current_line) +{ + if (mask.has_value() && mask->mode == Style::Mask::Mode::ReplaceEntireSelection) { + ++mask_it; + auto actual_end_offset = mask_it.is_end() ? view.length() : mask_it.key(); + auto end_offset = min(actual_end_offset, view.length()); + size_t j = 0; + for (auto it = mask->replacement_view.begin(); it != mask->replacement_view.end(); ++it) { + auto it_copy = it; + ++it_copy; + auto next_c = it_copy == mask->replacement_view.end() ? 0 : *it_copy; + state = actual_rendered_string_length_step(metrics, j, current_line, *it, next_c, state, {}); + ++j; + if (j <= actual_end_offset - i && j + i >= view.length()) + break; + } + current_line.masked_chars.empend(i, end_offset - i, j); + i = end_offset; + + if (mask_it.is_end()) + mask = {}; + else + mask = *mask_it; + return MaskedSelectionDecision::Skip; + } + return MaskedSelectionDecision::Continue; +} + +StringMetrics Editor::actual_rendered_string_metrics(StringView string, RedBlackTree<u32, Optional<Style::Mask>> const& masks) { StringMetrics metrics; StringMetrics::LineMetrics current_line; VTState state { Free }; Utf8View view { string }; auto it = view.begin(); + Optional<Style::Mask> mask; + size_t i = 0; + auto mask_it = masks.begin(); for (; it != view.end(); ++it) { + if (!mask_it.is_end() && mask_it.key() <= i) + mask = *mask_it; auto c = *it; auto it_copy = it; ++it_copy; + + if (resolve_masked_selection(mask, i, mask_it, view, state, metrics, current_line) == MaskedSelectionDecision::Skip) + continue; + auto next_c = it_copy == view.end() ? 0 : *it_copy; - state = actual_rendered_string_length_step(metrics, view.iterator_offset(it), current_line, c, next_c, state); + state = actual_rendered_string_length_step(metrics, view.iterator_offset(it), current_line, c, next_c, state, mask); + if (!mask_it.is_end() && mask_it.key() <= i) { + auto mask_it_peek = mask_it; + ++mask_it_peek; + if (!mask_it_peek.is_end() && mask_it_peek.key() > i) + mask_it = mask_it_peek; + } + ++i; } metrics.line_metrics.append(current_line); @@ -1739,16 +1851,33 @@ StringMetrics Editor::actual_rendered_string_metrics(StringView string) return metrics; } -StringMetrics Editor::actual_rendered_string_metrics(Utf32View const& view) +StringMetrics Editor::actual_rendered_string_metrics(Utf32View const& view, RedBlackTree<u32, Optional<Style::Mask>> const& masks) { StringMetrics metrics; StringMetrics::LineMetrics current_line; VTState state { Free }; + Optional<Style::Mask> mask; + + auto mask_it = masks.begin(); for (size_t i = 0; i < view.length(); ++i) { - auto c = view.code_points()[i]; + auto c = view[i]; + if (!mask_it.is_end() && mask_it.key() <= i) + mask = *mask_it; + + if (resolve_masked_selection(mask, i, mask_it, view, state, metrics, current_line) == MaskedSelectionDecision::Skip) { + --i; + continue; + } + auto next_c = i + 1 < view.length() ? view.code_points()[i + 1] : 0; - state = actual_rendered_string_length_step(metrics, i, current_line, c, next_c, state); + state = actual_rendered_string_length_step(metrics, i, current_line, c, next_c, state, mask); + if (!mask_it.is_end() && mask_it.key() <= i) { + auto mask_it_peek = mask_it; + ++mask_it_peek; + if (!mask_it_peek.is_end() && mask_it_peek.key() > i) + mask_it = mask_it_peek; + } } metrics.line_metrics.append(current_line); @@ -1759,10 +1888,10 @@ StringMetrics Editor::actual_rendered_string_metrics(Utf32View const& view) return metrics; } -Editor::VTState Editor::actual_rendered_string_length_step(StringMetrics& metrics, size_t index, StringMetrics::LineMetrics& current_line, u32 c, u32 next_c, VTState state) +VTState actual_rendered_string_length_step(StringMetrics& metrics, size_t index, StringMetrics::LineMetrics& current_line, u32 c, u32 next_c, VTState state, Optional<Style::Mask> const& mask) { switch (state) { - case Free: + case Free: { if (c == '\x1b') { // escape return Escape; } @@ -1779,12 +1908,26 @@ Editor::VTState Editor::actual_rendered_string_length_step(StringMetrics& metric current_line.length = 0; return state; } - if (is_ascii_control(c) && c != '\n') - current_line.masked_chars.append({ index, 1, c < 64 ? 2u : 4u }); // if the character cannot be represented as ^c, represent it as \xbb. + auto is_control = is_ascii_control(c); + if (is_control) { + if (mask.has_value()) + current_line.masked_chars.append({ index, 1, mask->replacement_view.length() }); + else + current_line.masked_chars.append({ index, 1, c < 64 ? 2u : 4u }); // if the character cannot be represented as ^c, represent it as \xbb. + } // FIXME: This will not support anything sophisticated - ++current_line.length; - ++metrics.total_length; + if (mask.has_value()) { + current_line.length += mask->replacement_view.length(); + metrics.total_length += mask->replacement_view.length(); + } else if (is_control) { + current_line.length += current_line.masked_chars.last().masked_length; + metrics.total_length += current_line.masked_chars.last().masked_length; + } else { + ++current_line.length; + ++metrics.total_length; + } return state; + } case Escape: if (c == ']') { if (next_c == '0') |