LibLine: Refactor suggestion handling and display logic out

This commit is contained in:
AnotherTest 2020-05-22 03:52:34 +04:30 committed by Andreas Kling
parent 65adf2aea2
commit f0862cf2b7
Notes: sideshowbarker 2024-07-19 06:14:09 +09:00
8 changed files with 692 additions and 303 deletions

View File

@ -1,5 +1,7 @@
set(SOURCES
Editor.cpp
SuggestionManager.cpp
XtermSuggestionDisplay.cpp
)
serenity_lib(LibLine line)

View File

@ -52,6 +52,7 @@ Editor::Editor(Configuration configuration)
m_num_columns = ws.ws_col;
m_num_lines = ws.ws_row;
}
m_suggestion_display = make<XtermSuggestionDisplay>(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;
auto completion_mode = m_times_tab_pressed == 1 ? SuggestionManager::CompletePrefix : m_times_tab_pressed == 2 ? SuggestionManager::ShowSuggestions : SuggestionManager::CycleSuggestions;
auto completion_result = m_suggestion_manager.attempt_completion(completion_mode, token_start);
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 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();
case SuggestionManager::CompletePrefix:
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;
}
for (size_t i = m_next_suggestion_invariant_offset; i < shown_length; ++i)
remove_at_index(actual_offset);
m_cursor = actual_offset;
m_inline_search_cursor = m_cursor;
m_refresh_needed = true;
}
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;
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;
}
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<int>(longest_suggestion_byte_length) + 2, suggestion.text_string.characters());
num_printed += longest_suggestion_length + 2;
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();
}
}
if (m_last_shown_suggestion_was_complete && index == current_suggestion_index) {
vt_apply_style(Style::reset_style());
fflush(stdout);
if (m_times_tab_pressed > 2) {
if (m_tab_direction == TabDirection::Forward)
m_suggestion_manager.next();
else
m_suggestion_manager.previous();
}
++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<u32, Style> 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);

View File

@ -40,59 +40,14 @@
#include <LibCore/DirIterator.h>
#include <LibLine/Span.h>
#include <LibLine/Style.h>
#include <LibLine/SuggestionDisplay.h>
#include <LibLine/SuggestionManager.h>
#include <LibLine/VT.h>
#include <sys/stat.h>
#include <termios.h>
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<u32> text;
Vector<u32> 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<bool(Editor&)> 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<size_t, 2> 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<u32, 1024>& 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<SuggestionDisplay> m_suggestion_display;
String m_new_prompt;
Vector<CompletionSuggestion> 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 };

View File

@ -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 <AK/Forward.h>
#include <AK/String.h>
#include <LibLine/SuggestionManager.h>
#include <stdlib.h>
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<Editor>)
{
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 };
};
}

View File

@ -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 <AK/Function.h>
#include <LibLine/SuggestionManager.h>
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<CompletionSuggestion>&& 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<IterationDecision(const CompletionSuggestion&, size_t)> 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;
}
}

View File

@ -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 <AK/Forward.h>
#include <AK/String.h>
#include <AK/Utf32View.h>
#include <AK/Utf8View.h>
#include <LibLine/Style.h>
#include <stdlib.h>
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<u32> text;
Vector<u32> 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<CompletionSuggestion>&& 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<IterationDecision(const CompletionSuggestion&, size_t)>) 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<Utf32View> insert {};
Optional<Style> style_to_apply {};
};
CompletionAttemptResult attempt_completion(CompletionMode, size_t initiation_start_index);
void next();
void previous();
void set_suggestion_variants(size_t static_offset, size_t invariant_offset, size_t suggestion_index) const
{
m_next_suggestion_index = suggestion_index;
m_next_suggestion_static_offset = static_offset;
m_next_suggestion_invariant_offset = invariant_offset;
}
const CompletionSuggestion& suggest();
const CompletionSuggestion& current_suggestion() const { return m_last_shown_suggestion; }
bool is_current_suggestion_complete() const { return m_last_shown_suggestion_was_complete; }
void reset()
{
m_last_shown_suggestion = String::empty();
m_last_shown_suggestion_display_length = 0;
m_suggestions.clear();
m_last_displayed_suggestion_index = 0;
}
private:
SuggestionManager()
{
}
Vector<CompletionSuggestion> 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 };
size_t m_selected_suggestion_index { 0 };
};
}

44
Libraries/LibLine/VT.h Normal file
View File

@ -0,0 +1,44 @@
/*
* 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 <AK/Types.h>
#include <LibLine/Style.h>
namespace Line {
namespace VT {
void save_cursor();
void restore_cursor();
void clear_to_end_of_line();
void clear_lines(size_t count_above, size_t count_below = 0);
void move_relative(int x, int y);
void move_absolute(u32 x, u32 y);
void apply_style(const Style&, bool is_starting = true);
}
}

View File

@ -0,0 +1,122 @@
/*
* 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 <AK/Function.h>
#include <LibLine/SuggestionDisplay.h>
#include <LibLine/VT.h>
#include <stdio.h>
namespace Line {
void XtermSuggestionDisplay::display(const SuggestionManager& manager)
{
size_t longest_suggestion_length = 0;
size_t longest_suggestion_byte_length = 0;
manager.for_each_suggestion([&](auto& suggestion, auto) {
longest_suggestion_length = max(longest_suggestion_length, suggestion.text_view.length());
longest_suggestion_byte_length = max(longest_suggestion_byte_length, suggestion.text_string.length());
return IterationDecision::Continue;
});
size_t num_printed = 0;
size_t lines_used { 1 };
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_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);
manager.for_each_suggestion([&](auto& suggestion, auto index) {
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)
return IterationDecision::Break;
// only apply colour to the selection if something is *actually* added to the buffer
if (manager.is_current_suggestion_complete() && index == manager.next_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<int>(longest_suggestion_byte_length) + 2, suggestion.text_string.characters());
num_printed += longest_suggestion_length + 2;
}
if (manager.is_current_suggestion_complete() && index == manager.next_index()) {
VT::apply_style(Style::reset_style());
fflush(stdout);
}
return IterationDecision::Continue;
});
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;
}
}
bool XtermSuggestionDisplay::cleanup()
{
if (m_lines_used_for_last_suggestions) {
VT::clear_lines(0, m_lines_used_for_last_suggestions);
m_lines_used_for_last_suggestions = 0;
return true;
}
return false;
}
}