mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-01-23 17:52:26 -05:00
LibLine: Support multiline editing
This commit also updates Shell, which uses actual_rendered_length.
This commit is contained in:
parent
22b20c381f
commit
a6fd969d93
Notes:
sideshowbarker
2024-07-19 05:21:25 +09:00
Author: https://github.com/alimpfard Commit: https://github.com/SerenityOS/serenity/commit/a6fd969d936 Pull-request: https://github.com/SerenityOS/serenity/pull/2632
3 changed files with 181 additions and 87 deletions
|
@ -847,12 +847,13 @@ void Editor::handle_read_event()
|
|||
|
||||
// Manually cleanup the search line.
|
||||
reposition_cursor();
|
||||
auto search_string_codepoint_length = Utf8View { search_string }.length_in_codepoints();
|
||||
VT::clear_lines(0, (search_string_codepoint_length + actual_rendered_string_length(search_prompt) + m_num_columns - 1) / m_num_columns);
|
||||
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));
|
||||
|
||||
reposition_cursor();
|
||||
|
||||
if (!m_reset_buffer_on_search_end || search_string_codepoint_length == 0) {
|
||||
if (!m_reset_buffer_on_search_end || search_metrics.total_length == 0) {
|
||||
// If the entry was empty, or we purposely quit without a newline,
|
||||
// do not return anything; instead, just end the search.
|
||||
end_search();
|
||||
|
@ -938,8 +939,8 @@ void Editor::recalculate_origin()
|
|||
// the new size is smaller than our prompt, which would
|
||||
// cause said prompt to take up more space, so we should
|
||||
// compensate for that.
|
||||
if (m_cached_prompt_length >= m_num_columns) {
|
||||
auto added_lines = (m_cached_prompt_length + 1) / m_num_columns - 1;
|
||||
if (m_cached_prompt_metrics.max_line_length >= m_num_columns) {
|
||||
auto added_lines = (m_cached_prompt_metrics.max_line_length + 1) / m_num_columns - 1;
|
||||
m_origin_row += added_lines;
|
||||
}
|
||||
|
||||
|
@ -949,11 +950,15 @@ void Editor::recalculate_origin()
|
|||
}
|
||||
void Editor::cleanup()
|
||||
{
|
||||
VT::move_relative(0, m_pending_chars.size() - m_chars_inserted_in_the_middle);
|
||||
VT::move_relative(-m_extra_forward_lines, m_pending_chars.size() - m_chars_inserted_in_the_middle);
|
||||
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_old_prompt_length - m_pending_chars.size() + m_chars_inserted_in_the_middle);
|
||||
// There's a newline at the top, don't clear that line.
|
||||
if (current_prompt_metrics().line_lengths.first() == 0)
|
||||
--current_line;
|
||||
VT::clear_lines(current_line - 1, num_lines() - current_line + m_extra_forward_lines);
|
||||
m_extra_forward_lines = 0;
|
||||
reposition_cursor();
|
||||
};
|
||||
|
||||
void Editor::refresh_display()
|
||||
|
@ -990,7 +995,7 @@ void Editor::refresh_display()
|
|||
if (m_cached_prompt_valid && !m_refresh_needed && m_pending_chars.size() == 0) {
|
||||
// Probably just moving around.
|
||||
reposition_cursor();
|
||||
m_cached_buffer_size = m_buffer.size();
|
||||
m_cached_buffer_metrics = actual_rendered_string_metrics(buffer_view());
|
||||
return;
|
||||
}
|
||||
// We might be at the last line, and have more than one line;
|
||||
|
@ -1016,14 +1021,13 @@ void Editor::refresh_display()
|
|||
fputs((char*)m_pending_chars.data(), stdout);
|
||||
m_pending_chars.clear();
|
||||
m_drawn_cursor = m_cursor;
|
||||
m_cached_buffer_size = m_buffer.size();
|
||||
m_cached_buffer_metrics = actual_rendered_string_metrics(buffer_view());
|
||||
fflush(stdout);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Ouch, reflow entire line.
|
||||
// FIXME: handle multiline stuff
|
||||
if (!has_cleaned_up) {
|
||||
cleanup();
|
||||
}
|
||||
|
@ -1078,7 +1082,7 @@ void Editor::refresh_display()
|
|||
|
||||
m_pending_chars.clear();
|
||||
m_refresh_needed = false;
|
||||
m_cached_buffer_size = m_buffer.size();
|
||||
m_cached_buffer_metrics = actual_rendered_string_metrics(buffer_view());
|
||||
m_chars_inserted_in_the_middle = 0;
|
||||
if (!m_cached_prompt_valid) {
|
||||
m_cached_prompt_valid = true;
|
||||
|
@ -1303,70 +1307,101 @@ void VT::clear_to_end_of_line()
|
|||
fflush(stdout);
|
||||
}
|
||||
|
||||
size_t Editor::actual_rendered_string_length(const StringView& string) const
|
||||
Editor::StringMetrics Editor::actual_rendered_string_metrics(const StringView& string) const
|
||||
{
|
||||
size_t length { 0 };
|
||||
enum VTState {
|
||||
Free = 1,
|
||||
Escape = 3,
|
||||
Bracket = 5,
|
||||
BracketArgsSemi = 7,
|
||||
Title = 9,
|
||||
} state { Free };
|
||||
StringMetrics metrics;
|
||||
VTState state { Free };
|
||||
Utf8View view { string };
|
||||
auto it = view.begin();
|
||||
|
||||
for (size_t i = 0; i < view.length_in_codepoints(); ++i, ++it) {
|
||||
for (; it != view.end(); ++it) {
|
||||
auto c = *it;
|
||||
switch (state) {
|
||||
case Free:
|
||||
if (c == '\x1b') { // escape
|
||||
state = Escape;
|
||||
continue;
|
||||
}
|
||||
if (c == '\r' || c == '\n') { // return or carriage return
|
||||
// Reset length to 0, since we either overwrite, or are on a newline.
|
||||
length = 0;
|
||||
continue;
|
||||
}
|
||||
// FIXME: This will not support anything sophisticated
|
||||
++length;
|
||||
break;
|
||||
case Escape:
|
||||
if (c == ']') {
|
||||
++i;
|
||||
++it;
|
||||
if (*it == '0')
|
||||
state = Title;
|
||||
continue;
|
||||
}
|
||||
if (c == '[') {
|
||||
state = Bracket;
|
||||
continue;
|
||||
}
|
||||
// FIXME: This does not support non-VT (aside from set-title) escapes
|
||||
break;
|
||||
case Bracket:
|
||||
if (isdigit(c)) {
|
||||
state = BracketArgsSemi;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
case BracketArgsSemi:
|
||||
if (c == ';') {
|
||||
state = Bracket;
|
||||
continue;
|
||||
}
|
||||
if (!isdigit(c))
|
||||
state = Free;
|
||||
break;
|
||||
case Title:
|
||||
if (c == 7)
|
||||
state = Free;
|
||||
break;
|
||||
}
|
||||
auto it_copy = it;
|
||||
++it_copy;
|
||||
auto next_c = it_copy == view.end() ? 0 : *it_copy;
|
||||
state = actual_rendered_string_length_step(metrics, length, c, next_c, state);
|
||||
}
|
||||
return length;
|
||||
|
||||
metrics.line_lengths.append(length);
|
||||
|
||||
for (auto& line : metrics.line_lengths)
|
||||
metrics.max_line_length = max(line, metrics.max_line_length);
|
||||
|
||||
return metrics;
|
||||
}
|
||||
|
||||
Editor::StringMetrics Editor::actual_rendered_string_metrics(const Utf32View& view) const
|
||||
{
|
||||
size_t length { 0 };
|
||||
StringMetrics metrics;
|
||||
VTState state { Free };
|
||||
|
||||
for (size_t i = 0; i < view.length(); ++i) {
|
||||
auto c = view.codepoints()[i];
|
||||
auto next_c = i + 1 < view.length() ? view.codepoints()[i + 1] : 0;
|
||||
state = actual_rendered_string_length_step(metrics, length, c, next_c, state);
|
||||
}
|
||||
|
||||
metrics.line_lengths.append(length);
|
||||
|
||||
for (auto& line : metrics.line_lengths)
|
||||
metrics.max_line_length = max(line, metrics.max_line_length);
|
||||
|
||||
return metrics;
|
||||
}
|
||||
|
||||
Editor::VTState Editor::actual_rendered_string_length_step(StringMetrics& metrics, size_t& length, u32 c, u32 next_c, VTState state) const
|
||||
{
|
||||
switch (state) {
|
||||
case Free:
|
||||
if (c == '\x1b') { // escape
|
||||
return Escape;
|
||||
}
|
||||
if (c == '\r') { // carriage return
|
||||
length = 0;
|
||||
if (!metrics.line_lengths.is_empty())
|
||||
metrics.line_lengths.last() = 0;
|
||||
return state;
|
||||
}
|
||||
if (c == '\n') { // return
|
||||
metrics.line_lengths.append(length);
|
||||
length = 0;
|
||||
return state;
|
||||
}
|
||||
// FIXME: This will not support anything sophisticated
|
||||
++length;
|
||||
++metrics.total_length;
|
||||
return state;
|
||||
case Escape:
|
||||
if (c == ']') {
|
||||
if (next_c == '0')
|
||||
state = Title;
|
||||
return state;
|
||||
}
|
||||
if (c == '[') {
|
||||
return Bracket;
|
||||
}
|
||||
// FIXME: This does not support non-VT (aside from set-title) escapes
|
||||
return state;
|
||||
case Bracket:
|
||||
if (isdigit(c)) {
|
||||
return BracketArgsSemi;
|
||||
}
|
||||
return state;
|
||||
case BracketArgsSemi:
|
||||
if (c == ';') {
|
||||
return Bracket;
|
||||
}
|
||||
if (!isdigit(c))
|
||||
state = Free;
|
||||
return state;
|
||||
case Title:
|
||||
if (c == 7)
|
||||
state = Free;
|
||||
return state;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
Vector<size_t, 2> Editor::vt_dsr()
|
||||
|
@ -1471,7 +1506,10 @@ 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);
|
||||
auto cp = m_buffer[index];
|
||||
m_buffer.remove(index);
|
||||
if (cp == '\n')
|
||||
++m_extra_forward_lines;
|
||||
}
|
||||
|
||||
void Editor::readjust_anchored_styles(size_t hint_index, ModificationKind modification)
|
||||
|
@ -1518,4 +1556,21 @@ void Editor::readjust_anchored_styles(size_t hint_index, ModificationKind modifi
|
|||
stylize(relocation.new_span, relocation.style);
|
||||
}
|
||||
}
|
||||
|
||||
size_t Editor::StringMetrics::lines_with_addition(const StringMetrics& offset, size_t column_width) const
|
||||
{
|
||||
size_t lines = 0;
|
||||
|
||||
for (size_t i = 0; i < line_lengths.size() - 1; ++i)
|
||||
lines += (line_lengths[i] + column_width) / column_width;
|
||||
|
||||
auto last = line_lengths.last();
|
||||
last += offset.line_lengths.first();
|
||||
lines += (last + column_width) / column_width;
|
||||
|
||||
for (size_t i = 1; i < offset.line_lengths.size(); ++i)
|
||||
lines += (offset.line_lengths[i] + column_width) / column_width;
|
||||
|
||||
return lines;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -104,7 +104,22 @@ public:
|
|||
const Vector<String>& history() const { return m_history; }
|
||||
|
||||
void register_character_input_callback(char ch, Function<bool(Editor&)> callback);
|
||||
size_t actual_rendered_string_length(const StringView& string) const;
|
||||
struct StringMetrics {
|
||||
Vector<size_t> line_lengths;
|
||||
size_t total_length { 0 };
|
||||
size_t max_line_length { 0 };
|
||||
|
||||
size_t lines_with_addition(const StringMetrics& offset, size_t column_width) const;
|
||||
void reset()
|
||||
{
|
||||
line_lengths.clear();
|
||||
total_length = 0;
|
||||
max_line_length = 0;
|
||||
line_lengths.append(0);
|
||||
}
|
||||
};
|
||||
StringMetrics actual_rendered_string_metrics(const StringView&) const;
|
||||
StringMetrics actual_rendered_string_metrics(const Utf32View&) const;
|
||||
|
||||
Function<Vector<CompletionSuggestion>(const Editor&)> on_tab_complete;
|
||||
Function<void()> on_interrupt_handled;
|
||||
|
@ -135,9 +150,9 @@ public:
|
|||
void set_prompt(const String& prompt)
|
||||
{
|
||||
if (m_cached_prompt_valid)
|
||||
m_old_prompt_length = m_cached_prompt_length;
|
||||
m_old_prompt_metrics = m_cached_prompt_metrics;
|
||||
m_cached_prompt_valid = false;
|
||||
m_cached_prompt_length = actual_rendered_string_length(prompt);
|
||||
m_cached_prompt_metrics = actual_rendered_string_metrics(prompt);
|
||||
m_new_prompt = prompt;
|
||||
}
|
||||
|
||||
|
@ -168,9 +183,21 @@ public:
|
|||
|
||||
bool is_editing() const { return m_is_editing; }
|
||||
|
||||
const Utf32View buffer_view() const { return { m_buffer.data(), m_buffer.size() }; }
|
||||
|
||||
private:
|
||||
explicit Editor(Configuration configuration = {});
|
||||
|
||||
enum VTState {
|
||||
Free = 1,
|
||||
Escape = 3,
|
||||
Bracket = 5,
|
||||
BracketArgsSemi = 7,
|
||||
Title = 9,
|
||||
};
|
||||
|
||||
VTState actual_rendered_string_length_step(StringMetrics&, size_t& length, u32, u32, VTState) const;
|
||||
|
||||
// ^Core::Object
|
||||
virtual void save_to(JsonObject&) override;
|
||||
|
||||
|
@ -215,12 +242,12 @@ private:
|
|||
|
||||
void reset()
|
||||
{
|
||||
m_cached_buffer_size = 0;
|
||||
m_cached_buffer_metrics.reset();
|
||||
m_cached_prompt_valid = false;
|
||||
m_cursor = 0;
|
||||
m_drawn_cursor = 0;
|
||||
m_inline_search_cursor = 0;
|
||||
m_old_prompt_length = m_cached_prompt_length;
|
||||
m_old_prompt_metrics = m_cached_prompt_metrics;
|
||||
set_origin(0, 0);
|
||||
m_prompt_lines_at_suggestion_initiation = 0;
|
||||
m_refresh_needed = true;
|
||||
|
@ -238,24 +265,36 @@ private:
|
|||
m_initialized = false;
|
||||
}
|
||||
|
||||
size_t current_prompt_length() const
|
||||
const StringMetrics& current_prompt_metrics() const
|
||||
{
|
||||
return m_cached_prompt_valid ? m_cached_prompt_length : m_old_prompt_length;
|
||||
return m_cached_prompt_valid ? m_cached_prompt_metrics : m_old_prompt_metrics;
|
||||
}
|
||||
|
||||
size_t num_lines() const
|
||||
{
|
||||
return (m_cached_buffer_size + m_num_columns + current_prompt_length() - 1) / m_num_columns;
|
||||
return current_prompt_metrics().lines_with_addition(m_cached_buffer_metrics, m_num_columns);
|
||||
}
|
||||
|
||||
size_t cursor_line() const
|
||||
{
|
||||
return (m_drawn_cursor + m_num_columns + current_prompt_length()) / m_num_columns;
|
||||
auto cursor = m_drawn_cursor;
|
||||
if (cursor > m_cursor)
|
||||
cursor = m_cursor;
|
||||
return current_prompt_metrics().lines_with_addition(
|
||||
actual_rendered_string_metrics(buffer_view().substring_view(0, cursor)),
|
||||
m_num_columns);
|
||||
}
|
||||
|
||||
size_t offset_in_line() const
|
||||
{
|
||||
return (m_drawn_cursor + current_prompt_length()) % m_num_columns;
|
||||
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));
|
||||
if (buffer_metrics.line_lengths.size() > 1)
|
||||
return buffer_metrics.line_lengths.last() % m_num_columns;
|
||||
|
||||
return (buffer_metrics.line_lengths.last() + current_prompt_metrics().line_lengths.last()) % m_num_columns;
|
||||
}
|
||||
|
||||
void set_origin()
|
||||
|
@ -305,9 +344,10 @@ private:
|
|||
size_t m_times_tab_pressed { 0 };
|
||||
size_t m_num_columns { 0 };
|
||||
size_t m_num_lines { 1 };
|
||||
size_t m_cached_prompt_length { 0 };
|
||||
size_t m_old_prompt_length { 0 };
|
||||
size_t m_cached_buffer_size { 0 };
|
||||
size_t m_extra_forward_lines { 0 };
|
||||
StringMetrics m_cached_prompt_metrics;
|
||||
StringMetrics m_old_prompt_metrics;
|
||||
StringMetrics m_cached_buffer_metrics;
|
||||
size_t m_prompt_lines_at_suggestion_initiation { 0 };
|
||||
bool m_cached_prompt_valid { false };
|
||||
|
||||
|
|
|
@ -126,7 +126,8 @@ String Shell::prompt() const
|
|||
};
|
||||
|
||||
auto the_prompt = build_prompt();
|
||||
auto prompt_length = editor->actual_rendered_string_length(the_prompt);
|
||||
auto prompt_metrics = editor->actual_rendered_string_metrics(the_prompt);
|
||||
auto prompt_length = prompt_metrics.line_lengths.last();
|
||||
|
||||
if (m_should_continue != ExitCodeOrContinuationRequest::Nothing) {
|
||||
const auto format_string = "\033[34m%.*-s\033[m";
|
||||
|
@ -1769,10 +1770,8 @@ bool Shell::read_single_line()
|
|||
if (line.is_empty())
|
||||
return true;
|
||||
|
||||
// FIXME: This might be a bit counter-intuitive, since we put nothing
|
||||
// between the two lines, even though the user has pressed enter
|
||||
// but since the LineEditor cannot yet handle literal newlines
|
||||
// inside the text, we opt to do this the wrong way (for the time being)
|
||||
if (!m_complete_line_builder.is_empty())
|
||||
m_complete_line_builder.append("\n");
|
||||
m_complete_line_builder.append(line);
|
||||
|
||||
auto complete_or_exit_code = run_command(m_complete_line_builder.string_view());
|
||||
|
|
Loading…
Add table
Reference in a new issue