mirror of
https://github.com/RGBCube/serenity
synced 2025-07-25 15:37:46 +00:00
LibLine: Add support for user-controlled masking
This commit is contained in:
parent
c257d27f0b
commit
78dc77f7e4
5 changed files with 212 additions and 59 deletions
|
@ -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,8 +1417,27 @@ void Editor::refresh_display()
|
|||
};
|
||||
|
||||
auto print_character_at = [&](size_t i) {
|
||||
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;
|
||||
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)
|
||||
|
@ -1414,6 +1455,10 @@ void Editor::refresh_display()
|
|||
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)
|
||||
// just append the new text with the appropriate styles.
|
||||
|
@ -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')
|
||||
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
|
||||
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')
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 };
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue