diff --git a/Libraries/LibLine/CMakeLists.txt b/Libraries/LibLine/CMakeLists.txt index 91fb113d3d9..7540a5a8f10 100644 --- a/Libraries/LibLine/CMakeLists.txt +++ b/Libraries/LibLine/CMakeLists.txt @@ -1,5 +1,7 @@ set(SOURCES Editor.cpp + SuggestionManager.cpp + XtermSuggestionDisplay.cpp ) serenity_lib(LibLine line) diff --git a/Libraries/LibLine/Editor.cpp b/Libraries/LibLine/Editor.cpp index 38a3e208905..db63696561e 100644 --- a/Libraries/LibLine/Editor.cpp +++ b/Libraries/LibLine/Editor.cpp @@ -52,6 +52,7 @@ Editor::Editor(Configuration configuration) m_num_columns = ws.ws_col; m_num_lines = ws.ws_row; } + m_suggestion_display = make(m_num_lines, m_num_columns); } Editor::~Editor() @@ -210,7 +211,6 @@ void Editor::stylize(const Span& span, const Style& style) void Editor::suggest(size_t invariant_offset, size_t static_offset, Span::Mode offset_mode) const { - m_next_suggestion_index = 0; auto internal_static_offset = static_offset; auto internal_invariant_offset = invariant_offset; if (offset_mode == Span::Mode::ByteOriented) { @@ -221,8 +221,7 @@ void Editor::suggest(size_t invariant_offset, size_t static_offset, Span::Mode o internal_static_offset = offsets.start; internal_invariant_offset = offsets.end - offsets.start; } - m_next_suggestion_static_offset = internal_static_offset; - m_next_suggestion_invariant_offset = internal_invariant_offset; + m_suggestion_manager.set_suggestion_variants(internal_static_offset, internal_invariant_offset, 0); } String Editor::get_line(const String& prompt) @@ -293,17 +292,6 @@ String Editor::get_line(const String& prompt) exit(0); auto reverse_tab = false; - auto increment_suggestion_index = [&] { - if (m_suggestions.size()) - m_next_suggestion_index = (m_next_suggestion_index + 1) % m_suggestions.size(); - else - m_next_suggestion_index = 0; - }; - auto decrement_suggestion_index = [&] { - if (m_next_suggestion_index == 0) - m_next_suggestion_index = m_suggestions.size(); - m_next_suggestion_index--; - }; auto ctrl_held = false; // discard starting bytes until they make sense as utf-8 @@ -497,239 +485,102 @@ String Editor::get_line(const String& prompt) // and scan for the largest common prefix to display // further tabs simply show the cached completions if (m_times_tab_pressed == 1) { - m_suggestions = on_tab_complete(*this); - size_t common_suggestion_prefix { 0 }; - if (m_suggestions.size() == 1) { - m_largest_common_suggestion_prefix_length = m_suggestions[0].text_view.length(); - } else if (m_suggestions.size()) { - u32 last_valid_suggestion_codepoint; - - for (;; ++common_suggestion_prefix) { - if (m_suggestions[0].text_view.length() <= common_suggestion_prefix) - goto no_more_commons; - - last_valid_suggestion_codepoint = m_suggestions[0].text_view.codepoints()[common_suggestion_prefix]; - - for (auto& suggestion : m_suggestions) { - if (suggestion.text_view.length() <= common_suggestion_prefix || suggestion.text_view.codepoints()[common_suggestion_prefix] != last_valid_suggestion_codepoint) { - goto no_more_commons; - } - } - } - no_more_commons:; - m_largest_common_suggestion_prefix_length = common_suggestion_prefix; - } else { - m_largest_common_suggestion_prefix_length = 0; + m_suggestion_manager.set_suggestions(on_tab_complete(*this)); + m_prompt_lines_at_suggestion_initiation = num_lines(); + if (m_suggestion_manager.count() == 0) { // there are no suggestions, beep~ putchar('\a'); fflush(stdout); } - m_prompt_lines_at_suggestion_initiation = num_lines(); } // Adjust already incremented / decremented index when switching tab direction if (reverse_tab && m_tab_direction != TabDirection::Backward) { - decrement_suggestion_index(); - decrement_suggestion_index(); + m_suggestion_manager.previous(); + m_suggestion_manager.previous(); m_tab_direction = TabDirection::Backward; } if (!reverse_tab && m_tab_direction != TabDirection::Forward) { - increment_suggestion_index(); - increment_suggestion_index(); + m_suggestion_manager.next(); + m_suggestion_manager.next(); m_tab_direction = TabDirection::Forward; } reverse_tab = false; - auto current_suggestion_index = m_next_suggestion_index; - if (m_next_suggestion_index < m_suggestions.size()) { - auto can_complete = m_next_suggestion_invariant_offset <= m_largest_common_suggestion_prefix_length; - if (!m_last_shown_suggestion.text.is_null()) { - size_t actual_offset; - size_t shown_length = m_last_shown_suggestion_display_length; - switch (m_times_tab_pressed) { - case 1: - actual_offset = m_cursor; - break; - case 2: - actual_offset = m_cursor - m_largest_common_suggestion_prefix_length + m_next_suggestion_invariant_offset; - if (can_complete) - shown_length = m_largest_common_suggestion_prefix_length + m_last_shown_suggestion.trivia_view.length(); - break; - default: - if (m_last_shown_suggestion_display_length == 0) - actual_offset = m_cursor; - else - actual_offset = m_cursor - m_last_shown_suggestion_display_length + m_next_suggestion_invariant_offset; - break; - } + auto completion_mode = m_times_tab_pressed == 1 ? SuggestionManager::CompletePrefix : m_times_tab_pressed == 2 ? SuggestionManager::ShowSuggestions : SuggestionManager::CycleSuggestions; - for (size_t i = m_next_suggestion_invariant_offset; i < shown_length; ++i) - remove_at_index(actual_offset); + auto completion_result = m_suggestion_manager.attempt_completion(completion_mode, token_start); - m_cursor = actual_offset; - m_inline_search_cursor = m_cursor; - m_refresh_needed = true; + auto new_cursor = m_cursor + completion_result.new_cursor_offset; + for (size_t i = completion_result.offset_region_to_remove.start; i < completion_result.offset_region_to_remove.end; ++i) + remove_at_index(new_cursor); + + m_cursor = new_cursor; + m_inline_search_cursor = new_cursor; + m_refresh_needed = true; + + for (auto& view : completion_result.insert) + insert(view); + + if (completion_result.style_to_apply.has_value()) { + // Apply the style of the last suggestion + readjust_anchored_styles(m_suggestion_manager.current_suggestion().start_index, ModificationKind::ForcedOverlapRemoval); + stylize({ m_suggestion_manager.current_suggestion().start_index, m_cursor, Span::Mode::CodepointOriented }, completion_result.style_to_apply.value()); + } + + switch (completion_result.new_completion_mode) { + case SuggestionManager::DontComplete: + m_times_tab_pressed = 0; + break; + case SuggestionManager::CompletePrefix: + break; + default: + ++m_times_tab_pressed; + break; + } + + if (m_times_tab_pressed > 1) { + if (m_suggestion_manager.count() > 0) { + if (m_suggestion_display->cleanup()) + reposition_cursor(); + + m_suggestion_display->set_initial_prompt_lines(m_prompt_lines_at_suggestion_initiation); + + m_suggestion_display->display(m_suggestion_manager); + + m_origin_x = m_suggestion_display->origin_x(); } - m_last_shown_suggestion = m_suggestions[m_next_suggestion_index]; + } - if (m_last_shown_suggestion_display_length) - m_last_shown_suggestion.token_start_index = token_start - m_next_suggestion_static_offset - m_last_shown_suggestion_display_length; + if (m_times_tab_pressed > 2) { + if (m_tab_direction == TabDirection::Forward) + m_suggestion_manager.next(); else - m_last_shown_suggestion.token_start_index = token_start - m_next_suggestion_static_offset - m_next_suggestion_invariant_offset; - - m_last_shown_suggestion_display_length = m_last_shown_suggestion.text_view.length(); - m_last_shown_suggestion_was_complete = true; - if (m_times_tab_pressed == 1) { - // This is the first time, so only auto-complete *if possible* - if (can_complete) { - insert(m_last_shown_suggestion.text_view.substring_view(m_next_suggestion_invariant_offset, m_largest_common_suggestion_prefix_length - m_next_suggestion_invariant_offset)); - m_last_shown_suggestion_display_length = m_largest_common_suggestion_prefix_length; - // do not increment the suggestion index, as the first tab should only be a *peek* - if (m_suggestions.size() == 1) { - // if there's one suggestion, commit and forget - m_times_tab_pressed = 0; - // add in the trivia of the last selected suggestion - insert(m_last_shown_suggestion.trivia_view); - m_last_shown_suggestion_display_length = 0; - readjust_anchored_styles(m_last_shown_suggestion.token_start_index, ModificationKind::ForcedOverlapRemoval); - stylize({ m_last_shown_suggestion.token_start_index, m_cursor, Span::Mode::CodepointOriented }, m_last_shown_suggestion.style); - } - } else { - m_last_shown_suggestion_display_length = 0; - } - ++m_times_tab_pressed; - m_last_shown_suggestion_was_complete = false; - } else { - insert(m_last_shown_suggestion.text_view.substring_view(m_next_suggestion_invariant_offset, m_last_shown_suggestion.text_view.length() - m_next_suggestion_invariant_offset)); - // add in the trivia of the last selected suggestion - insert(m_last_shown_suggestion.trivia_view); - m_last_shown_suggestion_display_length += m_last_shown_suggestion.trivia_view.length(); - if (m_tab_direction == TabDirection::Forward) - increment_suggestion_index(); - else - decrement_suggestion_index(); - } - } else { - m_next_suggestion_index = 0; + m_suggestion_manager.previous(); } - if (m_times_tab_pressed > 1 && !m_suggestions.is_empty()) { - size_t longest_suggestion_length = 0; - size_t longest_suggestion_byte_length = 0; - size_t start_index = 0; - - for (auto& suggestion : m_suggestions) { - if (start_index++ < m_last_displayed_suggestion_index) - continue; - longest_suggestion_length = max(longest_suggestion_length, suggestion.text_view.length()); - longest_suggestion_byte_length = max(longest_suggestion_byte_length, suggestion.text_string.length()); - } - - size_t num_printed = 0; - size_t lines_used { 1 }; - size_t index { 0 }; - vt_save_cursor(); - vt_clear_lines(0, m_lines_used_for_last_suggestions); - vt_restore_cursor(); - auto spans_entire_line { false }; - auto max_line_count = (m_cached_prompt_length + longest_suggestion_length + m_num_columns - 1) / m_num_columns; - if (longest_suggestion_length >= m_num_columns - 2) { - spans_entire_line = true; - // we should make enough space for the biggest entry in - // the suggestion list to fit in the prompt line - auto start = max_line_count - m_prompt_lines_at_suggestion_initiation; - for (size_t i = start; i < max_line_count; ++i) { - putchar('\n'); - } - lines_used += max_line_count; - longest_suggestion_length = 0; - } - vt_move_absolute(max_line_count + m_origin_x, 1); - for (auto& suggestion : m_suggestions) { - if (index < m_last_displayed_suggestion_index) { - ++index; - continue; - } - size_t next_column = num_printed + suggestion.text_view.length() + longest_suggestion_length + 2; - - if (next_column > m_num_columns) { - auto lines = (suggestion.text_view.length() + m_num_columns - 1) / m_num_columns; - lines_used += lines; - putchar('\n'); - num_printed = 0; - } - - // show just enough suggestions to fill up the screen - // without moving the prompt out of view - if (lines_used + m_prompt_lines_at_suggestion_initiation >= m_num_lines) - break; - - // only apply colour to the selection if something is *actually* added to the buffer - if (m_last_shown_suggestion_was_complete && index == current_suggestion_index) { - vt_apply_style({ Style::Foreground(Style::XtermColor::Blue) }); - fflush(stdout); - } - - if (spans_entire_line) { - num_printed += m_num_columns; - fprintf(stderr, "%s", suggestion.text_string.characters()); - } else { - fprintf(stderr, "%-*s", static_cast(longest_suggestion_byte_length) + 2, suggestion.text_string.characters()); - num_printed += longest_suggestion_length + 2; - } - - if (m_last_shown_suggestion_was_complete && index == current_suggestion_index) { - vt_apply_style(Style::reset_style()); - fflush(stdout); - } - - ++index; - } - m_lines_used_for_last_suggestions = lines_used; - - // if we filled the screen, move back the origin - if (m_origin_x + lines_used >= m_num_lines) { - m_origin_x = m_num_lines - lines_used; - } - - --index; - // cycle pages of suggestions - if (index == current_suggestion_index) - m_last_displayed_suggestion_index = index; - - if (m_last_displayed_suggestion_index >= m_suggestions.size() - 1) - m_last_displayed_suggestion_index = 0; - } - if (m_suggestions.size() < 2) { + if (m_suggestion_manager.count() < 2) { // we have none, or just one suggestion // we should just commit that and continue // after it, as if it were auto-completed suggest(0, 0, Span::CodepointOriented); - m_last_shown_suggestion = String::empty(); - m_last_shown_suggestion_display_length = 0; - m_suggestions.clear(); m_times_tab_pressed = 0; - m_last_displayed_suggestion_index = 0; + m_suggestion_manager.reset(); } continue; } if (m_times_tab_pressed) { // Apply the style of the last suggestion - readjust_anchored_styles(m_last_shown_suggestion.token_start_index, ModificationKind::ForcedOverlapRemoval); - stylize({ m_last_shown_suggestion.token_start_index, m_cursor, Span::Mode::CodepointOriented }, m_last_shown_suggestion.style); + readjust_anchored_styles(m_suggestion_manager.current_suggestion().start_index, ModificationKind::ForcedOverlapRemoval); + stylize({ m_suggestion_manager.current_suggestion().start_index, m_cursor, Span::Mode::CodepointOriented }, m_suggestion_manager.current_suggestion().style); // we probably have some suggestions drawn // let's clean them up - if (m_lines_used_for_last_suggestions) { - vt_clear_lines(0, m_lines_used_for_last_suggestions); + if (m_suggestion_display->cleanup()) { reposition_cursor(); m_refresh_needed = true; - m_lines_used_for_last_suggestions = 0; } - m_last_shown_suggestion_display_length = 0; - m_last_shown_suggestion = String::empty(); - m_last_displayed_suggestion_index = 0; - m_suggestions.clear(); + m_suggestion_manager.reset(); suggest(0, 0, Span::CodepointOriented); } m_times_tab_pressed = 0; // Safe to say if we get here, the user didn't press TAB @@ -777,9 +628,8 @@ String Editor::get_line(const String& prompt) // ^L if (codepoint == 0xc) { printf("\033[3J\033[H\033[2J"); // Clear screen. - vt_move_absolute(1, 1); - m_origin_x = 1; - m_origin_y = 1; + VT::move_absolute(1, 1); + set_origin(1, 1); m_refresh_needed = true; continue; } @@ -835,15 +685,13 @@ String Editor::get_line(const String& prompt) printf("\033[3J\033[H\033[2J"); // Clear screen. // refresh our own prompt - m_origin_x = 1; - m_origin_y = 1; + set_origin(1, 1); m_refresh_needed = true; refresh_display(); // move the search prompt below ours // and tell it to redraw itself - search_editor.m_origin_x = 2; - search_editor.m_origin_y = 1; + search_editor.set_origin(2, 1); search_editor.m_refresh_needed = true; return false; @@ -869,7 +717,7 @@ String Editor::get_line(const String& prompt) // 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); + VT::clear_lines(0, (search_string_codepoint_length + actual_rendered_string_length(search_prompt) + m_num_columns - 1) / m_num_columns); reposition_cursor(); @@ -970,11 +818,11 @@ void Editor::recalculate_origin() } void Editor::cleanup() { - vt_move_relative(0, m_pending_chars.size() - m_chars_inserted_in_the_middle); + VT::move_relative(0, 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); + 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); }; void Editor::refresh_display() @@ -993,6 +841,7 @@ void Editor::refresh_display() m_num_columns = ws.ws_col; m_num_lines = ws.ws_row; } + m_suggestion_display->set_vt_size(m_num_lines, m_num_columns); if (previous_num_columns != m_num_columns) { // we need to cleanup and redo everything @@ -1036,11 +885,11 @@ void Editor::refresh_display() if (!has_cleaned_up) { cleanup(); } - vt_move_absolute(m_origin_x, m_origin_y); + VT::move_absolute(m_origin_x, m_origin_y); fputs(m_new_prompt.characters(), stdout); - vt_clear_to_end_of_line(); + VT::clear_to_end_of_line(); HashMap empty_styles {}; StringBuilder builder; for (size_t i = 0; i < m_buffer.size(); ++i) { @@ -1060,11 +909,11 @@ void Editor::refresh_display() style.unify_with(applicable_style.value); // Disable any style that should be turned off - vt_apply_style(style, false); + VT::apply_style(style, false); // go back to defaults style = find_applicable_style(i); - vt_apply_style(style, true); + VT::apply_style(style, true); } if (starts.size() || anchored_starts.size()) { Style style; @@ -1076,14 +925,14 @@ void Editor::refresh_display() style.unify_with(applicable_style.value); // set new options - vt_apply_style(style, true); + VT::apply_style(style, true); } builder.clear(); builder.append(Utf32View { &m_buffer[i], 1 }); fputs(builder.to_string().characters(), stdout); } - vt_apply_style(Style::reset_style()); // don't bleed to EOL + VT::apply_style(Style::reset_style()); // don't bleed to EOL m_pending_chars.clear(); m_refresh_needed = false; @@ -1117,16 +966,16 @@ void Editor::reposition_cursor() auto line = cursor_line() - 1; auto column = offset_in_line(); - vt_move_absolute(line + m_origin_x, column + m_origin_y); + VT::move_absolute(line + m_origin_x, column + m_origin_y); } -void Editor::vt_move_absolute(u32 x, u32 y) +void VT::move_absolute(u32 x, u32 y) { printf("\033[%d;%dH", x, y); fflush(stdout); } -void Editor::vt_move_relative(int x, int y) +void VT::move_relative(int x, int y) { char x_op = 'A', y_op = 'D'; @@ -1268,7 +1117,7 @@ String Style::to_string() const return builder.build(); } -void Editor::vt_apply_style(const Style& style, bool is_starting) +void VT::apply_style(const Style& style, bool is_starting) { if (is_starting) { printf( @@ -1284,7 +1133,7 @@ void Editor::vt_apply_style(const Style& style, bool is_starting) } } -void Editor::vt_clear_lines(size_t count_above, size_t count_below) +void VT::clear_lines(size_t count_above, size_t count_below) { // go down count_below lines if (count_below > 0) @@ -1294,19 +1143,19 @@ void Editor::vt_clear_lines(size_t count_above, size_t count_below) fputs(i == 1 ? "\033[2K" : "\033[2K\033[A", stdout); } -void Editor::vt_save_cursor() +void VT::save_cursor() { fputs("\033[s", stdout); fflush(stdout); } -void Editor::vt_restore_cursor() +void VT::restore_cursor() { fputs("\033[u", stdout); fflush(stdout); } -void Editor::vt_clear_to_end_of_line() +void VT::clear_to_end_of_line() { fputs("\033[K", stdout); fflush(stdout); diff --git a/Libraries/LibLine/Editor.h b/Libraries/LibLine/Editor.h index 66d3ce0a2ed..ccbb77d5c6e 100644 --- a/Libraries/LibLine/Editor.h +++ b/Libraries/LibLine/Editor.h @@ -40,59 +40,14 @@ #include #include #include +#include +#include +#include #include #include namespace Line { -class Editor; - -// FIXME: These objects are pretty heavy since they store two copies of text -// somehow get rid of one -struct CompletionSuggestion { - friend class Editor; - // intentionally not explicit (allows suggesting bare strings) - CompletionSuggestion(const String& completion) - : CompletionSuggestion(completion, "", {}) - { - } - CompletionSuggestion(const StringView& completion, const StringView& trailing_trivia) - : CompletionSuggestion(completion, trailing_trivia, {}) - { - } - CompletionSuggestion(const StringView& completion, const StringView& trailing_trivia, Style style) - : style(style) - , text_string(completion) - { - Utf8View text_u8 { completion }; - Utf8View trivia_u8 { trailing_trivia }; - - for (auto cp : text_u8) - text.append(cp); - - for (auto cp : trivia_u8) - this->trailing_trivia.append(cp); - - text_view = Utf32View { text.data(), text.size() }; - trivia_view = Utf32View { this->trailing_trivia.data(), this->trailing_trivia.size() }; - } - - bool operator==(const CompletionSuggestion& suggestion) const - { - return suggestion.text == text; - } - - Vector text; - Vector trailing_trivia; - Style style; - size_t token_start_index { 0 }; - -private: - Utf32View text_view; - Utf32View trivia_view; - String text_string; -}; - struct Configuration { enum TokenSplitMechanism { Spaces, @@ -215,13 +170,6 @@ private: Function callback; }; - void vt_save_cursor(); - void vt_restore_cursor(); - void vt_clear_to_end_of_line(); - void vt_clear_lines(size_t count_above, size_t count_below = 0); - void vt_move_relative(int x, int y); - void vt_move_absolute(u32 x, u32 y); - void vt_apply_style(const Style&, bool is_starting = true); Vector vt_dsr(); void remove_at_index(size_t); @@ -258,8 +206,7 @@ private: m_drawn_cursor = 0; m_inline_search_cursor = 0; m_old_prompt_length = m_cached_prompt_length; - m_origin_x = 0; - m_origin_y = 0; + set_origin(0, 0); m_prompt_lines_at_suggestion_initiation = 0; m_refresh_needed = true; } @@ -297,8 +244,14 @@ private: void set_origin() { auto position = vt_dsr(); - m_origin_x = position[0]; - m_origin_y = position[1]; + set_origin(position[0], position[1]); + } + + void set_origin(int x, int y) + { + m_origin_x = x; + m_origin_y = y; + m_suggestion_display->set_origin(x, y, {}); } bool should_break_token(Vector& buffer, size_t index); @@ -335,7 +288,6 @@ private: size_t m_cached_prompt_length { 0 }; size_t m_old_prompt_length { 0 }; size_t m_cached_buffer_size { 0 }; - size_t m_lines_used_for_last_suggestions { 0 }; size_t m_prompt_lines_at_suggestion_initiation { 0 }; bool m_cached_prompt_valid { false }; @@ -343,16 +295,11 @@ private: size_t m_origin_x { 0 }; size_t m_origin_y { 0 }; + OwnPtr m_suggestion_display; + String m_new_prompt; - Vector m_suggestions; - CompletionSuggestion m_last_shown_suggestion { String::empty() }; - size_t m_last_shown_suggestion_display_length { 0 }; - bool m_last_shown_suggestion_was_complete { false }; - mutable size_t m_next_suggestion_index { 0 }; - mutable size_t m_next_suggestion_invariant_offset { 0 }; - mutable size_t m_next_suggestion_static_offset { 0 }; - size_t m_largest_common_suggestion_prefix_length { 0 }; - size_t m_last_displayed_suggestion_index { 0 }; + + SuggestionManager m_suggestion_manager; bool m_always_refresh { false }; diff --git a/Libraries/LibLine/SuggestionDisplay.h b/Libraries/LibLine/SuggestionDisplay.h new file mode 100644 index 00000000000..77393c06411 --- /dev/null +++ b/Libraries/LibLine/SuggestionDisplay.h @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2020, The SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once +#include +#include +#include +#include + +namespace Line { + +class Editor; + +class SuggestionDisplay { +public: + virtual ~SuggestionDisplay() { } + virtual void display(const SuggestionManager&) = 0; + virtual bool cleanup() = 0; + virtual void set_initial_prompt_lines(size_t) = 0; + + virtual void set_vt_size(size_t lines, size_t columns) = 0; + + size_t origin_x() const { return m_origin_x; } + size_t origin_y() const { return m_origin_y; } + + void set_origin(int x, int y, Badge) + { + m_origin_x = x; + m_origin_y = y; + } + +protected: + int m_origin_x { 0 }; + int m_origin_y { 0 }; +}; + +class XtermSuggestionDisplay : public SuggestionDisplay { +public: + XtermSuggestionDisplay(size_t lines, size_t columns) + : m_num_lines(lines) + , m_num_columns(columns) + { + } + virtual ~XtermSuggestionDisplay() override { } + virtual void display(const SuggestionManager&) override; + virtual bool cleanup() override; + + virtual void set_initial_prompt_lines(size_t lines) override + { + m_prompt_lines_at_suggestion_initiation = lines; + } + + virtual void set_vt_size(size_t lines, size_t columns) override + { + m_num_lines = lines; + m_num_columns = columns; + } + +private: + size_t m_lines_used_for_last_suggestions { 0 }; + size_t m_num_lines { 0 }; + size_t m_num_columns { 0 }; + size_t m_prompt_lines_at_suggestion_initiation { 0 }; + size_t m_prompt_length { 0 }; +}; + +} diff --git a/Libraries/LibLine/SuggestionManager.cpp b/Libraries/LibLine/SuggestionManager.cpp new file mode 100644 index 00000000000..8284b38a20c --- /dev/null +++ b/Libraries/LibLine/SuggestionManager.cpp @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2020, The SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include + +namespace Line { + +CompletionSuggestion::CompletionSuggestion(const StringView& completion, const StringView& trailing_trivia, Style style) + : style(style) + , text_string(completion) +{ + Utf8View text_u8 { completion }; + Utf8View trivia_u8 { trailing_trivia }; + + for (auto cp : text_u8) + text.append(cp); + + for (auto cp : trivia_u8) + this->trailing_trivia.append(cp); + + text_view = Utf32View { text.data(), text.size() }; + trivia_view = Utf32View { this->trailing_trivia.data(), this->trailing_trivia.size() }; +} + +void SuggestionManager::set_suggestions(Vector&& suggestions) +{ + m_suggestions = move(suggestions); + size_t common_suggestion_prefix { 0 }; + if (m_suggestions.size() == 1) { + m_largest_common_suggestion_prefix_length = m_suggestions[0].text_view.length(); + } else if (m_suggestions.size()) { + u32 last_valid_suggestion_codepoint; + + for (;; ++common_suggestion_prefix) { + if (m_suggestions[0].text_view.length() <= common_suggestion_prefix) + goto no_more_commons; + + last_valid_suggestion_codepoint = m_suggestions[0].text_view.codepoints()[common_suggestion_prefix]; + + for (auto& suggestion : m_suggestions) { + if (suggestion.text_view.length() <= common_suggestion_prefix || suggestion.text_view.codepoints()[common_suggestion_prefix] != last_valid_suggestion_codepoint) { + goto no_more_commons; + } + } + } + no_more_commons:; + m_largest_common_suggestion_prefix_length = common_suggestion_prefix; + } else { + m_largest_common_suggestion_prefix_length = 0; + } +} + +void SuggestionManager::next() +{ + if (m_suggestions.size()) + m_next_suggestion_index = (m_next_suggestion_index + 1) % m_suggestions.size(); + else + m_next_suggestion_index = 0; +} + +void SuggestionManager::previous() +{ + if (m_next_suggestion_index == 0) + m_next_suggestion_index = m_suggestions.size(); + m_next_suggestion_index--; +} + +const CompletionSuggestion& SuggestionManager::suggest() +{ + m_last_shown_suggestion = m_suggestions[m_next_suggestion_index]; + m_selected_suggestion_index = m_next_suggestion_index; + return m_last_shown_suggestion; +} + +void SuggestionManager::set_current_suggestion_initiation_index(size_t index) +{ + + if (m_last_shown_suggestion_display_length) + m_last_shown_suggestion.start_index = index - m_next_suggestion_static_offset - m_last_shown_suggestion_display_length; + else + m_last_shown_suggestion.start_index = index - m_next_suggestion_static_offset - m_next_suggestion_invariant_offset; + + m_last_shown_suggestion_display_length = m_last_shown_suggestion.text_view.length(); + m_last_shown_suggestion_was_complete = true; +} + +SuggestionManager::CompletionAttemptResult SuggestionManager::attempt_completion(CompletionMode mode, size_t initiation_start_index) +{ + CompletionAttemptResult result { mode }; + + if (m_next_suggestion_index < m_suggestions.size()) { + auto can_complete = m_next_suggestion_invariant_offset <= m_largest_common_suggestion_prefix_length; + if (!m_last_shown_suggestion.text.is_null()) { + ssize_t actual_offset; + size_t shown_length = m_last_shown_suggestion_display_length; + switch (mode) { + case CompletePrefix: + actual_offset = 0; + break; + case ShowSuggestions: + actual_offset = 0 - m_largest_common_suggestion_prefix_length + m_next_suggestion_invariant_offset; + if (can_complete) + shown_length = m_largest_common_suggestion_prefix_length + m_last_shown_suggestion.trivia_view.length(); + break; + default: + if (m_last_shown_suggestion_display_length == 0) + actual_offset = 0; + else + actual_offset = 0 - m_last_shown_suggestion_display_length + m_next_suggestion_invariant_offset; + break; + } + + result.offset_region_to_remove = { m_next_suggestion_invariant_offset, shown_length }; + result.new_cursor_offset = actual_offset; + } + + auto& suggestion = suggest(); + set_current_suggestion_initiation_index(initiation_start_index); + + if (mode == CompletePrefix) { + // only auto-complete *if possible* + if (can_complete) { + result.insert.append(suggestion.text_view.substring_view(m_next_suggestion_invariant_offset, m_largest_common_suggestion_prefix_length - m_next_suggestion_invariant_offset)); + m_last_shown_suggestion_display_length = m_largest_common_suggestion_prefix_length; + // do not increment the suggestion index, as the first tab should only be a *peek* + if (m_suggestions.size() == 1) { + // if there's one suggestion, commit and forget + result.new_completion_mode = DontComplete; + // add in the trivia of the last selected suggestion + result.insert.append(suggestion.trivia_view); + m_last_shown_suggestion_display_length = 0; + result.style_to_apply = suggestion.style; + m_last_shown_suggestion_was_complete = true; + return result; + } + } else { + m_last_shown_suggestion_display_length = 0; + } + result.new_completion_mode = CompletionMode::ShowSuggestions; + m_last_shown_suggestion_was_complete = false; + m_last_shown_suggestion = String::empty(); + } else { + result.insert.append(suggestion.text_view.substring_view(m_next_suggestion_invariant_offset, suggestion.text_view.length() - m_next_suggestion_invariant_offset)); + // add in the trivia of the last selected suggestion + result.insert.append(suggestion.trivia_view); + m_last_shown_suggestion_display_length += suggestion.trivia_view.length(); + } + } else { + m_next_suggestion_index = 0; + } + return result; +} + +size_t SuggestionManager::for_each_suggestion(Function callback) const +{ + size_t start_index { 0 }; + for (auto& suggestion : m_suggestions) { + if (start_index++ < m_last_displayed_suggestion_index) + continue; + if (callback(suggestion, start_index - 1) == IterationDecision::Break) + break; + } + return start_index; +} + +} diff --git a/Libraries/LibLine/SuggestionManager.h b/Libraries/LibLine/SuggestionManager.h new file mode 100644 index 00000000000..6b7918242c0 --- /dev/null +++ b/Libraries/LibLine/SuggestionManager.h @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2020, The SerenityOS developers. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once +#include +#include +#include +#include +#include +#include + +namespace Line { + +// FIXME: These objects are pretty heavy since they store two copies of text +// somehow get rid of one +struct CompletionSuggestion { + // intentionally not explicit (allows suggesting bare strings) + CompletionSuggestion(const String& completion) + : CompletionSuggestion(completion, "", {}) + { + } + + CompletionSuggestion(const StringView& completion, const StringView& trailing_trivia) + : CompletionSuggestion(completion, trailing_trivia, {}) + { + } + + CompletionSuggestion(const StringView& completion, const StringView& trailing_trivia, Style style); + + bool operator==(const CompletionSuggestion& suggestion) const + { + return suggestion.text == text; + } + + Vector text; + Vector trailing_trivia; + Style style; + size_t start_index { 0 }; + + Utf32View text_view; + Utf32View trivia_view; + String text_string; +}; + +class SuggestionManager { + friend class Editor; + +public: + void set_suggestions(Vector&& suggestions); + void set_current_suggestion_initiation_index(size_t index); + + size_t count() const { return m_suggestions.size(); } + size_t display_length() const { return m_last_shown_suggestion_display_length; } + size_t index() const { return m_last_displayed_suggestion_index; } + size_t next_index() const { return m_next_suggestion_index; } + + size_t for_each_suggestion(Function) const; + + enum CompletionMode { + DontComplete, + CompletePrefix, + ShowSuggestions, + CycleSuggestions, + }; + + class CompletionAttemptResult { + public: + CompletionMode new_completion_mode; + + ssize_t new_cursor_offset { 0 }; + + struct { + size_t start; + size_t end; + } offset_region_to_remove { 0, 0 }; // The region to remove as defined by [start, end) translated by (old_cursor + new_cursor_offset) + + Vector insert {}; + + Optional