mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2024-09-20 09:49:15 +03:00
LibLine: Refactor suggestion handling and display logic out
This commit is contained in:
parent
65adf2aea2
commit
f0862cf2b7
Notes:
sideshowbarker
2024-07-19 06:14:09 +09:00
Author: https://github.com/alimpfard Commit: https://github.com/SerenityOS/serenity/commit/f0862cf2b70 Pull-request: https://github.com/SerenityOS/serenity/pull/2320 Reviewed-by: https://github.com/awesomekling Reviewed-by: https://github.com/bugaevc
@ -1,5 +1,7 @@
|
||||
set(SOURCES
|
||||
Editor.cpp
|
||||
SuggestionManager.cpp
|
||||
XtermSuggestionDisplay.cpp
|
||||
)
|
||||
|
||||
serenity_lib(LibLine line)
|
||||
|
@ -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);
|
||||
|
@ -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 };
|
||||
|
||||
|
90
Libraries/LibLine/SuggestionDisplay.h
Normal file
90
Libraries/LibLine/SuggestionDisplay.h
Normal 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 };
|
||||
};
|
||||
|
||||
}
|
190
Libraries/LibLine/SuggestionManager.cpp
Normal file
190
Libraries/LibLine/SuggestionManager.cpp
Normal 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;
|
||||
}
|
||||
|
||||
}
|
145
Libraries/LibLine/SuggestionManager.h
Normal file
145
Libraries/LibLine/SuggestionManager.h
Normal 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
44
Libraries/LibLine/VT.h
Normal 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);
|
||||
|
||||
}
|
||||
}
|
122
Libraries/LibLine/XtermSuggestionDisplay.cpp
Normal file
122
Libraries/LibLine/XtermSuggestionDisplay.cpp
Normal 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;
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user