1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-25 17:37:37 +00:00

LibLine: Add support for user-controlled masking

This commit is contained in:
Ali Mohammad Pur 2022-05-02 16:10:42 +04:30 committed by Linus Groh
parent c257d27f0b
commit 78dc77f7e4
5 changed files with 212 additions and 59 deletions

View file

@ -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')

View file

@ -12,6 +12,7 @@
#include <AK/Function.h>
#include <AK/HashMap.h>
#include <AK/OwnPtr.h>
#include <AK/RedBlackTree.h>
#include <AK/Result.h>
#include <AK/String.h>
#include <AK/Traits.h>
@ -159,8 +160,8 @@ public:
void register_key_input_callback(Vector<Key> keys, Function<bool(Editor&)> callback) { m_callback_machine.register_key_input_callback(move(keys), move(callback)); }
void register_key_input_callback(Key key, Function<bool(Editor&)> callback) { register_key_input_callback(Vector<Key> { key }, move(callback)); }
static StringMetrics actual_rendered_string_metrics(StringView);
static StringMetrics actual_rendered_string_metrics(Utf32View const&);
static StringMetrics actual_rendered_string_metrics(StringView, RedBlackTree<u32, Optional<Style::Mask>> const& masks = {});
static StringMetrics actual_rendered_string_metrics(Utf32View const&, RedBlackTree<u32, Optional<Style::Mask>> const& masks = {});
Function<Vector<CompletionSuggestion>(Editor const&)> on_tab_complete;
Function<void(Utf32View, Editor&)> on_paste;
@ -202,7 +203,7 @@ public:
if (m_cached_prompt_valid)
m_old_prompt_metrics = m_cached_prompt_metrics;
m_cached_prompt_valid = false;
m_cached_prompt_metrics = actual_rendered_string_metrics(prompt);
m_cached_prompt_metrics = actual_rendered_string_metrics(prompt, {});
m_new_prompt = prompt;
}
@ -260,16 +261,6 @@ private:
void set_default_keybinds();
enum VTState {
Free = 1,
Escape = 3,
Bracket = 5,
BracketArgsSemi = 7,
Title = 9,
};
static VTState actual_rendered_string_length_step(StringMetrics&, size_t, StringMetrics::LineMetrics& current_line, u32, u32, VTState);
enum LoopExitCode {
Exit = 0,
Retry
@ -366,7 +357,7 @@ private:
if (cursor > m_cursor)
cursor = m_cursor;
return current_prompt_metrics().lines_with_addition(
actual_rendered_string_metrics(buffer_view().substring_view(0, cursor)),
actual_rendered_string_metrics(buffer_view().substring_view(0, cursor), m_current_masks),
m_num_columns);
}
@ -375,7 +366,7 @@ private:
auto cursor = m_drawn_cursor;
if (cursor > m_cursor)
cursor = m_cursor;
auto buffer_metrics = actual_rendered_string_metrics(buffer_view().substring_view(0, cursor));
auto buffer_metrics = actual_rendered_string_metrics(buffer_view().substring_view(0, cursor), m_current_masks);
return current_prompt_metrics().offset_with_addition(buffer_metrics, m_num_columns);
}
@ -508,6 +499,8 @@ private:
bool contains_up_to_offset(Spans const& other, size_t offset) const;
} m_drawn_spans, m_current_spans;
RedBlackTree<u32, Optional<Style::Mask>> m_current_masks;
RefPtr<Core::Notifier> m_notifier;
Vector<u32> m_paste_buffer;

View file

@ -345,8 +345,8 @@ void Editor::enter_search()
// Manually cleanup the search line.
OutputFileStream stderr_stream { stderr };
reposition_cursor(stderr_stream);
auto search_metrics = actual_rendered_string_metrics(search_string);
auto metrics = actual_rendered_string_metrics(search_prompt);
auto search_metrics = actual_rendered_string_metrics(search_string, {});
auto metrics = actual_rendered_string_metrics(search_prompt, {});
VT::clear_lines(0, metrics.lines_with_addition(search_metrics, m_num_columns) + search_end_row - m_origin_row - 1, stderr_stream);
reposition_cursor(stderr_stream);

View file

@ -21,17 +21,7 @@ struct StringMetrics {
Vector<MaskedChar> masked_chars;
size_t length { 0 };
size_t total_length(ssize_t offset = -1) const
{
size_t length = this->length;
for (auto& mask : masked_chars) {
if (offset < 0 || mask.position <= (size_t)offset) {
length -= mask.original_length;
length += mask.masked_length;
}
}
return length;
}
size_t total_length() const { return length; }
};
Vector<LineMetrics> line_metrics;

View file

@ -106,6 +106,28 @@ public:
bool m_has_link { false };
};
struct Mask {
bool operator==(Mask const& other) const
{
return other.mode == mode && other.replacement == replacement;
}
enum class Mode {
ReplaceEntireSelection,
ReplaceEachCodePointInSelection,
};
explicit Mask(StringView replacement, Mode mode = Mode::ReplaceEntireSelection)
: replacement(replacement)
, replacement_view(this->replacement)
, mode(mode)
{
}
String replacement;
mutable Utf8View replacement_view;
Mode mode;
};
static constexpr UnderlineTag Underline {};
static constexpr BoldTag Bold {};
static constexpr ItalicTag Italic {};
@ -141,6 +163,9 @@ public:
Background background() const { return m_background; }
Foreground foreground() const { return m_foreground; }
Hyperlink hyperlink() const { return m_hyperlink; }
Optional<Mask> mask() const { return m_mask; }
void unset_mask() const { m_mask = {}; }
void set(ItalicTag const&) { m_italic = true; }
void set(BoldTag const&) { m_bold = true; }
@ -149,6 +174,7 @@ public:
void set(Foreground const& fg) { m_foreground = fg; }
void set(Hyperlink const& link) { m_hyperlink = link; }
void set(AnchoredTag const&) { m_is_anchored = true; }
void set(Mask const& mask) { m_mask = mask; }
bool is_anchored() const { return m_is_anchored; }
bool is_empty() const { return m_is_empty; }
@ -162,6 +188,7 @@ private:
Background m_background { XtermColor::Unchanged };
Foreground m_foreground { XtermColor::Unchanged };
Hyperlink m_hyperlink;
mutable Optional<Mask> m_mask;
bool m_is_anchored { false };