diff --git a/Libraries/LibLine/Editor.cpp b/Libraries/LibLine/Editor.cpp index f3728f67b8..892d9a6880 100644 --- a/Libraries/LibLine/Editor.cpp +++ b/Libraries/LibLine/Editor.cpp @@ -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 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; +} } diff --git a/Libraries/LibLine/Editor.h b/Libraries/LibLine/Editor.h index 29bdf381ab..50c73e1482 100644 --- a/Libraries/LibLine/Editor.h +++ b/Libraries/LibLine/Editor.h @@ -104,7 +104,22 @@ public: const Vector& history() const { return m_history; } void register_character_input_callback(char ch, Function callback); - size_t actual_rendered_string_length(const StringView& string) const; + struct StringMetrics { + Vector 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(const Editor&)> on_tab_complete; Function 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 }; diff --git a/Shell/Shell.cpp b/Shell/Shell.cpp index ef210441e0..db41a684d4 100644 --- a/Shell/Shell.cpp +++ b/Shell/Shell.cpp @@ -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());