1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-27 03:17:35 +00:00

Libraries: Move to Userland/Libraries/

This commit is contained in:
Andreas Kling 2021-01-12 12:17:30 +01:00
parent dc28c07fa5
commit 13d7c09125
1857 changed files with 266 additions and 274 deletions

View file

@ -0,0 +1,10 @@
set(SOURCES
Editor.cpp
InternalFunctions.cpp
KeyCallbackMachine.cpp
SuggestionManager.cpp
XtermSuggestionDisplay.cpp
)
serenity_lib(LibLine line)
target_link_libraries(LibLine LibC LibCore)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,489 @@
/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* Copyright (c) 2021, 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/BinarySearch.h>
#include <AK/ByteBuffer.h>
#include <AK/Function.h>
#include <AK/HashMap.h>
#include <AK/NonnullOwnPtr.h>
#include <AK/QuickSort.h>
#include <AK/Result.h>
#include <AK/String.h>
#include <AK/Traits.h>
#include <AK/Utf32View.h>
#include <AK/Utf8View.h>
#include <AK/Vector.h>
#include <LibCore/DirIterator.h>
#include <LibCore/EventLoop.h>
#include <LibCore/Notifier.h>
#include <LibCore/Object.h>
#include <LibLine/KeyCallbackMachine.h>
#include <LibLine/Span.h>
#include <LibLine/StringMetrics.h>
#include <LibLine/Style.h>
#include <LibLine/SuggestionDisplay.h>
#include <LibLine/SuggestionManager.h>
#include <LibLine/VT.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <termios.h>
namespace Line {
struct KeyBinding {
Vector<Key> keys;
enum class Kind {
InternalFunction,
Insertion,
} kind { Kind::InternalFunction };
String binding;
};
struct Configuration {
enum RefreshBehaviour {
Lazy,
Eager,
};
enum OperationMode {
Unset,
Full,
NoEscapeSequences,
NonInteractive,
};
enum SignalHandler {
WithSignalHandlers,
NoSignalHandlers,
};
Configuration()
{
}
template<typename Arg, typename... Rest>
Configuration(Arg arg, Rest... rest)
: Configuration(rest...)
{
set(arg);
}
void set(RefreshBehaviour refresh) { refresh_behaviour = refresh; }
void set(OperationMode mode) { operation_mode = mode; }
void set(SignalHandler mode) { m_signal_mode = mode; }
void set(const KeyBinding& binding) { keybindings.append(binding); }
static Configuration from_config(const StringView& libname = "line");
RefreshBehaviour refresh_behaviour { RefreshBehaviour::Lazy };
SignalHandler m_signal_mode { SignalHandler::WithSignalHandlers };
OperationMode operation_mode { OperationMode::Unset };
Vector<KeyBinding> keybindings;
};
#define ENUMERATE_EDITOR_INTERNAL_FUNCTIONS(M) \
M(clear_screen) \
M(cursor_left_character) \
M(cursor_left_word) \
M(cursor_right_character) \
M(cursor_right_word) \
M(enter_search) \
M(erase_character_backwards) \
M(erase_character_forwards) \
M(erase_to_beginning) \
M(erase_to_end) \
M(erase_word_backwards) \
M(finish_edit) \
M(go_end) \
M(go_home) \
M(kill_line) \
M(search_backwards) \
M(search_forwards) \
M(transpose_characters) \
M(transpose_words) \
M(insert_last_words) \
M(erase_alnum_word_backwards) \
M(erase_alnum_word_forwards) \
M(capitalize_word) \
M(lowercase_word) \
M(uppercase_word)
#define EDITOR_INTERNAL_FUNCTION(name) \
[](auto& editor) { editor.name(); return false; }
class Editor : public Core::Object {
C_OBJECT(Editor);
public:
enum class Error {
ReadFailure,
Empty,
Eof,
};
~Editor();
Result<String, Error> get_line(const String& prompt);
void initialize();
void add_to_history(const String& line);
bool load_history(const String& path);
bool save_history(const String& path);
const auto& history() const { return m_history; }
void register_key_input_callback(const KeyBinding&);
void register_key_input_callback(Vector<Key> keys, Function<bool(Editor&)> callback) { m_callback_machine.register_key_input_callback(move(keys), move(callback)); }
void register_key_input_callback(Key key, Function<bool(Editor&)> callback) { register_key_input_callback(Vector<Key> { key }, move(callback)); }
static StringMetrics actual_rendered_string_metrics(const StringView&);
static StringMetrics actual_rendered_string_metrics(const Utf32View&);
Function<Vector<CompletionSuggestion>(const Editor&)> on_tab_complete;
Function<void()> on_interrupt_handled;
Function<void(Editor&)> on_display_refresh;
static Function<bool(Editor&)> find_internal_function(const StringView& name);
enum class CaseChangeOp {
Lowercase,
Uppercase,
Capital,
};
void case_change_word(CaseChangeOp);
#define __ENUMERATE_EDITOR_INTERNAL_FUNCTION(name) \
void name();
ENUMERATE_EDITOR_INTERNAL_FUNCTIONS(__ENUMERATE_EDITOR_INTERNAL_FUNCTION)
#undef __ENUMERATE_EDITOR_INTERNAL_FUNCTION
void interrupted();
void resized()
{
m_was_resized = true;
m_previous_num_columns = m_num_columns;
get_terminal_size();
m_suggestion_display->set_vt_size(m_num_lines, m_num_columns);
if (m_is_searching)
m_search_editor->resized();
}
size_t cursor() const { return m_cursor; }
void set_cursor(size_t cursor)
{
if (cursor > m_buffer.size())
cursor = m_buffer.size();
m_cursor = cursor;
}
const Vector<u32, 1024>& buffer() const { return m_buffer; }
u32 buffer_at(size_t pos) const { return m_buffer.at(pos); }
String line() const { return line(m_buffer.size()); }
String line(size_t up_to_index) const;
// Only makes sense inside a character_input callback or on_* callback.
void set_prompt(const String& prompt)
{
if (m_cached_prompt_valid)
m_old_prompt_metrics = m_cached_prompt_metrics;
m_cached_prompt_valid = false;
m_cached_prompt_metrics = actual_rendered_string_metrics(prompt);
m_new_prompt = prompt;
}
void clear_line();
void insert(const String&);
void insert(const StringView&);
void insert(const Utf32View&);
void insert(const u32);
void stylize(const Span&, const Style&);
void strip_styles(bool strip_anchored = false);
// Invariant Offset is an offset into the suggested data, hinting the editor what parts of the suggestion will not change
// Static Offset is an offset into the token, signifying where the suggestions start
// e.g.
// foobar<suggestion initiated>, on_tab_complete returns "barx", "bary", "barz"
// ^ ^
// +-|- static offset: the suggestions start here
// +- invariant offset: the suggestions do not change up to here
//
void suggest(size_t invariant_offset = 0, size_t static_offset = 0, Span::Mode offset_mode = Span::ByteOriented) const;
const struct termios& termios() const { return m_termios; }
const struct termios& default_termios() const { return m_default_termios; }
struct winsize terminal_size() const
{
winsize ws { (u16)m_num_lines, (u16)m_num_columns, 0, 0 };
return ws;
}
void finish()
{
m_finish = true;
}
bool is_editing() const { return m_is_editing; }
const Utf32View buffer_view() const { return { m_buffer.data(), m_buffer.size() }; }
private:
explicit Editor(Configuration configuration = Configuration::from_config());
void set_default_keybinds();
enum VTState {
Free = 1,
Escape = 3,
Bracket = 5,
BracketArgsSemi = 7,
Title = 9,
};
static VTState actual_rendered_string_length_step(StringMetrics&, size_t, StringMetrics::LineMetrics& current_line, u32, u32, VTState);
enum LoopExitCode {
Exit = 0,
Retry
};
// FIXME: Port to Core::Property
void save_to(JsonObject&);
void try_update_once();
void handle_interrupt_event();
void handle_read_event();
Vector<size_t, 2> vt_dsr();
void remove_at_index(size_t);
enum class ModificationKind {
Insertion,
Removal,
ForcedOverlapRemoval,
};
void readjust_anchored_styles(size_t hint_index, ModificationKind);
Style find_applicable_style(size_t offset) const;
bool search(const StringView&, bool allow_empty = false, bool from_beginning = true);
inline void end_search()
{
m_is_searching = false;
m_refresh_needed = true;
m_search_offset = 0;
if (m_reset_buffer_on_search_end) {
m_buffer.clear();
for (auto ch : m_pre_search_buffer)
m_buffer.append(ch);
m_cursor = m_pre_search_cursor;
}
m_reset_buffer_on_search_end = true;
m_search_editor = nullptr;
}
void reset()
{
m_cached_buffer_metrics.reset();
m_cached_prompt_valid = false;
m_cursor = 0;
m_drawn_cursor = 0;
m_inline_search_cursor = 0;
m_search_offset = 0;
m_search_offset_state = SearchOffsetState::Unbiased;
m_old_prompt_metrics = m_cached_prompt_metrics;
set_origin(0, 0);
m_prompt_lines_at_suggestion_initiation = 0;
m_refresh_needed = true;
m_input_error.clear();
m_returned_line = String::empty();
}
void refresh_display();
void cleanup();
void cleanup_suggestions();
void really_quit_event_loop();
void restore()
{
ASSERT(m_initialized);
tcsetattr(0, TCSANOW, &m_default_termios);
m_initialized = false;
for (auto id : m_signal_handlers)
Core::EventLoop::unregister_signal(id);
}
const StringMetrics& current_prompt_metrics() const
{
return m_cached_prompt_valid ? m_cached_prompt_metrics : m_old_prompt_metrics;
}
size_t num_lines() const
{
return current_prompt_metrics().lines_with_addition(m_cached_buffer_metrics, m_num_columns);
}
size_t cursor_line() const
{
auto cursor = m_drawn_cursor;
if (cursor > m_cursor)
cursor = m_cursor;
return current_prompt_metrics().lines_with_addition(
actual_rendered_string_metrics(buffer_view().substring_view(0, cursor)),
m_num_columns);
}
size_t offset_in_line() const
{
auto cursor = m_drawn_cursor;
if (cursor > m_cursor)
cursor = m_cursor;
auto buffer_metrics = actual_rendered_string_metrics(buffer_view().substring_view(0, cursor));
return current_prompt_metrics().offset_with_addition(buffer_metrics, m_num_columns);
}
void set_origin()
{
auto position = vt_dsr();
set_origin(position[0], position[1]);
}
void set_origin(int row, int col)
{
m_origin_row = row;
m_origin_column = col;
m_suggestion_display->set_origin(row, col, {});
}
void recalculate_origin();
void reposition_cursor(bool to_end = false);
struct CodepointRange {
size_t start { 0 };
size_t end { 0 };
};
CodepointRange byte_offset_range_to_code_point_offset_range(size_t byte_start, size_t byte_end, size_t code_point_scan_offset, bool reverse = false) const;
void get_terminal_size();
bool m_finish { false };
RefPtr<Editor> m_search_editor;
bool m_is_searching { false };
bool m_reset_buffer_on_search_end { true };
size_t m_search_offset { 0 };
enum class SearchOffsetState {
Unbiased,
Backwards,
Forwards,
} m_search_offset_state { SearchOffsetState::Unbiased };
size_t m_pre_search_cursor { 0 };
Vector<u32, 1024> m_pre_search_buffer;
Vector<u32, 1024> m_buffer;
ByteBuffer m_pending_chars;
Vector<char, 512> m_incomplete_data;
Optional<Error> m_input_error;
String m_returned_line;
size_t m_cursor { 0 };
size_t m_drawn_cursor { 0 };
size_t m_inline_search_cursor { 0 };
size_t m_chars_inserted_in_the_middle { 0 };
size_t m_times_tab_pressed { 0 };
size_t m_num_columns { 0 };
size_t m_num_lines { 1 };
size_t m_previous_num_columns { 0 };
size_t m_extra_forward_lines { 0 };
StringMetrics m_cached_prompt_metrics;
StringMetrics m_old_prompt_metrics;
StringMetrics m_cached_buffer_metrics;
size_t m_prompt_lines_at_suggestion_initiation { 0 };
bool m_cached_prompt_valid { false };
// Exact position before our prompt in the terminal.
size_t m_origin_row { 0 };
size_t m_origin_column { 0 };
OwnPtr<SuggestionDisplay> m_suggestion_display;
String m_new_prompt;
SuggestionManager m_suggestion_manager;
bool m_always_refresh { false };
enum class TabDirection {
Forward,
Backward,
};
TabDirection m_tab_direction { TabDirection::Forward };
KeyCallbackMachine m_callback_machine;
struct termios m_termios {
};
struct termios m_default_termios {
};
bool m_was_interrupted { false };
bool m_was_resized { false };
// FIXME: This should be something more take_first()-friendly.
struct HistoryEntry {
String entry;
time_t timestamp;
};
Vector<HistoryEntry> m_history;
size_t m_history_cursor { 0 };
size_t m_history_capacity { 1024 };
enum class InputState {
Free,
Verbatim,
GotEscape,
CSIExpectParameter,
CSIExpectIntermediate,
CSIExpectFinal,
};
InputState m_state { InputState::Free };
HashMap<u32, HashMap<u32, Style>> m_spans_starting;
HashMap<u32, HashMap<u32, Style>> m_spans_ending;
HashMap<u32, HashMap<u32, Style>> m_anchored_spans_starting;
HashMap<u32, HashMap<u32, Style>> m_anchored_spans_ending;
RefPtr<Core::Notifier> m_notifier;
bool m_initialized { false };
bool m_refresh_needed { false };
Vector<int, 2> m_signal_handlers;
bool m_is_editing { false };
Configuration m_configuration;
};
}

View file

@ -0,0 +1,496 @@
/*
* 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/StringBuilder.h>
#include <LibLine/Editor.h>
#include <ctype.h>
#include <stdio.h>
namespace {
constexpr u32 ctrl(char c) { return c & 0x3f; }
}
namespace Line {
Function<bool(Editor&)> Editor::find_internal_function(const StringView& name)
{
#define __ENUMERATE(internal_name) \
if (name == #internal_name) \
return EDITOR_INTERNAL_FUNCTION(internal_name);
ENUMERATE_EDITOR_INTERNAL_FUNCTIONS(__ENUMERATE)
return {};
}
void Editor::search_forwards()
{
ScopedValueRollback inline_search_cursor_rollback { m_inline_search_cursor };
StringBuilder builder;
builder.append(Utf32View { m_buffer.data(), m_inline_search_cursor });
String search_phrase = builder.to_string();
if (m_search_offset_state == SearchOffsetState::Backwards)
--m_search_offset;
if (m_search_offset > 0) {
ScopedValueRollback search_offset_rollback { m_search_offset };
--m_search_offset;
if (search(search_phrase, true)) {
m_search_offset_state = SearchOffsetState::Forwards;
search_offset_rollback.set_override_rollback_value(m_search_offset);
} else {
m_search_offset_state = SearchOffsetState::Unbiased;
}
} else {
m_search_offset_state = SearchOffsetState::Unbiased;
m_cursor = 0;
m_buffer.clear();
insert(search_phrase);
m_refresh_needed = true;
}
}
void Editor::search_backwards()
{
ScopedValueRollback inline_search_cursor_rollback { m_inline_search_cursor };
StringBuilder builder;
builder.append(Utf32View { m_buffer.data(), m_inline_search_cursor });
String search_phrase = builder.to_string();
if (m_search_offset_state == SearchOffsetState::Forwards)
++m_search_offset;
if (search(search_phrase, true)) {
m_search_offset_state = SearchOffsetState::Backwards;
++m_search_offset;
} else {
m_search_offset_state = SearchOffsetState::Unbiased;
--m_search_offset;
}
}
void Editor::cursor_left_word()
{
if (m_cursor > 0) {
auto skipped_at_least_one_character = false;
for (;;) {
if (m_cursor == 0)
break;
if (skipped_at_least_one_character && !isalnum(m_buffer[m_cursor - 1])) // stop *after* a non-alnum, but only if it changes the position
break;
skipped_at_least_one_character = true;
--m_cursor;
}
}
m_inline_search_cursor = m_cursor;
}
void Editor::cursor_left_character()
{
if (m_cursor > 0)
--m_cursor;
m_inline_search_cursor = m_cursor;
}
void Editor::cursor_right_word()
{
if (m_cursor < m_buffer.size()) {
// Temporarily put a space at the end of our buffer,
// doing this greatly simplifies the logic below.
m_buffer.append(' ');
for (;;) {
if (m_cursor >= m_buffer.size())
break;
if (!isalnum(m_buffer[++m_cursor]))
break;
}
m_buffer.take_last();
}
m_inline_search_cursor = m_cursor;
m_search_offset = 0;
}
void Editor::cursor_right_character()
{
if (m_cursor < m_buffer.size()) {
++m_cursor;
}
m_inline_search_cursor = m_cursor;
m_search_offset = 0;
}
void Editor::erase_character_backwards()
{
if (m_is_searching) {
return;
}
if (m_cursor == 0) {
fputc('\a', stderr);
fflush(stderr);
return;
}
remove_at_index(m_cursor - 1);
--m_cursor;
m_inline_search_cursor = m_cursor;
// We will have to redraw :(
m_refresh_needed = true;
}
void Editor::erase_character_forwards()
{
if (m_cursor == m_buffer.size()) {
fputc('\a', stderr);
fflush(stderr);
return;
}
remove_at_index(m_cursor);
m_refresh_needed = true;
}
void Editor::finish_edit()
{
fprintf(stderr, "<EOF>\n");
if (!m_always_refresh) {
m_input_error = Error::Eof;
finish();
really_quit_event_loop();
}
}
void Editor::kill_line()
{
for (size_t i = 0; i < m_cursor; ++i)
remove_at_index(0);
m_cursor = 0;
m_refresh_needed = true;
}
void Editor::erase_word_backwards()
{
// A word here is space-separated. `foo=bar baz` is two words.
bool has_seen_nonspace = false;
while (m_cursor > 0) {
if (isspace(m_buffer[m_cursor - 1])) {
if (has_seen_nonspace)
break;
} else {
has_seen_nonspace = true;
}
erase_character_backwards();
}
}
void Editor::erase_to_end()
{
while (m_cursor < m_buffer.size())
erase_character_forwards();
}
void Editor::erase_to_beginning()
{
}
void Editor::transpose_characters()
{
if (m_cursor > 0 && m_buffer.size() >= 2) {
if (m_cursor < m_buffer.size())
++m_cursor;
swap(m_buffer[m_cursor - 1], m_buffer[m_cursor - 2]);
// FIXME: Update anchored styles too.
m_refresh_needed = true;
}
}
void Editor::enter_search()
{
if (m_is_searching) {
// How did we get here?
ASSERT_NOT_REACHED();
} else {
m_is_searching = true;
m_search_offset = 0;
m_pre_search_buffer.clear();
for (auto code_point : m_buffer)
m_pre_search_buffer.append(code_point);
m_pre_search_cursor = m_cursor;
// Disable our own notifier so as to avoid interfering with the search editor.
m_notifier->set_enabled(false);
m_search_editor = Editor::construct(Configuration { Configuration::Eager, Configuration::NoSignalHandlers }); // Has anyone seen 'Inception'?
m_search_editor->initialize();
add_child(*m_search_editor);
m_search_editor->on_display_refresh = [this](Editor& search_editor) {
StringBuilder builder;
builder.append(Utf32View { search_editor.buffer().data(), search_editor.buffer().size() });
if (!search(builder.build(), false, false)) {
m_buffer.clear();
m_cursor = 0;
}
refresh_display();
};
// Whenever the search editor gets a ^R, cycle between history entries.
m_search_editor->register_key_input_callback(ctrl('R'), [this](Editor& search_editor) {
++m_search_offset;
search_editor.m_refresh_needed = true;
return false; // Do not process this key event
});
// Whenever the search editor gets a backspace, cycle back between history entries
// unless we're at the zeroth entry, in which case, allow the deletion.
m_search_editor->register_key_input_callback(m_termios.c_cc[VERASE], [this](Editor& search_editor) {
if (m_search_offset > 0) {
--m_search_offset;
search_editor.m_refresh_needed = true;
return false; // Do not process this key event
}
search_editor.erase_character_backwards();
return false;
});
// ^L - This is a source of issues, as the search editor refreshes first,
// and we end up with the wrong order of prompts, so we will first refresh
// ourselves, then refresh the search editor, and then tell him not to process
// this event.
m_search_editor->register_key_input_callback(ctrl('L'), [this](auto& search_editor) {
fprintf(stderr, "\033[3J\033[H\033[2J"); // Clear screen.
// refresh our own prompt
set_origin(1, 1);
m_refresh_needed = true;
refresh_display();
// move the search prompt below ours
// and tell it to redraw itself
search_editor.set_origin(2, 1);
search_editor.m_refresh_needed = true;
return false;
});
// quit without clearing the current buffer
m_search_editor->register_key_input_callback('\t', [this](Editor& search_editor) {
search_editor.finish();
m_reset_buffer_on_search_end = false;
return false;
});
fprintf(stderr, "\n");
fflush(stderr);
auto search_prompt = "\x1b[32msearch:\x1b[0m ";
// While the search editor is active, we do not want editing events.
m_is_editing = false;
auto search_string_result = m_search_editor->get_line(search_prompt);
// Grab where the search origin last was, anything up to this point will be cleared.
auto search_end_row = m_search_editor->m_origin_row;
remove_child(*m_search_editor);
m_search_editor = nullptr;
m_is_searching = false;
m_is_editing = true;
m_search_offset = 0;
// Re-enable the notifier after discarding the search editor.
m_notifier->set_enabled(true);
if (search_string_result.is_error()) {
// Somethine broke, fail
m_input_error = search_string_result.error();
finish();
return;
}
auto& search_string = search_string_result.value();
// Manually cleanup the search line.
reposition_cursor();
auto search_metrics = actual_rendered_string_metrics(search_string);
auto metrics = actual_rendered_string_metrics(search_prompt);
VT::clear_lines(0, metrics.lines_with_addition(search_metrics, m_num_columns) + search_end_row - m_origin_row - 1);
reposition_cursor();
if (!m_reset_buffer_on_search_end || search_metrics.total_length == 0) {
// If the entry was empty, or we purposely quit without a newline,
// do not return anything; instead, just end the search.
end_search();
return;
}
// Return the string,
finish();
}
}
void Editor::transpose_words()
{
// A word here is contiguous alnums. `foo=bar baz` is three words.
// 'abcd,.:efg...' should become 'efg...,.:abcd' if caret is after
// 'efg...'. If it's in 'efg', it should become 'efg,.:abcd...'
// with the caret after it, which then becomes 'abcd...,.:efg'
// when alt-t is pressed a second time.
// Move to end of word under (or after) caret.
size_t cursor = m_cursor;
while (cursor < m_buffer.size() && !isalnum(m_buffer[cursor]))
++cursor;
while (cursor < m_buffer.size() && isalnum(m_buffer[cursor]))
++cursor;
// Move left over second word and the space to its right.
size_t end = cursor;
size_t start = cursor;
while (start > 0 && !isalnum(m_buffer[start - 1]))
--start;
while (start > 0 && isalnum(m_buffer[start - 1]))
--start;
size_t start_second_word = start;
// Move left over space between the two words.
while (start > 0 && !isalnum(m_buffer[start - 1]))
--start;
size_t start_gap = start;
// Move left over first word.
while (start > 0 && isalnum(m_buffer[start - 1]))
--start;
if (start != start_gap) {
// To swap the two words, swap each word (and the gap) individually, and then swap the whole range.
auto swap_range = [this](auto from, auto to) {
for (size_t i = 0; i < (to - from) / 2; ++i)
swap(m_buffer[from + i], m_buffer[to - 1 - i]);
};
swap_range(start, start_gap);
swap_range(start_gap, start_second_word);
swap_range(start_second_word, end);
swap_range(start, end);
m_cursor = cursor;
// FIXME: Update anchored styles too.
m_refresh_needed = true;
}
}
void Editor::go_home()
{
m_cursor = 0;
m_inline_search_cursor = m_cursor;
m_search_offset = 0;
}
void Editor::go_end()
{
m_cursor = m_buffer.size();
m_inline_search_cursor = m_cursor;
m_search_offset = 0;
}
void Editor::clear_screen()
{
fprintf(stderr, "\033[3J\033[H\033[2J"); // Clear screen.
VT::move_absolute(1, 1);
set_origin(1, 1);
m_refresh_needed = true;
}
void Editor::insert_last_words()
{
if (!m_history.is_empty()) {
// FIXME: This isn't quite right: if the last arg was `"foo bar"` or `foo\ bar` (but not `foo\\ bar`), we should insert that whole arg as last token.
if (auto last_words = m_history.last().entry.split_view(' '); !last_words.is_empty())
insert(last_words.last());
}
}
void Editor::erase_alnum_word_backwards()
{
// A word here is contiguous alnums. `foo=bar baz` is three words.
bool has_seen_alnum = false;
while (m_cursor > 0) {
if (!isalnum(m_buffer[m_cursor - 1])) {
if (has_seen_alnum)
break;
} else {
has_seen_alnum = true;
}
erase_character_backwards();
}
}
void Editor::erase_alnum_word_forwards()
{
// A word here is contiguous alnums. `foo=bar baz` is three words.
bool has_seen_alnum = false;
while (m_cursor < m_buffer.size()) {
if (!isalnum(m_buffer[m_cursor])) {
if (has_seen_alnum)
break;
} else {
has_seen_alnum = true;
}
erase_character_forwards();
}
}
void Editor::case_change_word(Editor::CaseChangeOp change_op)
{
// A word here is contiguous alnums. `foo=bar baz` is three words.
while (m_cursor < m_buffer.size() && !isalnum(m_buffer[m_cursor]))
++m_cursor;
size_t start = m_cursor;
while (m_cursor < m_buffer.size() && isalnum(m_buffer[m_cursor])) {
if (change_op == CaseChangeOp::Uppercase || (change_op == CaseChangeOp::Capital && m_cursor == start)) {
m_buffer[m_cursor] = toupper(m_buffer[m_cursor]);
} else {
ASSERT(change_op == CaseChangeOp::Lowercase || (change_op == CaseChangeOp::Capital && m_cursor > start));
m_buffer[m_cursor] = tolower(m_buffer[m_cursor]);
}
++m_cursor;
m_refresh_needed = true;
}
}
void Editor::capitalize_word()
{
case_change_word(CaseChangeOp::Capital);
}
void Editor::lowercase_word()
{
case_change_word(CaseChangeOp::Lowercase);
}
void Editor::uppercase_word()
{
case_change_word(CaseChangeOp::Uppercase);
}
}

View file

@ -0,0 +1,113 @@
/*
* 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 <LibLine/Editor.h>
namespace {
constexpr u32 ctrl(char c) { return c & 0x3f; }
}
namespace Line {
void KeyCallbackMachine::register_key_input_callback(Vector<Key> keys, Function<bool(Editor&)> callback)
{
m_key_callbacks.set(keys, make<KeyCallback>(move(callback)));
}
void KeyCallbackMachine::key_pressed(Editor& editor, Key key)
{
#ifdef CALLBACK_MACHINE_DEBUG
dbgln("Key<{}, {}> pressed, seq_length={}, {} things in the matching vector", key.key, key.modifiers, m_sequence_length, m_current_matching_keys.size());
#endif
if (m_sequence_length == 0) {
ASSERT(m_current_matching_keys.is_empty());
for (auto& it : m_key_callbacks) {
if (it.key.first() == key)
m_current_matching_keys.append(it.key);
}
if (m_current_matching_keys.is_empty()) {
m_should_process_this_key = true;
return;
}
}
++m_sequence_length;
Vector<Vector<Key>> old_macthing_keys;
swap(m_current_matching_keys, old_macthing_keys);
for (auto& okey : old_macthing_keys) {
if (okey.size() < m_sequence_length)
continue;
if (okey[m_sequence_length - 1] == key)
m_current_matching_keys.append(okey);
}
if (m_current_matching_keys.is_empty()) {
// Insert any keys that were captured
if (!old_macthing_keys.is_empty()) {
auto& keys = old_macthing_keys.first();
for (size_t i = 0; i < m_sequence_length - 1; ++i)
editor.insert(keys[i].key);
}
m_sequence_length = 0;
m_should_process_this_key = true;
return;
}
#ifdef CALLBACK_MACHINE_DEBUG
dbgln("seq_length={}, matching vector:", m_sequence_length);
for (auto& key : m_current_matching_keys) {
for (auto& k : key)
dbgln(" {}, {}", k.key, k.modifiers);
dbgln("");
}
#endif
m_should_process_this_key = false;
for (auto& key : m_current_matching_keys) {
if (key.size() == m_sequence_length) {
m_should_process_this_key = m_key_callbacks.get(key).value()->callback(editor);
m_sequence_length = 0;
m_current_matching_keys.clear();
return;
}
}
}
void KeyCallbackMachine::interrupted(Editor& editor)
{
m_sequence_length = 0;
m_current_matching_keys.clear();
if (auto callback = m_key_callbacks.get({ ctrl('C') }); callback.has_value())
m_should_process_this_key = callback.value()->callback(editor);
else
m_should_process_this_key = true;
}
}

View file

@ -0,0 +1,112 @@
/*
* 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/Function.h>
#include <AK/HashMap.h>
#include <AK/String.h>
#include <AK/Vector.h>
namespace Line {
class Editor;
struct Key {
enum Modifier : int {
None = 0,
Alt = 1,
};
int modifiers { None };
unsigned key { 0 };
Key(unsigned c)
: modifiers(None)
, key(c) {};
Key(unsigned c, int modifiers)
: modifiers(modifiers)
, key(c)
{
}
bool operator==(const Key& other) const
{
return other.key == key && other.modifiers == modifiers;
}
bool operator!=(const Key& other) const
{
return !(*this == other);
}
};
struct KeyCallback {
KeyCallback(Function<bool(Editor&)> cb)
: callback(move(cb))
{
}
Function<bool(Editor&)> callback;
};
class KeyCallbackMachine {
public:
void register_key_input_callback(Vector<Key>, Function<bool(Editor&)> callback);
void key_pressed(Editor&, Key);
void interrupted(Editor&);
bool should_process_last_pressed_key() const { return m_should_process_this_key; }
private:
HashMap<Vector<Key>, NonnullOwnPtr<KeyCallback>> m_key_callbacks;
Vector<Vector<Key>> m_current_matching_keys;
size_t m_sequence_length { 0 };
bool m_should_process_this_key { true };
};
}
namespace AK {
template<>
struct Traits<Line::Key> : public GenericTraits<Line::Key> {
static constexpr bool is_trivial() { return true; }
static unsigned hash(Line::Key k) { return pair_int_hash(k.key, k.modifiers); }
};
template<>
struct Traits<Vector<Line::Key>> : public GenericTraits<Vector<Line::Key>> {
static constexpr bool is_trivial() { return false; }
static unsigned hash(const Vector<Line::Key>& ks)
{
unsigned h = 0;
for (auto& k : ks)
h ^= Traits<Line::Key>::hash(k);
return h;
}
};
}

View file

@ -0,0 +1,55 @@
/*
* 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
namespace Line {
class Span {
public:
enum Mode {
ByteOriented,
CodepointOriented,
};
Span(size_t start, size_t end, Mode mode = ByteOriented)
: m_beginning(start)
, m_end(end)
, m_mode(mode)
{
}
size_t beginning() const { return m_beginning; }
size_t end() const { return m_end; }
Mode mode() const { return m_mode; }
private:
size_t m_beginning { 0 };
size_t m_end { 0 };
Mode m_mode { CodepointOriented };
};
}

View file

@ -0,0 +1,72 @@
/*
* Copyright (c) 2020-2021, 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 <AK/Vector.h>
namespace Line {
struct StringMetrics {
struct MaskedChar {
size_t position { 0 };
size_t original_length { 0 };
size_t masked_length { 0 };
};
struct LineMetrics {
Vector<MaskedChar> masked_chars;
size_t length { 0 };
size_t total_length(ssize_t offset = -1) const
{
size_t length = this->length;
for (auto& mask : masked_chars) {
if (offset < 0 || mask.position <= (size_t)offset) {
length -= mask.original_length;
length += mask.masked_length;
}
}
return length;
}
};
Vector<LineMetrics> line_metrics;
size_t total_length { 0 };
size_t max_line_length { 0 };
size_t lines_with_addition(const StringMetrics& offset, size_t column_width) const;
size_t offset_with_addition(const StringMetrics& offset, size_t column_width) const;
void reset()
{
line_metrics.clear();
total_length = 0;
max_line_length = 0;
line_metrics.append({ {}, 0 });
}
};
}

View file

@ -0,0 +1,184 @@
/*
* 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/String.h>
#include <AK/Types.h>
#include <AK/Vector.h>
#include <stdlib.h>
namespace Line {
class Style {
public:
enum class XtermColor : int {
Default = 9,
Black = 0,
Red,
Green,
Yellow,
Blue,
Magenta,
Cyan,
White,
Unchanged,
};
struct AnchoredTag {
};
struct UnderlineTag {
};
struct BoldTag {
};
struct ItalicTag {
};
struct Color {
explicit Color(XtermColor color)
: m_xterm_color(color)
, m_is_rgb(false)
{
}
Color(u8 r, u8 g, u8 b)
: m_rgb_color({ r, g, b })
, m_is_rgb(true)
{
}
bool is_default() const
{
return !m_is_rgb && m_xterm_color == XtermColor::Unchanged;
}
XtermColor m_xterm_color { XtermColor::Unchanged };
Vector<int, 3> m_rgb_color;
bool m_is_rgb { false };
};
struct Background : public Color {
explicit Background(XtermColor color)
: Color(color)
{
}
Background(u8 r, u8 g, u8 b)
: Color(r, g, b)
{
}
String to_vt_escape() const;
};
struct Foreground : public Color {
explicit Foreground(XtermColor color)
: Color(color)
{
}
Foreground(u8 r, u8 g, u8 b)
: Color(r, g, b)
{
}
String to_vt_escape() const;
};
struct Hyperlink {
explicit Hyperlink(const StringView& link)
: m_link(link)
{
m_has_link = true;
}
Hyperlink() { }
String to_vt_escape(bool starting) const;
bool is_empty() const { return !m_has_link; }
String m_link;
bool m_has_link { false };
};
static constexpr UnderlineTag Underline {};
static constexpr BoldTag Bold {};
static constexpr ItalicTag Italic {};
static constexpr AnchoredTag Anchored {};
// Prepare for the horror of templates.
template<typename T, typename... Rest>
Style(const T& style_arg, Rest... rest)
: Style(rest...)
{
set(style_arg);
m_is_empty = false;
}
Style() { }
static Style reset_style()
{
return { Foreground(XtermColor::Default), Background(XtermColor::Default), Hyperlink("") };
}
Style unified_with(const Style& other, bool prefer_other = true) const
{
Style style = *this;
style.unify_with(other, prefer_other);
return style;
}
void unify_with(const Style&, bool prefer_other = false);
bool underline() const { return m_underline; }
bool bold() const { return m_bold; }
bool italic() const { return m_italic; }
Background background() const { return m_background; }
Foreground foreground() const { return m_foreground; }
Hyperlink hyperlink() const { return m_hyperlink; }
void set(const ItalicTag&) { m_italic = true; }
void set(const BoldTag&) { m_bold = true; }
void set(const UnderlineTag&) { m_underline = true; }
void set(const Background& bg) { m_background = bg; }
void set(const Foreground& fg) { m_foreground = fg; }
void set(const Hyperlink& link) { m_hyperlink = link; }
void set(const AnchoredTag&) { m_is_anchored = true; }
bool is_anchored() const { return m_is_anchored; }
bool is_empty() const { return m_is_empty; }
String to_string() const;
private:
bool m_underline { false };
bool m_bold { false };
bool m_italic { false };
Background m_background { XtermColor::Unchanged };
Foreground m_foreground { XtermColor::Unchanged };
Hyperlink m_hyperlink;
bool m_is_anchored { false };
bool m_is_empty { true };
};
}

View file

@ -0,0 +1,104 @@
/*
* 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/StringMetrics.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 finish() = 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_row() const { return m_origin_row; }
size_t origin_col() const { return m_origin_column; }
void set_origin(int row, int col, Badge<Editor>)
{
m_origin_row = row;
m_origin_column = col;
}
protected:
int m_origin_row { 0 };
int m_origin_column { 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 finish() override
{
m_pages.clear();
}
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;
m_pages.clear();
}
private:
size_t fit_to_page_boundary(size_t selection_index);
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 };
struct PageRange {
size_t start;
size_t end;
};
Vector<PageRange> m_pages;
};
}

View file

@ -0,0 +1,196 @@
/*
* 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)
, is_valid(true)
{
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);
// make sure we were not given invalid suggestions
for (auto& suggestion : m_suggestions)
ASSERT(suggestion.is_valid);
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_code_point;
for (;; ++common_suggestion_prefix) {
if (m_suggestions[0].text_view.length() <= common_suggestion_prefix)
goto no_more_commons;
last_valid_suggestion_code_point = m_suggestions[0].text_view.code_points()[common_suggestion_prefix];
for (auto& suggestion : m_suggestions) {
if (suggestion.text_view.length() <= common_suggestion_prefix || suggestion.text_view.code_points()[common_suggestion_prefix] != last_valid_suggestion_code_point) {
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,161 @@
/*
* 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 {
private:
struct ForSearchTag {
};
public:
static constexpr ForSearchTag ForSearch {};
// Intentionally not explicit. (To allow suggesting bare strings)
CompletionSuggestion(const String& completion)
: CompletionSuggestion(completion, "", {})
{
}
CompletionSuggestion(const String& completion, ForSearchTag)
: text_string(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_string == text_string;
}
Vector<u32> text;
Vector<u32> trailing_trivia;
Style style;
size_t start_index { 0 };
size_t input_offset { 0 };
Utf32View text_view;
Utf32View trivia_view;
String text_string;
bool is_valid { false };
};
class SuggestionManager {
friend class Editor;
public:
void set_suggestions(Vector<CompletionSuggestion>&& suggestions);
void set_current_suggestion_initiation_index(size_t start_index);
size_t count() const { return m_suggestions.size(); }
size_t display_length() const { return m_last_shown_suggestion_display_length; }
size_t start_index() const { return m_last_displayed_suggestion_index; }
size_t next_index() const { return m_next_suggestion_index; }
void set_start_index(size_t index) const { m_last_displayed_suggestion_index = 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 };
mutable size_t m_last_displayed_suggestion_index { 0 };
size_t m_selected_suggestion_index { 0 };
};
}

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,205 @@
/*
* Copyright (c) 2020-2021, 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/BinarySearch.h>
#include <AK/Function.h>
#include <AK/StringBuilder.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 };
Vector<StringMetrics::LineMetrics> lines;
for (size_t i = 0; i < m_prompt_lines_at_suggestion_initiation - 1; ++i)
lines.append({ {}, 0 });
lines.append({ {}, longest_suggestion_length });
auto max_line_count = StringMetrics { move(lines) }.lines_with_addition({ { { {}, 0 } } }, 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) {
fputc('\n', stderr);
}
lines_used += max_line_count;
longest_suggestion_length = 0;
}
VT::move_absolute(max_line_count + m_origin_row, 1);
if (m_pages.is_empty()) {
size_t num_printed = 0;
size_t lines_used = 1;
// Cache the pages.
manager.set_start_index(0);
size_t page_start = 0;
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;
num_printed = 0;
}
if (lines_used + m_prompt_lines_at_suggestion_initiation >= m_num_lines) {
m_pages.append({ page_start, index });
page_start = index;
lines_used = 1;
num_printed = 0;
}
if (spans_entire_line)
num_printed += m_num_columns;
else
num_printed += longest_suggestion_length + 2;
return IterationDecision::Continue;
});
// Append the last page.
m_pages.append({ page_start, manager.count() });
}
auto page_index = fit_to_page_boundary(manager.next_index());
manager.set_start_index(m_pages[page_index].start);
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;
fputc('\n', stderr);
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 color 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(stderr);
}
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(stderr);
}
return IterationDecision::Continue;
});
m_lines_used_for_last_suggestions = lines_used;
// If we filled the screen, move back the origin.
if (m_origin_row + lines_used >= m_num_lines) {
m_origin_row = m_num_lines - lines_used;
}
if (m_pages.size() > 1) {
auto left_arrow = page_index > 0 ? '<' : ' ';
auto right_arrow = page_index < m_pages.size() - 1 ? '>' : ' ';
auto string = String::format("%c page %zu of %zu %c", left_arrow, page_index + 1, m_pages.size(), right_arrow);
if (string.length() > m_num_columns - 1) {
// This would overflow into the next line, so just don't print an indicator.
fflush(stderr);
return;
}
VT::move_absolute(m_origin_row + lines_used, m_num_columns - string.length() - 1);
VT::apply_style({ Style::Background(Style::XtermColor::Green) });
fputs(string.characters(), stderr);
VT::apply_style(Style::reset_style());
}
fflush(stderr);
}
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;
}
size_t XtermSuggestionDisplay::fit_to_page_boundary(size_t selection_index)
{
ASSERT(m_pages.size() > 0);
size_t index = 0;
auto* match = binary_search(
m_pages.span(),
PageRange { selection_index, selection_index },
&index,
[](auto& a, auto& b) -> int {
if (a.start >= b.start && a.start < b.end)
return 0;
return a.start - b.start;
});
if (!match)
return m_pages.size() - 1;
return index;
}
}