diff --git a/Libraries/CMakeLists.txt b/Libraries/CMakeLists.txt index 2e5964d312..54fbfb39de 100644 --- a/Libraries/CMakeLists.txt +++ b/Libraries/CMakeLists.txt @@ -43,5 +43,3 @@ endif() if (WIN32) return() endif() - -add_subdirectory(LibLine) diff --git a/Libraries/LibLine/CMakeLists.txt b/Libraries/LibLine/CMakeLists.txt deleted file mode 100644 index 61a2163555..0000000000 --- a/Libraries/LibLine/CMakeLists.txt +++ /dev/null @@ -1,10 +0,0 @@ -set(SOURCES - Editor.cpp - InternalFunctions.cpp - KeyCallbackMachine.cpp - SuggestionManager.cpp - XtermSuggestionDisplay.cpp -) - -ladybird_lib(LibLine line) -target_link_libraries(LibLine PRIVATE LibCore LibUnicode) diff --git a/Libraries/LibLine/Editor.cpp b/Libraries/LibLine/Editor.cpp deleted file mode 100644 index 0e01289baa..0000000000 --- a/Libraries/LibLine/Editor.cpp +++ /dev/null @@ -1,2368 +0,0 @@ -/* - * Copyright (c) 2018-2020, Andreas Kling - * Copyright (c) 2021, the SerenityOS developers. - * - * SPDX-License-Identifier: BSD-2-Clause - */ - -#include "Editor.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace Line { - -Configuration Configuration::from_config(StringView libname) -{ - Configuration configuration; - auto config_file = Core::ConfigFile::open_for_lib(libname).release_value_but_fixme_should_propagate_errors(); - - // Read behavior options. - auto refresh = config_file->read_entry("behavior", "refresh", "lazy"); - auto operation = config_file->read_entry("behavior", "operation_mode"); - auto bracketed_paste = config_file->read_bool_entry("behavior", "bracketed_paste", true); - auto default_text_editor = config_file->read_entry("behavior", "default_text_editor"); - - Configuration::Flags flags { Configuration::Flags::None }; - if (bracketed_paste) - flags = static_cast(flags | Configuration::Flags::BracketedPaste); - - configuration.set(flags); - - if (refresh.equals_ignoring_ascii_case("lazy"sv)) - configuration.set(Configuration::Lazy); - else if (refresh.equals_ignoring_ascii_case("eager"sv)) - configuration.set(Configuration::Eager); - - if (operation.equals_ignoring_ascii_case("full"sv)) - configuration.set(Configuration::OperationMode::Full); - else if (operation.equals_ignoring_ascii_case("noescapesequences"sv)) - configuration.set(Configuration::OperationMode::NoEscapeSequences); - else if (operation.equals_ignoring_ascii_case("noninteractive"sv)) - configuration.set(Configuration::OperationMode::NonInteractive); - else - configuration.set(Configuration::OperationMode::Unset); - - if (!default_text_editor.is_empty()) - configuration.set(DefaultTextEditor { move(default_text_editor) }); - else - configuration.set(DefaultTextEditor { "/bin/TextEditor" }); - - // Read keybinds. - - for (auto& binding_key : config_file->keys("keybinds")) { - GenericLexer key_lexer(binding_key); - auto has_ctrl = false; - auto alt = false; - auto escape = false; - Vector keys; - - while (!key_lexer.is_eof()) { - unsigned key; - if (escape) { - key = key_lexer.consume_escaped_character(); - escape = false; - } else { - if (key_lexer.next_is("alt+"sv)) { - alt = key_lexer.consume_specific("alt+"sv); - continue; - } - if (key_lexer.next_is("^["sv)) { - alt = key_lexer.consume_specific("^["sv); - continue; - } - if (key_lexer.next_is("^"sv)) { - has_ctrl = key_lexer.consume_specific("^"sv); - continue; - } - if (key_lexer.next_is("ctrl+"sv)) { - has_ctrl = key_lexer.consume_specific("ctrl+"sv); - continue; - } - if (key_lexer.next_is("\\"sv)) { - escape = true; - continue; - } - // FIXME: Support utf? - key = key_lexer.consume(); - } - if (has_ctrl) - key = ctrl(key); - - keys.append(Key { key, alt ? Key::Alt : Key::None }); - alt = false; - has_ctrl = false; - } - - GenericLexer value_lexer { config_file->read_entry("keybinds", binding_key) }; - StringBuilder value_builder; - while (!value_lexer.is_eof()) - value_builder.append(value_lexer.consume_escaped_character()); - auto value = value_builder.string_view(); - if (value.starts_with("internal:"sv)) { - configuration.set(KeyBinding { - keys, - KeyBinding::Kind::InternalFunction, - value.substring_view(9, value.length() - 9) }); - } else { - configuration.set(KeyBinding { - keys, - KeyBinding::Kind::Insertion, - value }); - } - } - - return configuration; -} - -void Editor::set_default_keybinds() -{ - register_key_input_callback(ctrl('N'), EDITOR_INTERNAL_FUNCTION(search_forwards)); - register_key_input_callback(ctrl('P'), EDITOR_INTERNAL_FUNCTION(search_backwards)); - register_key_input_callback(ctrl('A'), EDITOR_INTERNAL_FUNCTION(go_home)); - register_key_input_callback(ctrl('B'), EDITOR_INTERNAL_FUNCTION(cursor_left_character)); - register_key_input_callback(ctrl('D'), EDITOR_INTERNAL_FUNCTION(erase_character_forwards)); - register_key_input_callback(ctrl('E'), EDITOR_INTERNAL_FUNCTION(go_end)); - register_key_input_callback(ctrl('F'), EDITOR_INTERNAL_FUNCTION(cursor_right_character)); - // ^H: ctrl('H') == '\b' - register_key_input_callback(ctrl('H'), EDITOR_INTERNAL_FUNCTION(erase_character_backwards)); - // DEL - Some terminals send this instead of ^H. - register_key_input_callback((char)127, EDITOR_INTERNAL_FUNCTION(erase_character_backwards)); - register_key_input_callback(ctrl('K'), EDITOR_INTERNAL_FUNCTION(erase_to_end)); - register_key_input_callback(ctrl('L'), EDITOR_INTERNAL_FUNCTION(clear_screen)); - register_key_input_callback(ctrl('R'), EDITOR_INTERNAL_FUNCTION(enter_search)); - register_key_input_callback(ctrl(']'), EDITOR_INTERNAL_FUNCTION(search_character_forwards)); - register_key_input_callback(Key { ctrl(']'), Key::Alt }, EDITOR_INTERNAL_FUNCTION(search_character_backwards)); - register_key_input_callback(ctrl('T'), EDITOR_INTERNAL_FUNCTION(transpose_characters)); - register_key_input_callback('\n', EDITOR_INTERNAL_FUNCTION(finish)); - - // ^X^E: Edit in external editor - register_key_input_callback(Vector { ctrl('X'), ctrl('E') }, EDITOR_INTERNAL_FUNCTION(edit_in_external_editor)); - - // ^[.: alt-.: insert last arg of previous command (similar to `!$`) - register_key_input_callback(Key { '.', Key::Alt }, EDITOR_INTERNAL_FUNCTION(insert_last_words)); - register_key_input_callback(ctrl('Y'), EDITOR_INTERNAL_FUNCTION(insert_last_erased)); - register_key_input_callback(Key { 'b', Key::Alt }, EDITOR_INTERNAL_FUNCTION(cursor_left_word)); - register_key_input_callback(Key { 'f', Key::Alt }, EDITOR_INTERNAL_FUNCTION(cursor_right_word)); - register_key_input_callback(Key { ctrl('B'), Key::Alt }, EDITOR_INTERNAL_FUNCTION(cursor_left_nonspace_word)); - register_key_input_callback(Key { ctrl('F'), Key::Alt }, EDITOR_INTERNAL_FUNCTION(cursor_right_nonspace_word)); - // ^[^H: alt-backspace: backward delete word - register_key_input_callback(Key { '\b', Key::Alt }, EDITOR_INTERNAL_FUNCTION(erase_alnum_word_backwards)); - register_key_input_callback(Key { 'd', Key::Alt }, EDITOR_INTERNAL_FUNCTION(erase_alnum_word_forwards)); - register_key_input_callback(Key { '\\', Key::Alt }, EDITOR_INTERNAL_FUNCTION(erase_spaces)); - register_key_input_callback(Key { 'c', Key::Alt }, EDITOR_INTERNAL_FUNCTION(capitalize_word)); - register_key_input_callback(Key { 'l', Key::Alt }, EDITOR_INTERNAL_FUNCTION(lowercase_word)); - register_key_input_callback(Key { 'u', Key::Alt }, EDITOR_INTERNAL_FUNCTION(uppercase_word)); - register_key_input_callback(Key { 't', Key::Alt }, EDITOR_INTERNAL_FUNCTION(transpose_words)); - - // Register these last to all the user to override the previous key bindings - // Normally ^W. `stty werase \^n` can change it to ^N (or something else). - register_key_input_callback(m_termios.c_cc[VWERASE], EDITOR_INTERNAL_FUNCTION(erase_word_backwards)); - // Normally ^U. `stty kill \^n` can change it to ^N (or something else). - register_key_input_callback(m_termios.c_cc[VKILL], EDITOR_INTERNAL_FUNCTION(kill_line)); - register_key_input_callback(m_termios.c_cc[VERASE], EDITOR_INTERNAL_FUNCTION(erase_character_backwards)); -} - -Editor::Editor(Configuration configuration) - : m_configuration(move(configuration)) -{ - m_always_refresh = m_configuration.refresh_behavior == Configuration::RefreshBehavior::Eager; - m_pending_chars = {}; - get_terminal_size(); - m_suggestion_display = make(m_num_lines, m_num_columns); -} - -Editor::~Editor() -{ - if (m_initialized) - restore(); -} - -void Editor::ensure_free_lines_from_origin(size_t count) -{ - if (count > m_num_lines) { - // FIXME: Implement paging - } - - if (m_origin_row + count <= m_num_lines) - return; - - auto diff = m_origin_row + count - m_num_lines - 1; - out(stderr, "\x1b[{}S", diff); - fflush(stderr); - m_origin_row -= diff; - m_refresh_needed = false; - m_chars_touched_in_the_middle = 0; -} - -void Editor::get_terminal_size() -{ - struct winsize ws; - ioctl(STDERR_FILENO, TIOCGWINSZ, &ws); - if (ws.ws_col == 0 || ws.ws_row == 0) { - // LLDB uses ttys which "work" and then gives us a zero sized - // terminal which is far from useful - if (int fd = open("/dev/tty", O_RDONLY); fd != -1) { - ioctl(fd, TIOCGWINSZ, &ws); - close(fd); - } - } - m_num_columns = ws.ws_col; - m_num_lines = ws.ws_row; -} - -void Editor::add_to_history(ByteString const& line) -{ - if (line.is_empty()) - return; - ByteString histcontrol = getenv("HISTCONTROL"); - auto ignoredups = histcontrol == "ignoredups" || histcontrol == "ignoreboth"; - auto ignorespace = histcontrol == "ignorespace" || histcontrol == "ignoreboth"; - if (ignoredups && !m_history.is_empty() && line == m_history.last().entry) - return; - if (ignorespace && line.starts_with(' ')) - return; - if ((m_history.size() + 1) > m_history_capacity) - m_history.take_first(); - auto now = UnixDateTime::now(); - m_history.append({ line, now.seconds_since_epoch() }); - m_history_dirty = true; -} - -ErrorOr> Editor::try_load_history(StringView path) -{ - auto history_file_or_error = Core::File::open(path, Core::File::OpenMode::Read); - - // We ignore "No such file or directory" errors, as that is just equivalent to an empty history. - if (history_file_or_error.is_error() && history_file_or_error.error().is_errno() && history_file_or_error.error().code() == ENOENT) - return Vector {}; - - auto history_file = history_file_or_error.release_value(); - auto data = TRY(history_file->read_until_eof()); - auto hist = StringView { data }; - Vector history; - for (auto& str : hist.split_view("\n\n"sv)) { - auto it = str.find("::"sv).value_or(0); - auto time = str.substring_view(0, it).to_number().value_or(0); - auto string = str.substring_view(it == 0 ? it : it + 2); - history.append({ string, time }); - } - return history; -} - -bool Editor::load_history(ByteString const& path) -{ - auto history_or_error = try_load_history(path); - if (history_or_error.is_error()) - return false; - auto maybe_error = m_history.try_extend(history_or_error.release_value()); - auto okay = !maybe_error.is_error(); - return okay; -} - -template -static void merge(It0&& begin0, It0 const& end0, It1&& begin1, It1 const& end1, OutputT& output, LessThan less_than) -{ - for (;;) { - if (begin0 == end0 && begin1 == end1) - return; - - if (begin0 == end0) { - auto&& right = *begin1; - if (output.last().entry != right.entry) - output.append(right); - ++begin1; - continue; - } - - auto&& left = *begin0; - if (left.entry.is_whitespace()) { - ++begin0; - continue; - } - if (begin1 == end1) { - if (output.last().entry != left.entry) - output.append(left); - ++begin0; - continue; - } - - auto&& right = *begin1; - if (less_than(left, right)) { - if (output.last().entry != left.entry) - output.append(left); - ++begin0; - } else { - if (output.last().entry != right.entry) - output.append(right); - ++begin1; - if (right.entry == left.entry) - ++begin0; - } - } -} - -bool Editor::save_history(ByteString const& path) -{ - // Note: Use a dummy entry to simplify merging. - Vector final_history { { "", 0 } }; - { - auto history_or_error = try_load_history(path); - if (history_or_error.is_error()) - return false; - Vector old_history = history_or_error.release_value(); - merge( - old_history.begin(), old_history.end(), - m_history.begin(), m_history.end(), - final_history, - [](HistoryEntry const& left, HistoryEntry const& right) { return left.timestamp < right.timestamp; }); - } - - auto file_or_error = Core::File::open(path, Core::File::OpenMode::Write, 0600); - if (file_or_error.is_error()) - return false; - auto file = file_or_error.release_value(); - // Skip the dummy entry: - for (auto iter = final_history.begin() + 1; iter != final_history.end(); ++iter) { - auto const& entry = *iter; - auto buffer = ByteString::formatted("{}::{}\n\n", entry.timestamp, entry.entry); - auto maybe_error = file->write_until_depleted(buffer.bytes()); - if (maybe_error.is_error()) - return false; - } - - m_history_dirty = false; - return true; -} - -void Editor::clear_line() -{ - for (size_t i = 0; i < m_cursor; ++i) - fputc(0x8, stderr); - fputs("\033[K", stderr); - fflush(stderr); - m_chars_touched_in_the_middle = buffer().size(); - m_buffer.clear(); - m_cursor = 0; - m_inline_search_cursor = m_cursor; -} - -void Editor::insert(Utf32View const& string) -{ - for (size_t i = 0; i < string.length(); ++i) - insert(string.code_points()[i]); -} - -void Editor::insert(ByteString const& string) -{ - for (auto ch : Utf8View { string }) - insert(ch); -} - -void Editor::insert(StringView string_view) -{ - auto view = Utf8View { string_view }; - insert(view); -} - -void Editor::insert(Utf8View& view) -{ - for (auto ch : view) - insert(ch); -} - -void Editor::insert(u32 const cp) -{ - StringBuilder builder; - builder.append(Utf32View(&cp, 1)); - auto str = builder.to_byte_string(); - if (m_pending_chars.try_append(str.characters(), str.length()).is_error()) - return; - - readjust_anchored_styles(m_cursor, ModificationKind::Insertion); - - if (m_cursor == m_buffer.size()) { - m_buffer.append(cp); - m_cursor = m_buffer.size(); - m_inline_search_cursor = m_cursor; - return; - } - - m_buffer.insert(m_cursor, cp); - ++m_chars_touched_in_the_middle; - ++m_cursor; - m_inline_search_cursor = m_cursor; -} - -void Editor::register_key_input_callback(KeyBinding const& binding) -{ - if (binding.kind == KeyBinding::Kind::InternalFunction) { - auto internal_function = find_internal_function(binding.binding); - if (!internal_function) { - dbgln("LibLine: Unknown internal function '{}'", binding.binding); - return; - } - return register_key_input_callback(binding.keys, move(internal_function)); - } - - return register_key_input_callback(binding.keys, [binding = ByteString(binding.binding)](auto& editor) { - editor.insert(binding); - return false; - }); -} - -static size_t code_point_length_in_utf8(u32 code_point) -{ - if (code_point <= 0x7f) - return 1; - if (code_point <= 0x07ff) - return 2; - if (code_point <= 0xffff) - return 3; - if (code_point <= 0x10ffff) - return 4; - return 3; -} - -// buffer [ 0 1 2 3 . . . A . . . B . . . M . . . N ] -// ^ ^ ^ ^ -// | | | +- end of buffer -// | | +- scan offset = M -// | +- range end = M - B -// +- range start = M - A -// This method converts a byte range defined by [start_byte_offset, end_byte_offset] to a code_point range [M - A, M - B] as shown in the diagram above. -// If `reverse' is true, A and B are before M, if not, A and B are after M. -Editor::CodepointRange Editor::byte_offset_range_to_code_point_offset_range(size_t start_byte_offset, size_t end_byte_offset, size_t scan_code_point_offset, bool reverse) const -{ - size_t byte_offset = 0; - size_t code_point_offset = scan_code_point_offset + (reverse ? 1 : 0); - CodepointRange range; - - for (;;) { - if (!reverse) { - if (code_point_offset >= m_buffer.size()) - break; - } else { - if (code_point_offset == 0) - break; - } - - if (byte_offset > end_byte_offset) - break; - - if (byte_offset < start_byte_offset) - ++range.start; - - if (byte_offset < end_byte_offset) - ++range.end; - - byte_offset += code_point_length_in_utf8(m_buffer[reverse ? --code_point_offset : code_point_offset++]); - } - - return range; -} - -void Editor::stylize(Span const& span, Style const& style) -{ - if (!span.is_empty()) - return; - if (style.is_empty()) - return; - - auto start = span.beginning(); - auto end = span.end(); - - if (span.mode() == Span::ByteOriented) { - auto offsets = byte_offset_range_to_code_point_offset_range(start, end, 0); - - start = offsets.start; - end = offsets.end; - } - - if (auto maybe_mask = style.mask(); maybe_mask.has_value()) { - auto it = m_current_masks.find_smallest_not_below_iterator(span.beginning()); - Optional last_encountered_entry; - if (!it.is_end()) { - // Delete all overlapping old masks. - while (true) { - auto next_it = m_current_masks.find_largest_not_above_iterator(span.end()); - if (next_it.is_end()) - break; - if (it->has_value()) - last_encountered_entry = *it; - m_current_masks.remove(next_it.key()); - } - } - m_current_masks.insert(span.beginning(), move(maybe_mask)); - m_current_masks.insert(span.end(), {}); - if (last_encountered_entry.has_value()) - m_current_masks.insert(span.end() + 1, move(last_encountered_entry)); - style.unset_mask(); - } - - auto& spans_starting = style.is_anchored() ? m_current_spans.m_anchored_spans_starting : m_current_spans.m_spans_starting; - auto& spans_ending = style.is_anchored() ? m_current_spans.m_anchored_spans_ending : m_current_spans.m_spans_ending; - - auto& starting_map = spans_starting.ensure(start); - if (!starting_map.contains(end)) - m_refresh_needed = true; - starting_map.set(end, style); - - auto& ending_map = spans_ending.ensure(end); - if (!ending_map.contains(start)) - m_refresh_needed = true; - ending_map.set(start, style); -} - -void Editor::transform_suggestion_offsets(size_t& invariant_offset, size_t& static_offset, Span::Mode offset_mode) const -{ - auto internal_static_offset = static_offset; - auto internal_invariant_offset = invariant_offset; - if (offset_mode == Span::Mode::ByteOriented) { - // FIXME: We're assuming that invariant_offset points to the end of the available data - // this is not necessarily true, but is true in most cases. - auto offsets = byte_offset_range_to_code_point_offset_range(internal_static_offset, internal_invariant_offset + internal_static_offset, m_cursor - 1, true); - - internal_static_offset = offsets.start; - internal_invariant_offset = offsets.end - offsets.start; - } - invariant_offset = internal_invariant_offset; - static_offset = internal_static_offset; -} - -void Editor::initialize() -{ - if (m_initialized) - return; - - struct termios termios; - tcgetattr(0, &termios); - m_default_termios = termios; // grab a copy to restore - - get_terminal_size(); - - if (m_configuration.operation_mode == Configuration::Unset) { - auto istty = isatty(STDIN_FILENO) && isatty(STDERR_FILENO); - if (!istty) { - m_configuration.set(Configuration::NonInteractive); - } else { - auto* term = getenv("TERM"); - if ((term != NULL) && StringView { term, strlen(term) }.starts_with("xterm"sv)) - m_configuration.set(Configuration::Full); - else - m_configuration.set(Configuration::NoEscapeSequences); - } - } - - // Because we use our own line discipline which includes echoing, - // we disable ICANON and ECHO. - if (m_configuration.operation_mode == Configuration::Full) { - termios.c_lflag &= ~(ECHO | ICANON); - tcsetattr(0, TCSANOW, &termios); - } - - m_termios = termios; - - set_default_keybinds(); - for (auto& keybind : m_configuration.keybindings) - register_key_input_callback(keybind); - - if (m_configuration.m_signal_mode == Configuration::WithSignalHandlers) { - m_signal_handlers.append(Core::EventLoop::register_signal(SIGINT, [this](int) { - Core::EventLoop::current().deferred_invoke([this] { interrupted().release_value_but_fixme_should_propagate_errors(); }); - })); - - m_signal_handlers.append(Core::EventLoop::register_signal(SIGWINCH, [this](int) { - Core::EventLoop::current().deferred_invoke([this] { resized().release_value_but_fixme_should_propagate_errors(); }); - })); - } - - m_initialized = true; -} - -void Editor::refetch_default_termios() -{ - struct termios termios; - tcgetattr(0, &termios); - m_default_termios = termios; - if (m_configuration.operation_mode == Configuration::Full) - termios.c_lflag &= ~(ECHO | ICANON); - m_termios = termios; -} - -ErrorOr Editor::interrupted() -{ - if (m_is_searching) - return m_search_editor->interrupted(); - - if (!m_is_editing) - return {}; - - m_was_interrupted = true; - handle_interrupt_event(); - if (!m_finish || !m_previous_interrupt_was_handled_as_interrupt) - return {}; - - m_finish = false; - { - auto stderr_stream = TRY(Core::File::standard_error()); - TRY(reposition_cursor(*stderr_stream, true)); - if (TRY(m_suggestion_display->cleanup())) { - TRY(reposition_cursor(*stderr_stream, true)); - TRY(cleanup_suggestions()); - } - TRY(stderr_stream->write_until_depleted("\r"sv.bytes())); - } - m_buffer.clear(); - m_chars_touched_in_the_middle = buffer().size(); - m_is_editing = false; - restore(); - m_notifier->set_enabled(false); - m_notifier = nullptr; - Core::EventLoop::current().quit(Retry); - return {}; -} - -ErrorOr Editor::resized() -{ - m_was_resized = true; - m_previous_num_columns = m_num_columns; - auto old_origin_row = m_origin_row; - auto old_origin_column = m_origin_column; - - get_terminal_size(); - - if (!m_has_origin_reset_scheduled) { - // Reset the origin, but make sure it doesn't blow up if we can't read it - if (set_origin(false)) { - // The origin we have right now actually points to where the cursor should be (in the middle of the buffer somewhere) - // Find the "true" origin. - auto current_buffer_metrics = actual_rendered_string_metrics(buffer_view(), m_current_masks); - auto lines = m_cached_prompt_metrics.lines_with_addition(current_buffer_metrics, m_num_columns); - auto offset = m_cached_prompt_metrics.offset_with_addition(current_buffer_metrics, m_num_columns); - if (lines > m_origin_row) - m_origin_row = 1; - else - m_origin_row -= lines - 1; // the prompt and the origin share a line. - - if (offset > m_origin_column) - m_origin_column = 1; - else - m_origin_column -= offset; - - set_origin(m_origin_row, m_origin_column); - - TRY(handle_resize_event(false)); - if (old_origin_column != m_origin_column || old_origin_row != m_origin_row) { - m_expected_origin_changed = true; - deferred_invoke([this] { - (void)refresh_display(); - }); - } - } else { - deferred_invoke([this] { handle_resize_event(true).release_value_but_fixme_should_propagate_errors(); }); - m_has_origin_reset_scheduled = true; - } - } - - return {}; -} - -ErrorOr Editor::handle_resize_event(bool reset_origin) -{ - m_has_origin_reset_scheduled = false; - if (reset_origin && !set_origin(false)) { - m_has_origin_reset_scheduled = true; - deferred_invoke([this] { handle_resize_event(true).release_value_but_fixme_should_propagate_errors(); }); - return {}; - } - - set_origin(m_origin_row, 1); - - auto stderr_stream = TRY(Core::File::standard_error()); - - TRY(reposition_cursor(*stderr_stream, true)); - TRY(m_suggestion_display->redisplay(m_suggestion_manager, m_num_lines, m_num_columns)); - m_origin_row = m_suggestion_display->origin_row(); - TRY(reposition_cursor(*stderr_stream)); - - if (m_is_searching) - TRY(m_search_editor->resized()); - - return {}; -} - -ErrorOr Editor::really_quit_event_loop() -{ - m_finish = false; - { - auto stderr_stream = TRY(Core::File::standard_error()); - TRY(reposition_cursor(*stderr_stream, true)); - TRY(stderr_stream->write_until_depleted("\n"sv.bytes())); - } - auto string = line(); - m_buffer.clear(); - m_chars_touched_in_the_middle = buffer().size(); - m_is_editing = false; - - if (m_initialized) - restore(); - - m_returned_line = string; - m_notifier->set_enabled(false); - m_notifier = nullptr; - Core::EventLoop::current().quit(Exit); - return {}; -} - -auto Editor::get_line(ByteString const& prompt) -> Result -{ - initialize(); - m_is_editing = true; - - if (m_configuration.operation_mode == Configuration::NoEscapeSequences || m_configuration.operation_mode == Configuration::NonInteractive) { - // Do not use escape sequences, instead, use LibC's getline. - size_t size = 0; - char* line = nullptr; - // Show the prompt only on interactive mode (NoEscapeSequences in this case). - if (m_configuration.operation_mode != Configuration::NonInteractive) - fputs(prompt.characters(), stderr); - auto line_length = getline(&line, &size, stdin); - // getline() returns -1 and sets errno=0 on EOF. - if (line_length == -1) { - if (line) - free(line); - if (errno == 0) - return Error::Eof; - - return Error::ReadFailure; - } - restore(); - if (line) { - ByteString result { line, (size_t)line_length, Chomp }; - free(line); - return result; - } - - return Error::ReadFailure; - } - - auto old_cols = m_num_columns; - auto old_lines = m_num_lines; - get_terminal_size(); - - if (m_configuration.enable_bracketed_paste) - fprintf(stderr, "\x1b[?2004h"); - - if (m_num_columns != old_cols || m_num_lines != old_lines) - m_refresh_needed = true; - - set_prompt(prompt); - reset(); - strip_styles(true); - - { - auto stderr_stream = Core::File::standard_error().release_value_but_fixme_should_propagate_errors(); - auto prompt_lines = max(current_prompt_metrics().line_metrics.size(), 1ul) - 1; - for (size_t i = 0; i < prompt_lines; ++i) - stderr_stream->write_until_depleted("\n"sv.bytes()).release_value_but_fixme_should_propagate_errors(); - - VT::move_relative(-static_cast(prompt_lines), 0, *stderr_stream).release_value_but_fixme_should_propagate_errors(); - } - - set_origin(); - - m_history_cursor = m_history.size(); - - if (auto refresh_result = refresh_display(); refresh_result.is_error()) - m_input_error = Error::ReadFailure; - - Core::EventLoop loop; - - m_notifier = Core::Notifier::construct(STDIN_FILENO, Core::Notifier::Type::Read); - - if (m_input_error.has_value()) - loop.quit(Exit); - - m_notifier->on_activation = [&] { - if (try_update_once().is_error()) - loop.quit(Exit); - }; - - if (!m_incomplete_data.is_empty()) { - deferred_invoke([&] { - if (try_update_once().is_error()) - loop.quit(Exit); - }); - } - - if (loop.exec() == Retry) - return get_line(prompt); - - return m_input_error.has_value() ? Result { m_input_error.value() } : Result { m_returned_line }; -} - -ErrorOr Editor::try_update_once() -{ - if (m_was_interrupted) { - handle_interrupt_event(); - } - - TRY(handle_read_event()); - - if (m_always_refresh) - m_refresh_needed = true; - - TRY(refresh_display()); - - if (m_finish) - TRY(really_quit_event_loop()); - - return {}; -} - -void Editor::handle_interrupt_event() -{ - m_was_interrupted = false; - m_previous_interrupt_was_handled_as_interrupt = false; - - m_callback_machine.interrupted(*this); - if (!m_callback_machine.should_process_last_pressed_key()) - return; - - m_previous_interrupt_was_handled_as_interrupt = true; - - fprintf(stderr, "^C\n"); - fflush(stderr); - - if (on_interrupt_handled) - on_interrupt_handled(); - - m_buffer.clear(); - m_chars_touched_in_the_middle = buffer().size(); - m_cursor = 0; - set_origin(false); - - finish(); -} - -ErrorOr Editor::handle_read_event() -{ - if (m_prohibit_input_processing) { - m_have_unprocessed_read_event = true; - return {}; - } - - auto prohibit_scope = prohibit_input(); - - char keybuf[1024]; - ssize_t nread = 0; - - if (!m_incomplete_data.size()) - nread = read(0, keybuf, sizeof(keybuf)); - - if (nread < 0) { - if (errno == EINTR) { - if (!m_was_interrupted) { - if (m_was_resized) - return {}; - - finish(); - return {}; - } - - handle_interrupt_event(); - return {}; - } - - ScopedValueRollback errno_restorer(errno); - perror("read failed"); - - m_input_error = Error::ReadFailure; - finish(); - return {}; - } - - m_incomplete_data.append(keybuf, nread); - auto available_bytes = m_incomplete_data.size(); - - if (available_bytes == 0) { - m_input_error = Error::Empty; - finish(); - return {}; - } - - auto reverse_tab = false; - - // Discard starting bytes until they make sense as utf-8. - size_t valid_bytes = 0; - while (available_bytes > 0) { - Utf8View { StringView { m_incomplete_data.data(), available_bytes } }.validate(valid_bytes); - if (valid_bytes != 0) - break; - m_incomplete_data.take_first(); - --available_bytes; - } - - Utf8View input_view { StringView { m_incomplete_data.data(), valid_bytes } }; - size_t consumed_code_points = 0; - - static NeverDestroyed> s_csi_parameter_bytes; - static NeverDestroyed> s_csi_intermediate_bytes; - auto& csi_parameter_bytes = *s_csi_parameter_bytes; - auto& csi_intermediate_bytes = *s_csi_intermediate_bytes; - Vector csi_parameters; - u8 csi_final; - enum CSIMod { - Shift = 1, - Alt = 2, - Ctrl = 4, - }; - - for (auto code_point : input_view) { - if (m_finish) - break; - - ++consumed_code_points; - - if (code_point == 0) - continue; - - switch (m_state) { - case InputState::GotEscape: - switch (code_point) { - case '[': - m_state = InputState::CSIExpectParameter; - continue; - default: { - m_callback_machine.key_pressed(*this, { code_point, Key::Alt }); - m_state = InputState::Free; - TRY(cleanup_suggestions()); - continue; - } - } - case InputState::CSIExpectParameter: - if (code_point >= 0x30 && code_point <= 0x3f) { // '0123456789:;<=>?' - csi_parameter_bytes.append(code_point); - continue; - } - m_state = InputState::CSIExpectIntermediate; - [[fallthrough]]; - case InputState::CSIExpectIntermediate: - if (code_point >= 0x20 && code_point <= 0x2f) { // ' !"#$%&\'()*+,-./' - csi_intermediate_bytes.append(code_point); - continue; - } - m_state = InputState::CSIExpectFinal; - [[fallthrough]]; - case InputState::CSIExpectFinal: { - m_state = m_previous_free_state; - auto is_in_paste = m_state == InputState::Paste; - for (auto& parameter : ByteString::copy(csi_parameter_bytes).split(';')) { - if (auto value = parameter.to_number(); value.has_value()) - csi_parameters.append(value.value()); - else - csi_parameters.append(0); - } - unsigned param1 = 0, param2 = 0; - if (csi_parameters.size() >= 1) - param1 = csi_parameters[0]; - if (csi_parameters.size() >= 2) - param2 = csi_parameters[1]; - unsigned modifiers = param2 ? param2 - 1 : 0; - - if (is_in_paste && code_point != '~' && param1 != 201) { - // The only valid escape to process in paste mode is the stop-paste sequence. - // so treat everything else as part of the pasted data. - insert('\x1b'); - insert('['); - insert(StringView { csi_parameter_bytes.data(), csi_parameter_bytes.size() }); - insert(StringView { csi_intermediate_bytes.data(), csi_intermediate_bytes.size() }); - insert(code_point); - continue; - } - if (!(code_point >= 0x40 && code_point <= 0x7f)) { - dbgln("LibLine: Invalid CSI: {:02x} ({:c})", code_point, code_point); - continue; - } - csi_final = code_point; - csi_parameters.clear(); - csi_parameter_bytes.clear(); - csi_intermediate_bytes.clear(); - - if (csi_final == 'Z') { - // 'reverse tab' - reverse_tab = true; - break; - } - TRY(cleanup_suggestions()); - - switch (csi_final) { - case 'A': // ^[[A: arrow up - search_backwards(); - continue; - case 'B': // ^[[B: arrow down - search_forwards(); - continue; - case 'D': // ^[[D: arrow left - if (modifiers == CSIMod::Alt || modifiers == CSIMod::Ctrl) - cursor_left_word(); - else - cursor_left_character(); - continue; - case 'C': // ^[[C: arrow right - if (modifiers == CSIMod::Alt || modifiers == CSIMod::Ctrl) - cursor_right_word(); - else - cursor_right_character(); - continue; - case 'H': // ^[[H: home - go_home(); - continue; - case 'F': // ^[[F: end - go_end(); - continue; - case 127: - if (modifiers == CSIMod::Ctrl) - erase_alnum_word_backwards(); - else - erase_character_backwards(); - continue; - case '~': - if (param1 == 3) { // ^[[3~: delete - if (modifiers == CSIMod::Ctrl) - erase_alnum_word_forwards(); - else - erase_character_forwards(); - m_search_offset = 0; - continue; - } - if (m_configuration.enable_bracketed_paste) { - // ^[[200~: start bracketed paste - // ^[[201~: end bracketed paste - if (!is_in_paste && param1 == 200) { - m_state = InputState::Paste; - continue; - } - if (is_in_paste && param1 == 201) { - m_state = InputState::Free; - if (on_paste) { - on_paste(Utf32View { m_paste_buffer.data(), m_paste_buffer.size() }, *this); - m_paste_buffer.clear_with_capacity(); - } - if (!m_paste_buffer.is_empty()) - insert(Utf32View { m_paste_buffer.data(), m_paste_buffer.size() }); - continue; - } - } - // ^[[5~: page up - // ^[[6~: page down - dbgln("LibLine: Unhandled '~': {}", param1); - continue; - default: - dbgln("LibLine: Unhandled final: {:02x} ({:c})", code_point, code_point); - continue; - } - VERIFY_NOT_REACHED(); - } - case InputState::Verbatim: - m_state = InputState::Free; - // Verbatim mode will bypass all mechanisms and just insert the code point. - insert(code_point); - continue; - case InputState::Paste: - if (code_point == 27) { - m_previous_free_state = InputState::Paste; - m_state = InputState::GotEscape; - continue; - } - if (on_paste) - m_paste_buffer.append(code_point); - else - insert(code_point); - continue; - case InputState::Free: - m_previous_free_state = InputState::Free; - if (code_point == 27) { - m_callback_machine.key_pressed(*this, code_point); - // Note that this should also deal with explicitly registered keys - // that would otherwise be interpreted as escapes. - if (m_callback_machine.should_process_last_pressed_key()) - m_state = InputState::GotEscape; - continue; - } - if (code_point == 22) { // ^v - m_callback_machine.key_pressed(*this, code_point); - if (m_callback_machine.should_process_last_pressed_key()) - m_state = InputState::Verbatim; - continue; - } - break; - } - - // There are no sequences past this point, so short of 'tab', we will want to cleanup the suggestions. - ArmedScopeGuard suggestion_cleanup { [this] { cleanup_suggestions().release_value_but_fixme_should_propagate_errors(); } }; - - // Normally ^D. `stty eof \^n` can change it to ^N (or something else), but Serenity doesn't have `stty` yet. - // Process this here since the keybinds might override its behavior. - // This only applies when the buffer is empty. at any other time, the behavior should be configurable. - if (code_point == m_termios.c_cc[VEOF] && m_buffer.size() == 0) { - finish_edit(); - continue; - } - - m_callback_machine.key_pressed(*this, code_point); - if (!m_callback_machine.should_process_last_pressed_key()) - continue; - - m_search_offset = 0; // reset search offset on any key - - if (code_point == '\t' || reverse_tab) { - suggestion_cleanup.disarm(); - - if (!on_tab_complete) - continue; - - // Reverse tab can count as regular tab here. - m_times_tab_pressed++; - - int token_start = m_cursor; - - // Ask for completions only on the first tab - // and scan for the largest common prefix to display, - // further tabs simply show the cached completions. - if (m_times_tab_pressed == 1) { - m_suggestion_manager.set_suggestions(on_tab_complete(*this)); - m_suggestion_manager.set_start_index(0); - m_prompt_lines_at_suggestion_initiation = num_lines(); - if (m_suggestion_manager.count() == 0) { - // There are no suggestions, beep. - fputc('\a', stderr); - fflush(stderr); - } - } - - // Adjust already incremented / decremented index when switching tab direction. - if (reverse_tab && m_tab_direction != TabDirection::Backward) { - m_suggestion_manager.previous(); - m_suggestion_manager.previous(); - m_tab_direction = TabDirection::Backward; - } - if (!reverse_tab && m_tab_direction != TabDirection::Forward) { - m_suggestion_manager.next(); - m_suggestion_manager.next(); - m_tab_direction = TabDirection::Forward; - } - reverse_tab = false; - - SuggestionManager::CompletionMode completion_mode; - switch (m_times_tab_pressed) { - case 1: - completion_mode = SuggestionManager::CompletePrefix; - break; - case 2: - completion_mode = SuggestionManager::ShowSuggestions; - break; - default: - completion_mode = SuggestionManager::CycleSuggestions; - break; - } - - insert(Utf32View { m_remembered_suggestion_static_data.data(), m_remembered_suggestion_static_data.size() }); - m_remembered_suggestion_static_data.clear_with_capacity(); - - auto completion_result = m_suggestion_manager.attempt_completion(completion_mode, token_start); - - auto new_cursor = m_cursor; - - new_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); - - new_cursor -= completion_result.static_offset_from_cursor; - for (size_t i = 0; i < completion_result.static_offset_from_cursor; ++i) { - m_remembered_suggestion_static_data.append(m_buffer[new_cursor]); - remove_at_index(new_cursor); - } - - m_cursor = new_cursor; - m_inline_search_cursor = new_cursor; - m_refresh_needed = true; - m_chars_touched_in_the_middle++; - - for (auto& view : completion_result.insert) - insert(view); - - auto stderr_stream = TRY(Core::File::standard_error()); - TRY(reposition_cursor(*stderr_stream)); - - 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; - m_remembered_suggestion_static_data.clear_with_capacity(); - break; - case SuggestionManager::CompletePrefix: - break; - default: - ++m_times_tab_pressed; - break; - } - - if (m_times_tab_pressed > 1 && m_suggestion_manager.count() > 0) { - if (TRY(m_suggestion_display->cleanup())) - TRY(reposition_cursor(*stderr_stream)); - - m_suggestion_display->set_initial_prompt_lines(m_prompt_lines_at_suggestion_initiation); - - TRY(m_suggestion_display->display(m_suggestion_manager)); - - m_origin_row = m_suggestion_display->origin_row(); - } - - if (m_times_tab_pressed > 2) { - if (m_tab_direction == TabDirection::Forward) - m_suggestion_manager.next(); - else - m_suggestion_manager.previous(); - } - - if (m_suggestion_manager.count() < 2 && !completion_result.avoid_committing_to_single_suggestion) { - // We have none, or just one suggestion, - // we should just commit that and continue - // after it, as if it were auto-completed. - TRY(reposition_cursor(*stderr_stream, true)); - TRY(cleanup_suggestions()); - m_remembered_suggestion_static_data.clear_with_capacity(); - } - continue; - } - - // If we got here, manually cleanup the suggestions and then insert the new code point. - m_remembered_suggestion_static_data.clear_with_capacity(); - suggestion_cleanup.disarm(); - TRY(cleanup_suggestions()); - insert(code_point); - } - - if (consumed_code_points == input_view.length()) { - m_incomplete_data.clear(); - } else { - auto bytes_to_drop = input_view.byte_offset_of(consumed_code_points + 1); - m_incomplete_data.remove(0, bytes_to_drop); - } - - if (!m_incomplete_data.is_empty() && !m_finish) - deferred_invoke([&] { try_update_once().release_value_but_fixme_should_propagate_errors(); }); - - return {}; -} - -ErrorOr Editor::cleanup_suggestions() -{ - if (m_times_tab_pressed != 0) { - // 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 }, m_suggestion_manager.current_suggestion().style); - // We probably have some suggestions drawn, - // let's clean them up. - if (TRY(m_suggestion_display->cleanup())) { - auto stderr_stream = TRY(Core::File::standard_error()); - TRY(reposition_cursor(*stderr_stream)); - m_refresh_needed = true; - } - m_suggestion_manager.reset(); - m_suggestion_display->finish(); - } - m_times_tab_pressed = 0; // Safe to say if we get here, the user didn't press TAB - return {}; -} - -bool Editor::search(StringView phrase, bool allow_empty, bool from_beginning) -{ - int last_matching_offset = -1; - bool found = false; - - // Do not search for empty strings. - if (allow_empty || phrase.length() > 0) { - size_t search_offset = m_search_offset; - for (size_t i = m_history_cursor; i > 0; --i) { - auto& entry = m_history[i - 1]; - auto contains = from_beginning ? entry.entry.starts_with(phrase) : entry.entry.contains(phrase); - if (contains) { - last_matching_offset = i - 1; - if (search_offset == 0) { - found = true; - break; - } - --search_offset; - } - } - - if (!found) { - fputc('\a', stderr); - fflush(stderr); - } - } - - if (found) { - // We plan to clear the buffer, so mark the entire thing touched. - m_chars_touched_in_the_middle = m_buffer.size(); - m_buffer.clear(); - m_cursor = 0; - insert(m_history[last_matching_offset].entry); - // Always needed, as we have cleared the buffer above. - m_refresh_needed = true; - } - - return found; -} - -void Editor::recalculate_origin() -{ - // Changing the columns can affect our origin if - // the new size is smaller than our prompt, which would - // cause said prompt to take up more space, so we should - // compensate for that. - if (m_cached_prompt_metrics.max_line_length >= m_num_columns) { - auto added_lines = (m_cached_prompt_metrics.max_line_length + 1) / m_num_columns - 1; - m_origin_row += added_lines; - } - - // We also need to recalculate our cursor position, - // but that will be calculated and applied at the next - // refresh cycle. -} - -ErrorOr Editor::cleanup() -{ - auto current_buffer_metrics = actual_rendered_string_metrics(buffer_view(), m_current_masks); - auto new_lines = current_prompt_metrics().lines_with_addition(current_buffer_metrics, m_num_columns); - if (new_lines < m_shown_lines) - m_extra_forward_lines = max(m_shown_lines - new_lines, m_extra_forward_lines); - - auto stderr_stream = TRY(Core::File::standard_error()); - TRY(reposition_cursor(*stderr_stream, true)); - auto current_line = num_lines() - 1; - TRY(VT::clear_lines(current_line, m_extra_forward_lines, *stderr_stream)); - m_extra_forward_lines = 0; - TRY(reposition_cursor(*stderr_stream)); - return {}; -} - -ErrorOr Editor::refresh_display() -{ - AllocatingMemoryStream output_stream; - ScopeGuard flush_stream { - [&] { - m_shown_lines = current_prompt_metrics().lines_with_addition(m_cached_buffer_metrics, m_num_columns); - - if (output_stream.used_buffer_size() == 0) - return; - - auto buffer = ByteBuffer::create_uninitialized(output_stream.used_buffer_size()).release_value_but_fixme_should_propagate_errors(); - output_stream.read_until_filled(buffer).release_value_but_fixme_should_propagate_errors(); - fwrite(buffer.data(), sizeof(char), buffer.size(), stderr); - } - }; - - auto has_cleaned_up = false; - // Someone changed the window size, figure it out - // and react to it, we might need to redraw. - if (m_was_resized) { - if (m_expected_origin_changed || m_previous_num_columns != m_num_columns) { - // We need to cleanup and redo everything. - m_expected_origin_changed = false; - m_cached_prompt_valid = false; - m_refresh_needed = true; - swap(m_previous_num_columns, m_num_columns); - recalculate_origin(); - TRY(cleanup()); - swap(m_previous_num_columns, m_num_columns); - has_cleaned_up = true; - } - m_was_resized = false; - } - // We might be at the last line, and have more than one line; - // Refreshing the display will cause the terminal to scroll, - // so note that fact and bring origin up, making sure to - // reserve the space for however many lines we move it up. - auto current_num_lines = num_lines(); - if (m_origin_row + current_num_lines > m_num_lines) { - if (current_num_lines > m_num_lines) { - for (size_t i = 0; i < m_num_lines; ++i) - TRY(output_stream.write_until_depleted("\n"sv.bytes())); - m_origin_row = 0; - } else { - auto old_origin_row = m_origin_row; - m_origin_row = m_num_lines - current_num_lines + 1; - for (size_t i = 0; i < old_origin_row - m_origin_row; ++i) - TRY(output_stream.write_until_depleted("\n"sv.bytes())); - } - } - // Do not call hook on pure cursor movement. - if (m_cached_prompt_valid && !m_refresh_needed && m_pending_chars.size() == 0) { - // Probably just moving around. - TRY(reposition_cursor(output_stream)); - m_cached_buffer_metrics = actual_rendered_string_metrics(buffer_view(), m_current_masks); - m_drawn_end_of_line_offset = m_buffer.size(); - return {}; - } - - if (on_display_refresh) - on_display_refresh(*this); - - if (m_cached_prompt_valid) { - if (!m_refresh_needed && m_cursor == m_buffer.size()) { - // Just write the characters out and continue, - // no need to refresh the entire line. - TRY(output_stream.write_until_depleted(m_pending_chars)); - m_pending_chars.clear(); - m_drawn_cursor = m_cursor; - m_drawn_end_of_line_offset = m_buffer.size(); - m_cached_buffer_metrics = actual_rendered_string_metrics(buffer_view(), m_current_masks); - m_drawn_spans = m_current_spans; - return {}; - } - } - - auto apply_styles = [&, empty_styles = HashMap {}](size_t i) -> ErrorOr { - auto& ends = m_current_spans.m_spans_ending.get(i).value_or<>(empty_styles); - auto& starts = m_current_spans.m_spans_starting.get(i).value_or<>(empty_styles); - - auto& anchored_ends = m_current_spans.m_anchored_spans_ending.get(i).value_or<>(empty_styles); - auto& anchored_starts = m_current_spans.m_anchored_spans_starting.get(i).value_or<>(empty_styles); - - if (ends.size() || anchored_ends.size()) { - Style style; - - for (auto& applicable_style : ends) - style.unify_with(applicable_style.value); - - for (auto& applicable_style : anchored_ends) - style.unify_with(applicable_style.value); - - // Disable any style that should be turned off. - TRY(VT::apply_style(style, output_stream, false)); - - // Reapply styles for overlapping spans that include this one. - style = find_applicable_style(i); - TRY(VT::apply_style(style, output_stream, true)); - } - if (starts.size() || anchored_starts.size()) { - Style style; - - for (auto& applicable_style : starts) - style.unify_with(applicable_style.value); - - for (auto& applicable_style : anchored_starts) - style.unify_with(applicable_style.value); - - // Set new styles. - TRY(VT::apply_style(style, output_stream, true)); - } - - return {}; - }; - - auto print_character_at = [&](size_t i) { - Variant c { Utf8View {} }; - if (auto it = m_current_masks.find_largest_not_above_iterator(i); !it.is_end() && it->has_value()) { - auto offset = i - it.key(); - if (it->value().mode == Style::Mask::Mode::ReplaceEntireSelection) { - auto& mask = it->value().replacement_view; - auto replacement = mask.begin().peek(offset); - if (!replacement.has_value()) - return; - c = replacement.value(); - ++it; - u32 next_offset = it.is_end() ? m_drawn_end_of_line_offset : it.key(); - if (i + 1 == next_offset) - c = mask.unicode_substring_view(offset, mask.length() - offset); - } else { - c = it->value().replacement_view; - } - } else { - c = m_buffer[i]; - } - auto print_single_character = [&](auto c) -> ErrorOr { - StringBuilder builder; - bool should_print_masked = is_ascii_control(c) && c != '\n'; - bool should_print_caret = c < 64 && should_print_masked; - if (should_print_caret) - builder.appendff("^{:c}", c + 64); - else if (should_print_masked) - builder.appendff("\\x{:0>2x}", c); - else - builder.append(Utf32View { &c, 1 }); - - if (should_print_masked) - TRY(output_stream.write_until_depleted("\033[7m"sv.bytes())); - - TRY(output_stream.write_until_depleted(builder.string_view().bytes())); - - if (should_print_masked) - TRY(output_stream.write_until_depleted("\033[27m"sv.bytes())); - - return {}; - }; - c.visit( - [&](u32 c) { print_single_character(c).release_value_but_fixme_should_propagate_errors(); }, - [&](auto& view) { for (auto c : view) print_single_character(c).release_value_but_fixme_should_propagate_errors(); }); - }; - - // If there have been no changes to previous sections of the line (style or text) - // just append the new text with the appropriate styles. - if (!m_always_refresh && m_cached_prompt_valid && m_chars_touched_in_the_middle == 0 && m_drawn_spans.contains_up_to_offset(m_current_spans, m_drawn_cursor)) { - auto initial_style = find_applicable_style(m_drawn_end_of_line_offset); - TRY(VT::apply_style(initial_style, output_stream)); - - for (size_t i = m_drawn_end_of_line_offset; i < m_buffer.size(); ++i) { - TRY(apply_styles(i)); - print_character_at(i); - } - - TRY(VT::apply_style(Style::reset_style(), output_stream)); - m_pending_chars.clear(); - m_refresh_needed = false; - m_cached_buffer_metrics = actual_rendered_string_metrics(buffer_view(), m_current_masks); - m_chars_touched_in_the_middle = 0; - m_drawn_cursor = m_cursor; - m_drawn_end_of_line_offset = m_buffer.size(); - - // No need to reposition the cursor, the cursor is already where it needs to be. - return {}; - } - - if constexpr (LINE_EDITOR_DEBUG) { - if (m_cached_prompt_valid && m_chars_touched_in_the_middle == 0) { - auto x = m_drawn_spans.contains_up_to_offset(m_current_spans, m_drawn_cursor); - dbgln("Contains: {} At offset: {}", x, m_drawn_cursor); - dbgln("Drawn Spans:"); - for (auto& sentry : m_drawn_spans.m_spans_starting) { - for (auto& entry : sentry.value) { - dbgln("{}-{}: {}", sentry.key, entry.key, entry.value.to_byte_string()); - } - } - dbgln("=========================================================================="); - dbgln("Current Spans:"); - for (auto& sentry : m_current_spans.m_spans_starting) { - for (auto& entry : sentry.value) { - dbgln("{}-{}: {}", sentry.key, entry.key, entry.value.to_byte_string()); - } - } - } - } - - // Ouch, reflow entire line. - if (!has_cleaned_up) { - TRY(cleanup()); - } - TRY(VT::move_absolute(m_origin_row, m_origin_column, output_stream)); - - TRY(output_stream.write_until_depleted(m_new_prompt.bytes())); - - TRY(VT::clear_to_end_of_line(output_stream)); - StringBuilder builder; - for (size_t i = 0; i < m_buffer.size(); ++i) { - TRY(apply_styles(i)); - print_character_at(i); - } - - TRY(VT::apply_style(Style::reset_style(), output_stream)); // don't bleed to EOL - - m_pending_chars.clear(); - m_refresh_needed = false; - m_cached_buffer_metrics = actual_rendered_string_metrics(buffer_view(), m_current_masks); - m_chars_touched_in_the_middle = 0; - m_drawn_spans = m_current_spans; - m_drawn_end_of_line_offset = m_buffer.size(); - m_cached_prompt_valid = true; - - TRY(reposition_cursor(output_stream)); - return {}; -} - -void Editor::strip_styles(bool strip_anchored) -{ - m_current_spans.m_spans_starting.clear(); - m_current_spans.m_spans_ending.clear(); - m_current_masks.clear(); - m_cached_buffer_metrics = actual_rendered_string_metrics(buffer_view(), {}); - - if (strip_anchored) { - m_current_spans.m_anchored_spans_starting.clear(); - m_current_spans.m_anchored_spans_ending.clear(); - } - - m_refresh_needed = true; -} - -ErrorOr Editor::reposition_cursor(Stream& stream, bool to_end) -{ - auto cursor = m_cursor; - auto saved_cursor = m_cursor; - if (to_end) - cursor = m_buffer.size(); - - m_cursor = cursor; - m_drawn_cursor = cursor; - - auto line = cursor_line() - 1; - auto column = offset_in_line(); - - ensure_free_lines_from_origin(line); - - VERIFY(column + m_origin_column <= m_num_columns); - TRY(VT::move_absolute(line + m_origin_row, column + m_origin_column, stream)); - - m_cursor = saved_cursor; - return {}; -} - -ErrorOr VT::move_absolute(u32 row, u32 col, Stream& stream) -{ - return stream.write_until_depleted(ByteString::formatted("\033[{};{}H", row, col)); -} - -ErrorOr VT::move_relative(int row, int col, Stream& stream) -{ - char x_op = 'A', y_op = 'D'; - - if (row > 0) - x_op = 'B'; - else - row = -row; - if (col > 0) - y_op = 'C'; - else - col = -col; - - if (row > 0) - TRY(stream.write_until_depleted(ByteString::formatted("\033[{}{}", row, x_op))); - if (col > 0) - TRY(stream.write_until_depleted(ByteString::formatted("\033[{}{}", col, y_op))); - - return {}; -} - -Style Editor::find_applicable_style(size_t offset) const -{ - // Walk through our styles and merge all that fit in the offset. - auto style = Style::reset_style(); - auto unify = [&](auto& entry) { - if (entry.key >= offset) - return; - for (auto& style_value : entry.value) { - if (style_value.key <= offset) - return; - style.unify_with(style_value.value, true); - } - }; - - for (auto& entry : m_current_spans.m_spans_starting) { - unify(entry); - } - - for (auto& entry : m_current_spans.m_anchored_spans_starting) { - unify(entry); - } - - return style; -} - -ByteString Style::Background::to_vt_escape() const -{ - if (is_default()) - return ""; - - if (m_is_rgb) { - return ByteString::formatted("\e[48;2;{};{};{}m", m_rgb_color[0], m_rgb_color[1], m_rgb_color[2]); - } else { - return ByteString::formatted("\e[{}m", (u8)m_xterm_color + 40); - } -} - -ByteString Style::Foreground::to_vt_escape() const -{ - if (is_default()) - return ""; - - if (m_is_rgb) { - return ByteString::formatted("\e[38;2;{};{};{}m", m_rgb_color[0], m_rgb_color[1], m_rgb_color[2]); - } else { - return ByteString::formatted("\e[{}m", (u8)m_xterm_color + 30); - } -} - -ByteString Style::Hyperlink::to_vt_escape(bool starting) const -{ - if (is_empty()) - return ""; - - return ByteString::formatted("\e]8;;{}\e\\", starting ? m_link : ByteString::empty()); -} - -void Style::unify_with(Style const& other, bool prefer_other) -{ - // Unify colors. - if (prefer_other || m_background.is_default()) - m_background = other.background(); - - if (prefer_other || m_foreground.is_default()) - m_foreground = other.foreground(); - - // Unify graphic renditions. - if (other.bold()) - set(Bold); - - if (other.italic()) - set(Italic); - - if (other.underline()) - set(Underline); - - // Unify links. - if (prefer_other || m_hyperlink.is_empty()) - m_hyperlink = other.hyperlink(); - - m_is_empty &= other.m_is_empty; -} - -ByteString Style::to_byte_string() const -{ - StringBuilder builder; - builder.append("Style { "sv); - - if (!m_foreground.is_default()) { - builder.append("Foreground("sv); - if (m_foreground.m_is_rgb) { - builder.join(", "sv, m_foreground.m_rgb_color); - } else { - builder.appendff("(XtermColor) {}", (int)m_foreground.m_xterm_color); - } - builder.append("), "sv); - } - - if (!m_background.is_default()) { - builder.append("Background("sv); - if (m_background.m_is_rgb) { - builder.join(' ', m_background.m_rgb_color); - } else { - builder.appendff("(XtermColor) {}", (int)m_background.m_xterm_color); - } - builder.append("), "sv); - } - - if (bold()) - builder.append("Bold, "sv); - - if (underline()) - builder.append("Underline, "sv); - - if (italic()) - builder.append("Italic, "sv); - - if (!m_hyperlink.is_empty()) - builder.appendff("Hyperlink(\"{}\"), ", m_hyperlink.m_link); - - if (!m_mask.has_value()) { - builder.appendff("Mask(\"{}\", {}), ", - m_mask->replacement, - m_mask->mode == Mask::Mode::ReplaceEntireSelection - ? "ReplaceEntireSelection" - : "ReplaceEachCodePointInSelection"); - } - - builder.append('}'); - - return builder.to_byte_string(); -} - -ErrorOr VT::apply_style(Style const& style, Stream& stream, bool is_starting) -{ - if (is_starting) { - TRY(stream.write_until_depleted(ByteString::formatted("\033[{};{};{}m{}{}{}", - style.bold() ? 1 : 22, - style.underline() ? 4 : 24, - style.italic() ? 3 : 23, - style.background().to_vt_escape(), - style.foreground().to_vt_escape(), - style.hyperlink().to_vt_escape(true)))); - } else { - TRY(stream.write_until_depleted(style.hyperlink().to_vt_escape(false))); - } - - return {}; -} - -ErrorOr VT::clear_lines(size_t count_above, size_t count_below, Stream& stream) -{ - if (count_below + count_above == 0) { - TRY(stream.write_until_depleted("\033[2K"sv)); - } else { - // Go down count_below lines. - if (count_below > 0) - TRY(stream.write_until_depleted(ByteString::formatted("\033[{}B", count_below))); - // Then clear lines going upwards. - for (size_t i = count_below + count_above; i > 0; --i) { - TRY(stream.write_until_depleted("\033[2K"sv)); - if (i != 1) - TRY(stream.write_until_depleted("\033[A"sv)); - } - } - - return {}; -} - -ErrorOr VT::save_cursor(Stream& stream) -{ - return stream.write_until_depleted("\033[s"sv); -} - -ErrorOr VT::restore_cursor(Stream& stream) -{ - return stream.write_until_depleted("\033[u"sv); -} - -ErrorOr VT::clear_to_end_of_line(Stream& stream) -{ - return stream.write_until_depleted("\033[K"sv); -} - -enum VTState { - Free = 1, - Escape = 3, - Bracket = 5, - BracketArgsSemi = 7, - Title = 9, - URL = 11, -}; -static VTState actual_rendered_string_length_step(StringMetrics& metrics, size_t index, StringMetrics::LineMetrics& current_line, u32 c, u32 next_c, VTState state, Optional const& mask, Optional const& maximum_line_width = {}, Optional last_return = {}); - -enum class MaskedSelectionDecision { - Skip, - Continue, -}; -static MaskedSelectionDecision resolve_masked_selection(Optional& mask, size_t& i, auto& mask_it, auto& view, auto& state, auto& metrics, auto& current_line) -{ - if (mask.has_value() && mask->mode == Style::Mask::Mode::ReplaceEntireSelection) { - ++mask_it; - auto actual_end_offset = mask_it.is_end() ? view.length() : mask_it.key(); - auto end_offset = min(actual_end_offset, view.length()); - size_t j = 0; - for (auto it = mask->replacement_view.begin(); it != mask->replacement_view.end(); ++it) { - auto it_copy = it; - ++it_copy; - auto next_c = it_copy == mask->replacement_view.end() ? 0 : *it_copy; - state = actual_rendered_string_length_step(metrics, j, current_line, *it, next_c, state, {}); - ++j; - if (j <= actual_end_offset - i && j + i >= view.length()) - break; - } - current_line.masked_chars.empend(i, end_offset - i, j); - i = end_offset; - - if (mask_it.is_end()) - mask = {}; - else - mask = *mask_it; - return MaskedSelectionDecision::Skip; - } - return MaskedSelectionDecision::Continue; -} - -StringMetrics Editor::actual_rendered_string_metrics(StringView string, RedBlackTree> const& masks, Optional maximum_line_width) -{ - Vector utf32_buffer; - utf32_buffer.ensure_capacity(string.length()); - for (auto c : Utf8View { string }) - utf32_buffer.append(c); - - return actual_rendered_string_metrics(Utf32View { utf32_buffer.data(), utf32_buffer.size() }, masks, maximum_line_width); -} - -StringMetrics Editor::actual_rendered_string_metrics(Utf32View const& view, RedBlackTree> const& masks, Optional maximum_line_width) -{ - StringMetrics metrics; - StringMetrics::LineMetrics current_line; - VTState state { Free }; - Optional mask; - size_t last_return { 0 }; - - auto mask_it = masks.begin(); - - auto segmenter = Unicode::Segmenter::create(Unicode::SegmenterGranularity::Grapheme); - Vector grapheme_breaks; - - segmenter->for_each_boundary(view, [&](size_t offset) -> IterationDecision { - if (offset >= view.length()) - return IterationDecision::Break; - - grapheme_breaks.append(offset); - return IterationDecision::Continue; - }); - - for (size_t break_index = 0; break_index < grapheme_breaks.size(); ++break_index) { - auto i = grapheme_breaks[break_index]; - auto c = view[i]; - if (!mask_it.is_end() && mask_it.key() <= i) - mask = *mask_it; - - if (resolve_masked_selection(mask, i, mask_it, view, state, metrics, current_line) == MaskedSelectionDecision::Skip) { - --i; - binary_search(grapheme_breaks, i, &break_index); - continue; - } - - auto next_c = break_index + 1 < grapheme_breaks.size() ? view.code_points()[grapheme_breaks[break_index + 1]] : 0; - state = actual_rendered_string_length_step(metrics, i, current_line, c, next_c, state, mask, maximum_line_width, last_return); - if (!mask_it.is_end() && mask_it.key() <= i) { - auto mask_it_peek = mask_it; - ++mask_it_peek; - if (!mask_it_peek.is_end() && mask_it_peek.key() > i) - mask_it = mask_it_peek; - } - } - - metrics.line_metrics.append(current_line); - - for (auto& line : metrics.line_metrics) - metrics.max_line_length = max(line.total_length(), metrics.max_line_length); - - metrics.grapheme_breaks = move(grapheme_breaks); - - return metrics; -} - -VTState actual_rendered_string_length_step(StringMetrics& metrics, size_t index, StringMetrics::LineMetrics& current_line, u32 c, u32 next_c, VTState state, Optional const& mask, Optional const& maximum_line_width, Optional last_return) -{ - auto const save_line = [&metrics, ¤t_line, &last_return, &index]() { - if (last_return.has_value()) { - auto const last_index = index - 1; - current_line.bit_length = last_index - *last_return + 1; - last_return.value() = last_index + 1; - } - metrics.line_metrics.append(current_line); - - current_line.masked_chars = {}; - current_line.length = 0; - current_line.visible_length = 0; - current_line.bit_length = {}; - }; - - // FIXME: current_line.visible_length can go above maximum_line_width when using masks - if (maximum_line_width.has_value() && current_line.visible_length >= maximum_line_width.value()) - save_line(); - - ScopeGuard bit_length_update { [&last_return, ¤t_line, &index]() { - if (last_return.has_value()) - current_line.bit_length = index - *last_return + 1; - } }; - - switch (state) { - case Free: { - if (c == '\x1b') { // escape - return Escape; - } - if (c == '\r') { // carriage return - current_line.masked_chars = {}; - current_line.length = 0; - current_line.visible_length = 0; - if (!metrics.line_metrics.is_empty()) - metrics.line_metrics.last() = { {}, 0 }; - return state; - } - if (c == '\n') { // return - save_line(); - return state; - } - if (c == '\t') { - // Tabs are a special case, because their width is variable. - ++current_line.length; - current_line.visible_length += (8 - (current_line.visible_length % 8)); - return state; - } - auto is_control = is_ascii_control(c); - if (is_control) { - if (mask.has_value()) - current_line.masked_chars.append({ index, 1, mask->replacement_view.length() }); - else - current_line.masked_chars.append({ index, 1, c < 64 ? 2u : 4u }); // if the character cannot be represented as ^c, represent it as \xbb. - } - // FIXME: This will not support anything sophisticated - if (mask.has_value()) { - current_line.length += mask->replacement_view.length(); - current_line.visible_length += mask->replacement_view.length(); - metrics.total_length += mask->replacement_view.length(); - } else if (is_control) { - current_line.length += current_line.masked_chars.last().masked_length; - current_line.visible_length += current_line.masked_chars.last().masked_length; - metrics.total_length += current_line.masked_chars.last().masked_length; - } else { - ++current_line.length; - ++current_line.visible_length; - ++metrics.total_length; - } - return state; - } - case Escape: - if (c == ']') { - if (next_c == '0') - state = Title; - if (next_c == '8') - state = URL; - return state; - } - if (c == '[') { - return Bracket; - } - // FIXME: This does not support non-VT (aside from set-title) escapes - return state; - case Bracket: - if (is_ascii_digit(c)) { - return BracketArgsSemi; - } - return state; - case BracketArgsSemi: - if (c == ';') { - return Bracket; - } - if (!is_ascii_digit(c)) - state = Free; - return state; - case Title: - if (c == 7) - state = Free; - return state; - case URL: - if (c == '\\') - state = Free; - return state; - } - return state; -} - -Result, Editor::Error> Editor::vt_dsr() -{ - char buf[16]; - - // Read whatever junk there is before talking to the terminal - // and insert them later when we're reading user input. - bool more_junk_to_read { false }; - timeval timeout { 0, 0 }; - fd_set readfds; - FD_ZERO(&readfds); - FD_SET(0, &readfds); - - do { - more_junk_to_read = false; - [[maybe_unused]] auto rc = select(1, &readfds, nullptr, nullptr, &timeout); - if (FD_ISSET(0, &readfds)) { - auto nread = read(0, buf, 16); - if (nread < 0) { - m_input_error = Error::ReadFailure; - finish(); - break; - } - - if (nread == 0) - break; - - m_incomplete_data.append(buf, nread); - more_junk_to_read = true; - } - } while (more_junk_to_read); - - if (m_input_error.has_value()) - return m_input_error.value(); - - fputs("\033[6n", stderr); - fflush(stderr); - - // Parse the DSR response - // it should be of the form .*\e[\d+;\d+R.* - // Anything not part of the response is just added to the incomplete data. - enum { - Free, - SawEsc, - SawBracket, - InFirstCoordinate, - SawSemicolon, - InSecondCoordinate, - SawR, - } state { Free }; - auto has_error = false; - Vector coordinate_buffer; - size_t row { 1 }, col { 1 }; - - do { - char c; - auto nread = read(0, &c, 1); - if (nread < 0) { - if (errno == 0 || errno == EINTR) { - // ???? - continue; - } - dbgln("Error while reading DSR: {}", strerror(errno)); - return Error::ReadFailure; - } - if (nread == 0) { - dbgln("Terminal DSR issue; received no response"); - return Error::Empty; - } - - switch (state) { - case Free: - if (c == '\x1b') { - state = SawEsc; - continue; - } - m_incomplete_data.append(c); - continue; - case SawEsc: - if (c == '[') { - state = SawBracket; - continue; - } - m_incomplete_data.append(c); - state = Free; - continue; - case SawBracket: - if (is_ascii_digit(c)) { - state = InFirstCoordinate; - coordinate_buffer.clear_with_capacity(); - coordinate_buffer.append(c); - continue; - } - m_incomplete_data.append(c); - state = Free; - continue; - case InFirstCoordinate: - if (is_ascii_digit(c)) { - coordinate_buffer.append(c); - continue; - } - if (c == ';') { - auto maybe_row = StringView { coordinate_buffer.data(), coordinate_buffer.size() }.to_number(); - if (!maybe_row.has_value()) - has_error = true; - row = maybe_row.value_or(1u); - coordinate_buffer.clear_with_capacity(); - state = SawSemicolon; - continue; - } - m_incomplete_data.append(c); - state = Free; - continue; - case SawSemicolon: - if (is_ascii_digit(c)) { - state = InSecondCoordinate; - coordinate_buffer.append(c); - continue; - } - m_incomplete_data.append(c); - state = Free; - continue; - case InSecondCoordinate: - if (is_ascii_digit(c)) { - coordinate_buffer.append(c); - continue; - } - if (c == 'R') { - auto maybe_column = StringView { coordinate_buffer.data(), coordinate_buffer.size() }.to_number(); - if (!maybe_column.has_value()) - has_error = true; - col = maybe_column.value_or(1u); - coordinate_buffer.clear_with_capacity(); - state = SawR; - continue; - } - m_incomplete_data.append(c); - state = Free; - continue; - case SawR: - m_incomplete_data.append(c); - continue; - default: - VERIFY_NOT_REACHED(); - } - } while (state != SawR); - - if (has_error) - dbgln("Terminal DSR issue, couldn't parse DSR response"); - return Vector { row, col }; -} - -ByteString Editor::line(size_t up_to_index) const -{ - StringBuilder builder; - builder.append(Utf32View { m_buffer.data(), min(m_buffer.size(), up_to_index) }); - return builder.to_byte_string(); -} - -void Editor::remove_at_index(size_t index) -{ - // See if we have any anchored styles, and reposition them if needed. - readjust_anchored_styles(index, ModificationKind::Removal); - auto cp = m_buffer[index]; - m_buffer.remove(index); - if (cp == '\n') - ++m_extra_forward_lines; - ++m_chars_touched_in_the_middle; -} - -void Editor::readjust_anchored_styles(size_t hint_index, ModificationKind modification) -{ - struct Anchor { - Span old_span; - Span new_span; - Style style; - }; - Vector anchors_to_relocate; - auto index_shift = modification == ModificationKind::Insertion ? 1 : -1; - auto forced_removal = modification == ModificationKind::ForcedOverlapRemoval; - - for (auto& start_entry : m_current_spans.m_anchored_spans_starting) { - for (auto& end_entry : start_entry.value) { - if (forced_removal) { - if (start_entry.key <= hint_index && end_entry.key > hint_index) { - // Remove any overlapping regions. - continue; - } - } - if (start_entry.key >= hint_index) { - if (start_entry.key == hint_index && end_entry.key == hint_index + 1 && modification == ModificationKind::Removal) { - // Remove the anchor, as all its text was wiped. - continue; - } - // Shift everything. - anchors_to_relocate.append({ { start_entry.key, end_entry.key, Span::Mode::CodepointOriented }, { start_entry.key + index_shift, end_entry.key + index_shift, Span::Mode::CodepointOriented }, end_entry.value }); - continue; - } - if (end_entry.key > hint_index) { - // Shift just the end. - anchors_to_relocate.append({ { start_entry.key, end_entry.key, Span::Mode::CodepointOriented }, { start_entry.key, end_entry.key + index_shift, Span::Mode::CodepointOriented }, end_entry.value }); - continue; - } - anchors_to_relocate.append({ { start_entry.key, end_entry.key, Span::Mode::CodepointOriented }, { start_entry.key, end_entry.key, Span::Mode::CodepointOriented }, end_entry.value }); - } - } - - m_current_spans.m_anchored_spans_ending.clear(); - m_current_spans.m_anchored_spans_starting.clear(); - // Pass over the relocations and update the stale entries. - for (auto& relocation : anchors_to_relocate) { - stylize(relocation.new_span, relocation.style); - } -} - -size_t StringMetrics::lines_with_addition(StringMetrics const& offset, size_t column_width) const -{ - size_t lines = 0; - - if (!line_metrics.is_empty()) { - for (size_t i = 0; i < line_metrics.size() - 1; ++i) - lines += (line_metrics[i].total_length() + column_width) / column_width; - - auto last = line_metrics.last().total_length(); - last += offset.line_metrics.first().total_length(); - lines += (last + column_width) / column_width; - } - - for (size_t i = 1; i < offset.line_metrics.size(); ++i) - lines += (offset.line_metrics[i].total_length() + column_width) / column_width; - - return lines; -} - -size_t StringMetrics::offset_with_addition(StringMetrics const& offset, size_t column_width) const -{ - if (offset.line_metrics.size() > 1) - return offset.line_metrics.last().total_length() % column_width; - - if (!line_metrics.is_empty()) { - auto last = line_metrics.last().total_length(); - last += offset.line_metrics.first().total_length(); - return last % column_width; - } - - if (offset.line_metrics.is_empty()) - return 0; - - return offset.line_metrics.first().total_length() % column_width; -} - -bool Editor::Spans::contains_up_to_offset(Spans const& other, size_t offset) const -{ - auto compare = [&](HashMap> const& left, HashMap> const& right) -> bool { - for (auto& entry : right) { - if (entry.key > offset + 1) - continue; - - auto left_map_it = left.find(entry.key); - if (left_map_it == left.end()) - return false; - - for (auto& left_entry : left_map_it->value) { - auto value_it = entry.value.find(left_entry.key); - if (value_it == entry.value.end()) { - // Might have the same thing with a longer span - bool found = false; - for (auto& possibly_longer_span_entry : entry.value) { - if (possibly_longer_span_entry.key > left_entry.key && possibly_longer_span_entry.key > offset && left_entry.value == possibly_longer_span_entry.value) { - found = true; - break; - } - } - if (found) - continue; - if constexpr (LINE_EDITOR_DEBUG) { - dbgln("Compare for {}-{} failed, no entry", entry.key, left_entry.key); - for (auto& x : entry.value) - dbgln("Have: {}-{} = {}", entry.key, x.key, x.value.to_byte_string()); - } - return false; - } else if (value_it->value != left_entry.value) { - dbgln_if(LINE_EDITOR_DEBUG, "Compare for {}-{} failed, different values: {} != {}", entry.key, left_entry.key, value_it->value.to_byte_string(), left_entry.value.to_byte_string()); - return false; - } - } - } - - return true; - }; - - return compare(m_spans_starting, other.m_spans_starting) - && compare(m_anchored_spans_starting, other.m_anchored_spans_starting); -} - -} diff --git a/Libraries/LibLine/Editor.h b/Libraries/LibLine/Editor.h deleted file mode 100644 index 3f96c48657..0000000000 --- a/Libraries/LibLine/Editor.h +++ /dev/null @@ -1,527 +0,0 @@ -/* - * Copyright (c) 2018-2020, Andreas Kling - * Copyright (c) 2021, the SerenityOS developers. - * - * SPDX-License-Identifier: BSD-2-Clause - */ - -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace Line { - -static constexpr u32 ctrl(char c) { return c & 0x3f; } - -struct KeyBinding { - Vector keys; - enum class Kind { - InternalFunction, - Insertion, - } kind { Kind::InternalFunction }; - ByteString binding; -}; - -struct Configuration { - enum RefreshBehavior { - Lazy, - Eager, - }; - enum OperationMode { - Unset, - Full, - NoEscapeSequences, - NonInteractive, - }; - enum SignalHandler { - WithSignalHandlers, - NoSignalHandlers, - }; - - enum Flags : u32 { - None = 0, - BracketedPaste = 1, - }; - - struct DefaultTextEditor { - ByteString command; - }; - - Configuration() - { - } - - template - Configuration(Arg arg, Rest... rest) - : Configuration(rest...) - { - set(arg); - } - - void set(RefreshBehavior refresh) { refresh_behavior = refresh; } - void set(OperationMode mode) { operation_mode = mode; } - void set(SignalHandler mode) { m_signal_mode = mode; } - void set(KeyBinding const& binding) { keybindings.append(binding); } - void set(DefaultTextEditor editor) { m_default_text_editor = move(editor.command); } - void set(Flags flags) - { - enable_bracketed_paste = flags & Flags::BracketedPaste; - } - - static Configuration from_config(StringView libname = "line"sv); - - RefreshBehavior refresh_behavior { RefreshBehavior::Lazy }; - SignalHandler m_signal_mode { SignalHandler::WithSignalHandlers }; - OperationMode operation_mode { OperationMode::Unset }; - Vector keybindings; - ByteString m_default_text_editor {}; - bool enable_bracketed_paste { false }; -}; - -#define ENUMERATE_EDITOR_INTERNAL_FUNCTIONS(M) \ - M(clear_screen) \ - M(cursor_left_character) \ - M(cursor_left_word) \ - M(cursor_left_nonspace_word) \ - M(cursor_right_character) \ - M(cursor_right_word) \ - M(cursor_right_nonspace_word) \ - M(enter_search) \ - M(search_character_backwards) \ - M(search_character_forwards) \ - 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(insert_last_erased) \ - M(erase_alnum_word_backwards) \ - M(erase_alnum_word_forwards) \ - M(erase_spaces) \ - M(capitalize_word) \ - M(lowercase_word) \ - M(uppercase_word) \ - M(edit_in_external_editor) - -#define EDITOR_INTERNAL_FUNCTION(name) \ - [](auto& editor) { editor.name(); return false; } - -class Editor : public Core::EventReceiver { - C_OBJECT(Editor); - -public: - enum class Error { - ReadFailure, - Empty, - Eof, - }; - - ~Editor(); - - Result get_line(ByteString const& prompt); - - void initialize(); - - void refetch_default_termios(); - - void add_to_history(ByteString const& line); - bool load_history(ByteString const& path); - bool save_history(ByteString const& path); - auto const& history() const { return m_history; } - bool is_history_dirty() const { return m_history_dirty; } - - void register_key_input_callback(KeyBinding const&); - void register_key_input_callback(Vector keys, Function callback) { m_callback_machine.register_key_input_callback(move(keys), move(callback)); } - void register_key_input_callback(Key key, Function callback) { register_key_input_callback(Vector { key }, move(callback)); } - - static StringMetrics actual_rendered_string_metrics(StringView, RedBlackTree> const& masks = {}, Optional maximum_line_width = {}); - static StringMetrics actual_rendered_string_metrics(Utf32View const&, RedBlackTree> const& masks = {}, Optional maximum_line_width = {}); - - Function(Editor const&)> on_tab_complete; - Function on_paste; - Function on_interrupt_handled; - Function on_display_refresh; - - static Function find_internal_function(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 - - ErrorOr interrupted(); - ErrorOr 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; - } - Vector const& buffer() const { return m_buffer; } - u32 buffer_at(size_t pos) const { return m_buffer.at(pos); } - ByteString line() const { return line(m_buffer.size()); } - ByteString line(size_t up_to_index) const; - - // Only makes sense inside a character_input callback or on_* callback. - void set_prompt(ByteString const& 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(ByteString const&); - void insert(StringView); - void insert(Utf8View&); - void insert(Utf32View const&); - void insert(u32 const); - void stylize(Span const&, Style const&); - 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, on_tab_complete returns "barx", "bary", "barz" - // ^ ^ - // +-|- static offset: the suggestions start here - // +- invariant offset: the suggestions do not change up to here - // - void transform_suggestion_offsets(size_t& invariant_offset, size_t& static_offset, 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; } - - Utf32View const buffer_view() const LIFETIME_BOUND { return { m_buffer.data(), m_buffer.size() }; } - - auto prohibit_input() - { - auto previous_value = m_prohibit_input_processing; - m_prohibit_input_processing = true; - m_have_unprocessed_read_event = false; - return ScopeGuard { - [this, previous_value] { - m_prohibit_input_processing = previous_value; - if (!m_prohibit_input_processing && m_have_unprocessed_read_event) - handle_read_event().release_value_but_fixme_should_propagate_errors(); - } - }; - } - -private: - explicit Editor(Configuration configuration = Configuration::from_config()); - - void set_default_keybinds(); - - enum LoopExitCode { - Exit = 0, - Retry - }; - - ErrorOr try_update_once(); - void handle_interrupt_event(); - ErrorOr handle_read_event(); - ErrorOr handle_resize_event(bool reset_origin); - - void ensure_free_lines_from_origin(size_t count); - - Result, Error> 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(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 = ByteString::empty(); - m_chars_touched_in_the_middle = 0; - m_drawn_end_of_line_offset = 0; - m_drawn_spans = {}; - m_paste_buffer.clear_with_capacity(); - } - - ErrorOr refresh_display(); - ErrorOr cleanup(); - ErrorOr cleanup_suggestions(); - ErrorOr really_quit_event_loop(); - - void restore() - { - VERIFY(m_initialized); - tcsetattr(0, TCSANOW, &m_default_termios); - m_initialized = false; - if (m_configuration.enable_bracketed_paste) - warn("\x1b[?2004l"); - for (auto id : m_signal_handlers) - Core::EventLoop::unregister_signal(id); - } - - StringMetrics const& 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_current_masks), - 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), m_current_masks); - return current_prompt_metrics().offset_with_addition(buffer_metrics, m_num_columns); - } - - bool set_origin(bool quit_on_error = true) - { - auto position = vt_dsr(); - if (!position.is_error()) { - set_origin(position.value()[0], position.value()[1]); - return true; - } - if (quit_on_error && position.is_error()) { - m_input_error = position.error(); - finish(); - } - return false; - } - - 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(); - ErrorOr reposition_cursor(Stream&, 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 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 m_pre_search_buffer; - - Vector m_buffer; - ByteBuffer m_pending_chars; - Vector m_incomplete_data; - Optional m_input_error; - ByteString m_returned_line; - - size_t m_cursor { 0 }; - size_t m_drawn_cursor { 0 }; - size_t m_drawn_end_of_line_offset { 0 }; - size_t m_inline_search_cursor { 0 }; - size_t m_chars_touched_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 }; - size_t m_shown_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 }; - bool m_expected_origin_changed { false }; - bool m_has_origin_reset_scheduled { false }; - - OwnPtr m_suggestion_display; - Vector m_remembered_suggestion_static_data; - - ByteString 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_previous_interrupt_was_handled_as_interrupt { true }; - bool m_was_resized { false }; - - // FIXME: This should be something more take_first()-friendly. - struct HistoryEntry { - ByteString entry; - time_t timestamp; - }; - Vector m_history; - size_t m_history_cursor { 0 }; - size_t m_history_capacity { 1024 }; - bool m_history_dirty { false }; - static ErrorOr> try_load_history(StringView path); - - enum class InputState { - Free, - Verbatim, - Paste, - GotEscape, - CSIExpectParameter, - CSIExpectIntermediate, - CSIExpectFinal, - }; - InputState m_state { InputState::Free }; - InputState m_previous_free_state { InputState::Free }; - - struct Spans { - HashMap> m_spans_starting; - HashMap> m_spans_ending; - HashMap> m_anchored_spans_starting; - HashMap> m_anchored_spans_ending; - - bool contains_up_to_offset(Spans const& other, size_t offset) const; - } m_drawn_spans, m_current_spans; - - RedBlackTree> m_current_masks; - - RefPtr m_notifier; - - Vector m_paste_buffer; - Vector m_last_erased; - - bool m_initialized { false }; - bool m_refresh_needed { false }; - Vector m_signal_handlers; - - bool m_is_editing { false }; - bool m_prohibit_input_processing { false }; - bool m_have_unprocessed_read_event { false }; - - Configuration m_configuration; -}; - -} diff --git a/Libraries/LibLine/InternalFunctions.cpp b/Libraries/LibLine/InternalFunctions.cpp deleted file mode 100644 index 16bd7059df..0000000000 --- a/Libraries/LibLine/InternalFunctions.cpp +++ /dev/null @@ -1,772 +0,0 @@ -/* - * Copyright (c) 2020, the SerenityOS developers. - * - * SPDX-License-Identifier: BSD-2-Clause - */ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace Line { - -Function Editor::find_internal_function(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 }); - auto search_phrase = builder.to_byte_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_chars_touched_in_the_middle = buffer().size(); - 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 }); - auto search_phrase = builder.to_byte_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() -{ - auto has_seen_alnum = false; - while (m_cursor) { - // after seeing at least one alnum, stop just before a non-alnum - if (not is_ascii_alphanumeric(m_buffer[m_cursor - 1])) { - if (has_seen_alnum) - break; - } else { - has_seen_alnum = true; - } - - --m_cursor; - } - m_inline_search_cursor = m_cursor; -} - -void Editor::cursor_left_nonspace_word() -{ - auto has_seen_nonspace = false; - while (m_cursor) { - // after seeing at least one non-space, stop just before a space - if (is_ascii_space(m_buffer[m_cursor - 1])) { - if (has_seen_nonspace) - break; - } else { - has_seen_nonspace = true; - } - - --m_cursor; - } - m_inline_search_cursor = m_cursor; -} - -void Editor::cursor_left_character() -{ - if (m_cursor > 0) { - size_t closest_cursor_left_offset; - binary_search(m_cached_buffer_metrics.grapheme_breaks, m_cursor - 1, &closest_cursor_left_offset); - m_cursor = m_cached_buffer_metrics.grapheme_breaks[closest_cursor_left_offset]; - } - m_inline_search_cursor = m_cursor; -} - -void Editor::cursor_right_word() -{ - auto has_seen_alnum = false; - while (m_cursor < m_buffer.size()) { - // after seeing at least one alnum, stop at the first non-alnum - if (not is_ascii_alphanumeric(m_buffer[m_cursor])) { - if (has_seen_alnum) - break; - } else { - has_seen_alnum = true; - } - - ++m_cursor; - } - m_inline_search_cursor = m_cursor; - m_search_offset = 0; -} - -void Editor::cursor_right_nonspace_word() -{ - auto has_seen_nonspace = false; - while (m_cursor < m_buffer.size()) { - // after seeing at least one non-space, stop at the first space - if (is_ascii_space(m_buffer[m_cursor])) { - if (has_seen_nonspace) - break; - } else { - has_seen_nonspace = true; - } - - ++m_cursor; - } - m_inline_search_cursor = m_cursor; - m_search_offset = 0; -} - -void Editor::cursor_right_character() -{ - if (m_cursor < m_buffer.size()) { - size_t closest_cursor_left_offset; - binary_search(m_cached_buffer_metrics.grapheme_breaks, m_cursor, &closest_cursor_left_offset); - m_cursor = closest_cursor_left_offset + 1 >= m_cached_buffer_metrics.grapheme_breaks.size() - ? m_buffer.size() - : m_cached_buffer_metrics.grapheme_breaks[closest_cursor_left_offset + 1]; - } - 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; - } - - size_t closest_cursor_left_offset; - binary_search(m_cached_buffer_metrics.grapheme_breaks, m_cursor - 1, &closest_cursor_left_offset); - auto start_of_previous_grapheme = m_cached_buffer_metrics.grapheme_breaks[closest_cursor_left_offset]; - for (; m_cursor > start_of_previous_grapheme; --m_cursor) - remove_at_index(m_cursor - 1); - - 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; - } - - size_t closest_cursor_left_offset; - binary_search(m_cached_buffer_metrics.grapheme_breaks, m_cursor, &closest_cursor_left_offset); - auto end_of_next_grapheme = closest_cursor_left_offset + 1 >= m_cached_buffer_metrics.grapheme_breaks.size() - ? m_buffer.size() - : m_cached_buffer_metrics.grapheme_breaks[closest_cursor_left_offset + 1]; - for (auto cursor = m_cursor; cursor < end_of_next_grapheme; ++cursor) - remove_at_index(m_cursor); - m_refresh_needed = true; -} - -void Editor::finish_edit() -{ - fprintf(stderr, "\n"); - if (!m_always_refresh) { - m_input_error = Error::Eof; - finish(); - really_quit_event_loop().release_value_but_fixme_should_propagate_errors(); - } -} - -void Editor::kill_line() -{ - if (m_cursor == 0) - return; - - m_last_erased.clear_with_capacity(); - - for (size_t i = 0; i < m_cursor; ++i) { - m_last_erased.append(m_buffer[0]); - remove_at_index(0); - } - m_cursor = 0; - m_inline_search_cursor = m_cursor; - m_refresh_needed = true; -} - -void Editor::erase_word_backwards() -{ - if (m_cursor == 0) - return; - - m_last_erased.clear_with_capacity(); - - // A word here is space-separated. `foo=bar baz` is two words. - bool has_seen_nonspace = false; - while (m_cursor > 0) { - if (is_ascii_space(m_buffer[m_cursor - 1])) { - if (has_seen_nonspace) - break; - } else { - has_seen_nonspace = true; - } - - m_last_erased.append(m_buffer[m_cursor - 1]); - erase_character_backwards(); - } - - m_last_erased.reverse(); -} - -void Editor::erase_to_end() -{ - if (m_cursor == m_buffer.size()) - return; - - m_last_erased.clear_with_capacity(); - - while (m_cursor < m_buffer.size()) { - m_last_erased.append(m_buffer[m_cursor]); - erase_character_forwards(); - } -} - -void Editor::erase_to_beginning() -{ -} - -void Editor::insert_last_erased() -{ - insert(Utf32View { m_last_erased.data(), m_last_erased.size() }); -} - -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; - m_chars_touched_in_the_middle += 2; - } -} - -void Editor::enter_search() -{ - if (m_is_searching) { - // How did we get here? - VERIFY_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; - - ensure_free_lines_from_origin(1 + num_lines()); - - // 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(); - - m_search_editor->on_display_refresh = [this](Editor& search_editor) { - // Remove the search editor prompt before updating ourselves (this avoids artifacts when we move the search editor around). - search_editor.cleanup().release_value_but_fixme_should_propagate_errors(); - - StringBuilder builder; - builder.append(Utf32View { search_editor.buffer().data(), search_editor.buffer().size() }); - if (!search(builder.to_byte_string(), false, false)) { - m_chars_touched_in_the_middle = m_buffer.size(); - m_refresh_needed = true; - m_buffer.clear(); - m_cursor = 0; - } - - refresh_display().release_value_but_fixme_should_propagate_errors(); - - // Move the search prompt below ours and tell it to redraw itself. - auto prompt_end_line = current_prompt_metrics().lines_with_addition(m_cached_buffer_metrics, m_num_columns); - search_editor.set_origin(prompt_end_line + m_origin_row, 1); - search_editor.m_refresh_needed = true; - }; - - // 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 - }); - - // ^C should cancel the search. - m_search_editor->register_key_input_callback(ctrl('C'), [this](Editor& search_editor) { - search_editor.finish(); - m_reset_buffer_on_search_end = true; - search_editor.end_search(); - search_editor.deferred_invoke([&search_editor] { search_editor.really_quit_event_loop().release_value_but_fixme_should_propagate_errors(); }); - return false; - }); - - // 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 - { - TemporaryChange refresh_change { m_always_refresh, true }; - set_origin(1, 1); - m_refresh_needed = true; - refresh_display().release_value_but_fixme_should_propagate_errors(); - } - - // move the search prompt below ours - // and tell it to redraw itself - auto prompt_end_line = current_prompt_metrics().lines_with_addition(m_cached_buffer_metrics, m_num_columns); - search_editor.set_origin(prompt_end_line + 1, 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; - }); - - auto search_prompt = "\x1b[32msearch:\x1b[0m "sv; - - // 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; - - 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. - auto stderr_stream = Core::File::standard_error().release_value_but_fixme_should_propagate_errors(); - reposition_cursor(*stderr_stream).release_value_but_fixme_should_propagate_errors(); - 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, *stderr_stream).release_value_but_fixme_should_propagate_errors(); - - reposition_cursor(*stderr_stream).release_value_but_fixme_should_propagate_errors(); - - m_refresh_needed = true; - m_cached_prompt_valid = false; - m_chars_touched_in_the_middle = 1; - - 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(); - } -} - -namespace { - -Optional read_unicode_char() -{ - // FIXME: It would be ideal to somehow communicate that the line editor is - // not operating in a normal mode and expects a character during the unicode - // read (cursor mode? change current cell? change prompt? Something else?) - StringBuilder builder; - - for (int i = 0; i < 4; ++i) { - char c = 0; - auto nread = read(0, &c, 1); - - if (nread <= 0) - return {}; - - builder.append(c); - - Utf8View search_char_utf8_view { builder.string_view() }; - - if (search_char_utf8_view.validate()) - return *search_char_utf8_view.begin(); - } - - return {}; -} - -} - -void Editor::search_character_forwards() -{ - auto optional_search_char = read_unicode_char(); - if (not optional_search_char.has_value()) - return; - u32 search_char = optional_search_char.value(); - - for (auto index = m_cursor + 1; index < m_buffer.size(); ++index) { - if (m_buffer[index] == search_char) { - m_cursor = index; - return; - } - } - - fputc('\a', stderr); - fflush(stderr); -} - -void Editor::search_character_backwards() -{ - auto optional_search_char = read_unicode_char(); - if (not optional_search_char.has_value()) - return; - u32 search_char = optional_search_char.value(); - - for (auto index = m_cursor; index > 0; --index) { - if (m_buffer[index - 1] == search_char) { - m_cursor = index - 1; - return; - } - } - - fputc('\a', stderr); - fflush(stderr); -} - -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() && !is_ascii_alphanumeric(m_buffer[cursor])) - ++cursor; - while (cursor < m_buffer.size() && is_ascii_alphanumeric(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 && !is_ascii_alphanumeric(m_buffer[start - 1])) - --start; - while (start > 0 && is_ascii_alphanumeric(m_buffer[start - 1])) - --start; - size_t start_second_word = start; - - // Move left over space between the two words. - while (start > 0 && !is_ascii_alphanumeric(m_buffer[start - 1])) - --start; - size_t start_gap = start; - - // Move left over first word. - while (start > 0 && is_ascii_alphanumeric(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; - m_chars_touched_in_the_middle += end - start; - } -} - -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() -{ - warn("\033[3J\033[H\033[2J"); - auto stream = Core::File::standard_error().release_value_but_fixme_should_propagate_errors(); - VT::move_absolute(1, 1, *stream).release_value_but_fixme_should_propagate_errors(); - set_origin(1, 1); - m_refresh_needed = true; - m_cached_prompt_valid = false; -} - -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() -{ - if (m_cursor == 0) - return; - - m_last_erased.clear_with_capacity(); - - // A word here is contiguous alnums. `foo=bar baz` is three words. - bool has_seen_alnum = false; - while (m_cursor > 0) { - if (!is_ascii_alphanumeric(m_buffer[m_cursor - 1])) { - if (has_seen_alnum) - break; - } else { - has_seen_alnum = true; - } - - m_last_erased.append(m_buffer[m_cursor - 1]); - erase_character_backwards(); - } - - m_last_erased.reverse(); -} - -void Editor::erase_alnum_word_forwards() -{ - if (m_cursor == m_buffer.size()) - return; - - m_last_erased.clear_with_capacity(); - - // A word here is contiguous alnums. `foo=bar baz` is three words. - bool has_seen_alnum = false; - while (m_cursor < m_buffer.size()) { - if (!is_ascii_alphanumeric(m_buffer[m_cursor])) { - if (has_seen_alnum) - break; - } else { - has_seen_alnum = true; - } - - m_last_erased.append(m_buffer[m_cursor]); - erase_character_forwards(); - } -} - -void Editor::erase_spaces() -{ - while (m_cursor < m_buffer.size()) { - if (is_ascii_space(m_buffer[m_cursor])) - erase_character_forwards(); - else - break; - } - - while (m_cursor > 0) { - if (is_ascii_space(m_buffer[m_cursor - 1])) - erase_character_backwards(); - else - break; - } -} - -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() && !is_ascii_alphanumeric(m_buffer[m_cursor])) - ++m_cursor; - size_t start = m_cursor; - while (m_cursor < m_buffer.size() && is_ascii_alphanumeric(m_buffer[m_cursor])) { - if (change_op == CaseChangeOp::Uppercase || (change_op == CaseChangeOp::Capital && m_cursor == start)) { - m_buffer[m_cursor] = to_ascii_uppercase(m_buffer[m_cursor]); - } else { - VERIFY(change_op == CaseChangeOp::Lowercase || (change_op == CaseChangeOp::Capital && m_cursor > start)); - m_buffer[m_cursor] = to_ascii_lowercase(m_buffer[m_cursor]); - } - ++m_cursor; - } - - m_refresh_needed = true; - m_chars_touched_in_the_middle = 1; -} - -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); -} - -void Editor::edit_in_external_editor() -{ - auto const* editor_command = getenv("EDITOR"); - if (!editor_command) - editor_command = m_configuration.m_default_text_editor.characters(); - - char file_path[] = "/tmp/line-XXXXXX"; - auto fd = mkstemp(file_path); - - if (fd < 0) { - perror("mktemp"); - return; - } - - { - auto write_fd = dup(fd); - auto stream = Core::File::adopt_fd(write_fd, Core::File::OpenMode::Write).release_value_but_fixme_should_propagate_errors(); - StringBuilder builder; - builder.append(Utf32View { m_buffer.data(), m_buffer.size() }); - auto bytes = builder.string_view().bytes(); - while (!bytes.is_empty()) { - auto nwritten = stream->write_some(bytes).release_value_but_fixme_should_propagate_errors(); - bytes = bytes.slice(nwritten); - } - lseek(fd, 0, SEEK_SET); - } - - ScopeGuard remove_temp_file_guard { - [fd, file_path] { - close(fd); - unlink(file_path); - } - }; - - Vector args { editor_command, file_path, nullptr }; - auto pid = fork(); - - if (pid == -1) { - perror("fork"); - return; - } - - if (pid == 0) { - execvp(editor_command, const_cast(args.data())); - perror("execv"); - _exit(126); - } else { - int wstatus = 0; - do { - waitpid(pid, &wstatus, 0); - } while (errno == EINTR); - - if (!(WIFEXITED(wstatus) && WEXITSTATUS(wstatus) == 0)) - return; - } - - { - auto file = Core::File::open({ file_path, strlen(file_path) }, Core::File::OpenMode::Read).release_value_but_fixme_should_propagate_errors(); - auto contents = file->read_until_eof().release_value_but_fixme_should_propagate_errors(); - StringView data { contents }; - while (data.ends_with('\n')) - data = data.substring_view(0, data.length() - 1); - - m_cursor = 0; - m_chars_touched_in_the_middle = m_buffer.size(); - m_buffer.clear_with_capacity(); - m_refresh_needed = true; - - Utf8View view { data }; - if (view.validate()) { - for (auto cp : view) - insert(cp); - } else { - for (auto ch : data) - insert(ch); - } - } -} - -} diff --git a/Libraries/LibLine/KeyCallbackMachine.cpp b/Libraries/LibLine/KeyCallbackMachine.cpp deleted file mode 100644 index b76cf401d5..0000000000 --- a/Libraries/LibLine/KeyCallbackMachine.cpp +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright (c) 2020, the SerenityOS developers. - * - * SPDX-License-Identifier: BSD-2-Clause - */ - -#include -#include - -namespace Line { - -void KeyCallbackMachine::register_key_input_callback(Vector keys, Function callback) -{ - m_key_callbacks.set(keys, make(move(callback))); -} - -void KeyCallbackMachine::key_pressed(Editor& editor, Key key) -{ - dbgln_if(CALLBACK_MACHINE_DEBUG, "Key<{}, {}> pressed, seq_length={}, {} things in the matching vector", key.key, key.modifiers, m_sequence_length, m_current_matching_keys.size()); - if (m_sequence_length == 0) { - VERIFY(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> old_matching_keys; - swap(m_current_matching_keys, old_matching_keys); - - for (auto& okey : old_matching_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_matching_keys.is_empty()) { - auto& keys = old_matching_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; - } - - if constexpr (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(""); - } - } - - 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; -} - -} diff --git a/Libraries/LibLine/KeyCallbackMachine.h b/Libraries/LibLine/KeyCallbackMachine.h deleted file mode 100644 index fbc9a55807..0000000000 --- a/Libraries/LibLine/KeyCallbackMachine.h +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright (c) 2020, the SerenityOS developers. - * - * SPDX-License-Identifier: BSD-2-Clause - */ - -#pragma once - -#include -#include -#include - -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==(Key const& other) const - { - return other.key == key && other.modifiers == modifiers; - } -}; - -struct KeyCallback { - KeyCallback(Function cb) - : callback(move(cb)) - { - } - Function callback; -}; - -class KeyCallbackMachine { -public: - void register_key_input_callback(Vector, Function callback); - void key_pressed(Editor&, Key); - void interrupted(Editor&); - bool should_process_last_pressed_key() const { return m_should_process_this_key; } - -private: - HashMap, NonnullOwnPtr> m_key_callbacks; - Vector> m_current_matching_keys; - size_t m_sequence_length { 0 }; - bool m_should_process_this_key { true }; -}; - -} - -namespace AK { - -template<> -struct Traits : public DefaultTraits { - static constexpr bool is_trivial() { return true; } - static unsigned hash(Line::Key k) { return pair_int_hash(k.key, k.modifiers); } -}; - -template<> -struct Traits> : public DefaultTraits> { - static constexpr bool is_trivial() { return false; } - static unsigned hash(Vector const& ks) - { - unsigned h = 0; - for (auto& k : ks) - h ^= Traits::hash(k); - return h; - } -}; - -} diff --git a/Libraries/LibLine/Span.h b/Libraries/LibLine/Span.h deleted file mode 100644 index c497926859..0000000000 --- a/Libraries/LibLine/Span.h +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (c) 2020, the SerenityOS developers. - * - * SPDX-License-Identifier: BSD-2-Clause - */ - -#pragma once - -#include - -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; } - - bool is_empty() const - { - return m_beginning < m_end; - } - -private: - size_t m_beginning { 0 }; - size_t m_end { 0 }; - Mode m_mode { CodepointOriented }; -}; - -} diff --git a/Libraries/LibLine/StringMetrics.h b/Libraries/LibLine/StringMetrics.h deleted file mode 100644 index a09a46b7a3..0000000000 --- a/Libraries/LibLine/StringMetrics.h +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (c) 2020-2021, the SerenityOS developers. - * - * SPDX-License-Identifier: BSD-2-Clause - */ - -#pragma once - -#include -#include - -namespace Line { - -struct StringMetrics { - struct MaskedChar { - size_t position { 0 }; - size_t original_length { 0 }; - size_t masked_length { 0 }; - }; - struct LineMetrics { - Vector masked_chars; - size_t length { 0 }; - size_t visible_length { 0 }; - Optional bit_length { 0 }; - - size_t total_length() const { return length; } - }; - - Vector line_metrics; - Vector grapheme_breaks {}; - size_t total_length { 0 }; - size_t max_line_length { 0 }; - - size_t lines_with_addition(StringMetrics const& offset, size_t column_width) const; - size_t offset_with_addition(StringMetrics const& offset, size_t column_width) const; - void reset() - { - line_metrics.clear(); - total_length = 0; - max_line_length = 0; - line_metrics.append({ {}, 0 }); - } -}; - -} diff --git a/Libraries/LibLine/Style.h b/Libraries/LibLine/Style.h deleted file mode 100644 index 8fc5c5edad..0000000000 --- a/Libraries/LibLine/Style.h +++ /dev/null @@ -1,198 +0,0 @@ -/* - * Copyright (c) 2020-2022, the SerenityOS developers. - * - * SPDX-License-Identifier: BSD-2-Clause - */ - -#pragma once - -#include -#include -#include -#include - -namespace Line { - -class Style { -public: - bool operator==(Style const&) const = default; - - 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 { - bool operator==(Color const&) const = default; - - 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 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) - { - } - ByteString 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) - { - } - - ByteString to_vt_escape() const; - }; - - struct Hyperlink { - bool operator==(Hyperlink const&) const = default; - - explicit Hyperlink(StringView link) - : m_link(link) - { - m_has_link = true; - } - - Hyperlink() = default; - - ByteString to_vt_escape(bool starting) const; - - bool is_empty() const { return !m_has_link; } - - ByteString m_link; - bool m_has_link { false }; - }; - - struct Mask { - bool operator==(Mask const& other) const - { - return other.mode == mode && other.replacement == replacement; - } - - enum class Mode { - ReplaceEntireSelection, - ReplaceEachCodePointInSelection, - }; - explicit Mask(StringView replacement, Mode mode = Mode::ReplaceEntireSelection) - : replacement(replacement) - , replacement_view(this->replacement) - , mode(mode) - { - } - - ByteString replacement; - mutable Utf8View replacement_view; - Mode mode; - }; - - static constexpr UnderlineTag Underline {}; - static constexpr BoldTag Bold {}; - static constexpr ItalicTag Italic {}; - static constexpr AnchoredTag Anchored {}; - - // Prepare for the horror of templates. - template - Style(T const& style_arg, Rest... rest) - : Style(rest...) - { - set(style_arg); - m_is_empty = false; - } - Style() = default; - - static Style reset_style() - { - return { Foreground(XtermColor::Default), Background(XtermColor::Default), Hyperlink(""sv) }; - } - - Style unified_with(Style const& other, bool prefer_other = true) const - { - Style style = *this; - style.unify_with(other, prefer_other); - return style; - } - - void unify_with(Style const&, 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; } - Optional mask() const { return m_mask; } - - void unset_mask() const { m_mask = {}; } - - void set(ItalicTag const&) { m_italic = true; } - void set(BoldTag const&) { m_bold = true; } - void set(UnderlineTag const&) { m_underline = true; } - void set(Background const& bg) { m_background = bg; } - void set(Foreground const& fg) { m_foreground = fg; } - void set(Hyperlink const& link) { m_hyperlink = link; } - void set(AnchoredTag const&) { m_is_anchored = true; } - void set(Mask const& mask) { m_mask = mask; } - - bool is_anchored() const { return m_is_anchored; } - bool is_empty() const { return m_is_empty; } - - ByteString to_byte_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; - mutable Optional m_mask; - - bool m_is_anchored { false }; - - bool m_is_empty { true }; -}; - -} diff --git a/Libraries/LibLine/SuggestionDisplay.h b/Libraries/LibLine/SuggestionDisplay.h deleted file mode 100644 index e83a740522..0000000000 --- a/Libraries/LibLine/SuggestionDisplay.h +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright (c) 2020-2022, the SerenityOS developers. - * - * SPDX-License-Identifier: BSD-2-Clause - */ - -#pragma once - -#include -#include -#include - -namespace Line { - -class Editor; - -class SuggestionDisplay { -public: - virtual ~SuggestionDisplay() = default; - virtual ErrorOr display(SuggestionManager const&) = 0; - virtual ErrorOr cleanup() = 0; - virtual void finish() = 0; - virtual void set_initial_prompt_lines(size_t) = 0; - - ErrorOr redisplay(SuggestionManager const& manager, size_t lines, size_t columns) - { - if (m_is_showing_suggestions) { - TRY(cleanup()); - set_vt_size(lines, columns); - TRY(display(manager)); - } else { - set_vt_size(lines, columns); - } - - return {}; - } - - 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) - { - m_origin_row = row; - m_origin_column = col; - } - -protected: - void did_display() { m_is_showing_suggestions = true; } - void did_cleanup() { m_is_showing_suggestions = false; } - - int m_origin_row { 0 }; - int m_origin_column { 0 }; - bool m_is_showing_suggestions { false }; -}; - -class XtermSuggestionDisplay : public SuggestionDisplay { -public: - XtermSuggestionDisplay(size_t lines, size_t columns) - : m_num_lines(lines) - , m_num_columns(columns) - { - } - virtual ~XtermSuggestionDisplay() override = default; - virtual ErrorOr display(SuggestionManager const&) override; - virtual ErrorOr 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 m_pages; -}; - -} diff --git a/Libraries/LibLine/SuggestionManager.cpp b/Libraries/LibLine/SuggestionManager.cpp deleted file mode 100644 index 1baabe8f35..0000000000 --- a/Libraries/LibLine/SuggestionManager.cpp +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Copyright (c) 2020, the SerenityOS developers. - * - * SPDX-License-Identifier: BSD-2-Clause - */ - -#include -#include -#include - -namespace Line { - -CompletionSuggestion::CompletionSuggestion(StringView completion, StringView trailing_trivia, StringView display_trivia, Style style) - : text(MUST(String::from_utf8(completion))) - , trailing_trivia(MUST(String::from_utf8(trailing_trivia))) - , display_trivia(MUST(String::from_utf8(display_trivia))) - , style(style) - , is_valid(true) -{ -} - -void SuggestionManager::set_suggestions(Vector&& suggestions) -{ - auto code_point_at = [](Utf8View view, size_t index) { - size_t count = 0; - for (auto cp : view) { - if (count == index) { - return cp; - } - count++; - } - VERIFY_NOT_REACHED(); - }; - - 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_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 = code_point_at(m_suggestions[0].text_view(), common_suggestion_prefix); - - for (auto& suggestion : m_suggestions) { - if (suggestion.text_view().length() <= common_suggestion_prefix || code_point_at(suggestion.text_view(), 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--; -} - -CompletionSuggestion const& SuggestionManager::suggest() -{ - auto const& suggestion = m_suggestions[m_next_suggestion_index]; - m_selected_suggestion_index = m_next_suggestion_index; - m_last_shown_suggestion = suggestion; - return suggestion; -} - -void SuggestionManager::set_current_suggestion_initiation_index(size_t index) -{ - auto& suggestion = m_suggestions[m_next_suggestion_index]; - - if (m_last_shown_suggestion_display_length) - m_last_shown_suggestion.start_index = index - suggestion.static_offset - m_last_shown_suggestion_display_length; - else - m_last_shown_suggestion.start_index = index - suggestion.static_offset - 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& next_suggestion = m_suggestions[m_next_suggestion_index]; - - if (mode == CompletePrefix && !next_suggestion.allow_commit_without_listing) { - result.new_completion_mode = CompletionMode::ShowSuggestions; - result.avoid_committing_to_single_suggestion = true; - m_last_shown_suggestion_display_length = 0; - m_last_shown_suggestion_was_complete = false; - m_last_shown_suggestion = ByteString::empty(); - return result; - } - - auto can_complete = next_suggestion.invariant_offset <= m_largest_common_suggestion_prefix_length; - 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 + next_suggestion.invariant_offset; - if (can_complete && next_suggestion.allow_commit_without_listing) - 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 + next_suggestion.invariant_offset; - break; - } - - auto& suggestion = suggest(); - set_current_suggestion_initiation_index(initiation_start_index); - - result.offset_region_to_remove = { next_suggestion.invariant_offset, shown_length }; - result.new_cursor_offset = actual_offset; - result.static_offset_from_cursor = next_suggestion.static_offset; - - if (mode == CompletePrefix) { - // Only auto-complete *if possible*. - if (can_complete) { - result.insert.append(suggestion.text_view().unicode_substring_view(suggestion.invariant_offset, m_largest_common_suggestion_prefix_length - 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.trailing_trivia.code_points()); - 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 = ByteString::empty(); - } else { - result.insert.append(suggestion.text_view().unicode_substring_view(suggestion.invariant_offset, suggestion.text_view().length() - suggestion.invariant_offset)); - // Add in the trivia of the last selected suggestion. - result.insert.append(suggestion.trailing_trivia.code_points()); - m_last_shown_suggestion_display_length += suggestion.trivia_view().length(); - } - } else { - m_next_suggestion_index = 0; - } - return result; -} - -ErrorOr SuggestionManager::for_each_suggestion(Function(CompletionSuggestion const&, size_t)> callback) const -{ - size_t start_index { 0 }; - for (auto& suggestion : m_suggestions) { - if (start_index++ < m_last_displayed_suggestion_index) - continue; - if (TRY(callback(suggestion, start_index - 1)) == IterationDecision::Break) - break; - } - return start_index; -} - -} diff --git a/Libraries/LibLine/SuggestionManager.h b/Libraries/LibLine/SuggestionManager.h deleted file mode 100644 index e3341a2117..0000000000 --- a/Libraries/LibLine/SuggestionManager.h +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright (c) 2020, the SerenityOS developers. - * - * SPDX-License-Identifier: BSD-2-Clause - */ - -#pragma once - -#include -#include -#include -#include -#include -#include - -namespace Line { - -struct CompletionSuggestion { -private: - struct ForSearchTag { - }; - -public: - static constexpr ForSearchTag ForSearch {}; - - // Intentionally not explicit. (To allow suggesting bare strings) - CompletionSuggestion(ByteString const& completion) - : CompletionSuggestion(completion, ""sv, {}) - { - } - - CompletionSuggestion(StringView completion, ForSearchTag) - : text(MUST(String::from_utf8(completion))) - { - } - - CompletionSuggestion(StringView completion, StringView trailing_trivia, StringView display_trivia = ""sv) - : CompletionSuggestion(completion, trailing_trivia, display_trivia, {}) - { - } - - CompletionSuggestion(StringView completion, StringView trailing_trivia, StringView display_trivia, Style style); - - bool operator==(CompletionSuggestion const& suggestion) const - { - return suggestion.text == text; - } - - String text; - String trailing_trivia; - String display_trivia; - Style style; - size_t start_index { 0 }; - size_t input_offset { 0 }; - size_t static_offset { 0 }; - size_t invariant_offset { 0 }; - bool allow_commit_without_listing { true }; - - Utf8View text_view() const LIFETIME_BOUND { return text.code_points(); } - Utf8View trivia_view() const LIFETIME_BOUND { return trailing_trivia.code_points(); } - Utf8View display_trivia_view() const LIFETIME_BOUND { return display_trivia.code_points(); } - StringView text_string() const LIFETIME_BOUND { return text.bytes_as_string_view(); } - StringView display_trivia_string() const LIFETIME_BOUND { return display_trivia.bytes_as_string_view(); } - bool is_valid { false }; -}; - -class SuggestionManager { - friend class Editor; - -public: - void set_suggestions(Vector&& 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; } - - ErrorOr for_each_suggestion(Function(CompletionSuggestion const&, 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) - - // This bit of data will be removed, but restored if the suggestion is rejected. - size_t static_offset_from_cursor { 0 }; - - Vector insert {}; - - Optional