From 70a213a6ec19aeee99af6a747c5b12890c038690 Mon Sep 17 00:00:00 2001 From: AnotherTest Date: Tue, 26 May 2020 15:04:39 +0430 Subject: LibLine: Use Core::EventLoop for outer read loop This commit changes LibLine's internal structure to work in an event loop, and as a result, also switches it to being a Core::Object. --- Applications/Debugger/main.cpp | 14 +- Libraries/LibLine/Editor.cpp | 932 ++++++++++++++++++++++------------------- Libraries/LibLine/Editor.h | 17 +- Shell/Shell.cpp | 18 +- Shell/main.cpp | 26 +- Userland/js.cpp | 4 +- Userland/test-crypto.cpp | 5 +- 7 files changed, 541 insertions(+), 475 deletions(-) diff --git a/Applications/Debugger/main.cpp b/Applications/Debugger/main.cpp index 61817c8839..31f47046a2 100644 --- a/Applications/Debugger/main.cpp +++ b/Applications/Debugger/main.cpp @@ -44,7 +44,7 @@ #include #include -static Line::Editor editor {}; +RefPtr editor; OwnPtr g_debug_session; @@ -173,6 +173,8 @@ void print_help() int main(int argc, char** argv) { + editor = Line::Editor::construct(); + if (pledge("stdio proc exec rpath tty sigaction", nullptr) < 0) { perror("pledge"); return 1; @@ -236,7 +238,7 @@ int main(int argc, char** argv) } for (;;) { - auto command_result = editor.get_line("(sdb) "); + auto command_result = editor->get_line("(sdb) "); if (command_result.is_error()) return DebugSession::DebugDecision::Detach; @@ -246,8 +248,8 @@ int main(int argc, char** argv) bool success = false; Optional decision; - if (command.is_empty() && !editor.history().is_empty()) { - command = editor.history().last(); + if (command.is_empty() && !editor->history().is_empty()) { + command = editor->history().last(); } if (command == "cont") { decision = DebugSession::DebugDecision::Continue; @@ -276,8 +278,8 @@ int main(int argc, char** argv) if (success && !command.is_empty()) { // Don't add repeated commands to history - if (editor.history().is_empty() || editor.history().last() != command) - editor.add_to_history(command); + if (editor->history().is_empty() || editor->history().last() != command) + editor->add_to_history(command); } if (!success) { print_help(); diff --git a/Libraries/LibLine/Editor.cpp b/Libraries/LibLine/Editor.cpp index 2b38e7fa11..4952f9a3da 100644 --- a/Libraries/LibLine/Editor.cpp +++ b/Libraries/LibLine/Editor.cpp @@ -25,9 +25,13 @@ */ #include "Editor.h" +#include #include #include #include +#include +#include +#include #include #include #include @@ -252,7 +256,7 @@ void Editor::initialize() m_initialized = true; } -Result Editor::get_line(const String& prompt) +auto Editor::get_line(const String& prompt) -> Result { initialize(); m_is_editing = true; @@ -279,13 +283,22 @@ Result Editor::get_line(const String& prompt) strip_styles(true); m_history_cursor = m_history.size(); - for (;;) { + + refresh_display(); + + Core::EventLoop loop; + + m_notifier = Core::Notifier::construct(STDIN_FILENO, Core::Notifier::Read); + add_child(*m_notifier); + + m_notifier->on_ready_to_read = [&] { + handle_read_event(); + if (m_always_refresh) m_refresh_needed = true; + refresh_display(); - if (m_input_error.has_value()) { - return m_input_error.value(); - } + if (m_finish) { m_finish = false; printf("\n"); @@ -294,533 +307,567 @@ Result Editor::get_line(const String& prompt) m_buffer.clear(); m_is_editing = false; restore(); - return string; - } - char keybuf[16]; - ssize_t nread = 0; - if (!m_incomplete_data.size()) - nread = read(0, keybuf, sizeof(keybuf)); + m_returned_line = string; - if (nread < 0) { - if (errno == EINTR) { - if (!m_was_interrupted) { - if (m_was_resized) - continue; + m_notifier->set_event_mask(Core::Notifier::None); + deferred_invoke([this](auto&) { + remove_child(*m_notifier); + m_notifier = nullptr; + Core::EventLoop::current().quit(0); + }); + } + }; - finish(); - continue; - } + loop.exec(); - m_was_interrupted = false; + return m_input_error.has_value() ? Result { m_input_error.value() } : Result { m_returned_line }; +} - if (!m_buffer.is_empty()) - printf("^C"); +void Editor::handle_read_event() +{ + char keybuf[16]; + ssize_t nread = 0; - m_buffer.clear(); - m_cursor = 0; + if (!m_incomplete_data.size()) + nread = read(0, keybuf, sizeof(keybuf)); - if (on_interrupt_handled) - on_interrupt_handled(); + if (nread < 0) { + if (errno == EINTR) { + if (!m_was_interrupted) { + if (m_was_resized) + return; - m_refresh_needed = true; - continue; + finish(); + return; } - ScopedValueRollback errno_restorer(errno); - perror("read failed"); + m_was_interrupted = false; - return Error::ReadFailure; + if (!m_buffer.is_empty()) + printf("^C"); + + m_buffer.clear(); + m_cursor = 0; + + if (on_interrupt_handled) + on_interrupt_handled(); + + m_refresh_needed = true; + return; } - m_incomplete_data.append(keybuf, nread); - nread = m_incomplete_data.size(); + ScopedValueRollback errno_restorer(errno); + perror("read failed"); - if (nread == 0) - return Error::Empty; + m_input_error = Error::ReadFailure; + finish(); + return; + } - auto reverse_tab = false; - auto ctrl_held = false; + m_incomplete_data.append(keybuf, nread); + nread = m_incomplete_data.size(); - // Discard starting bytes until they make sense as utf-8. - size_t valid_bytes = 0; - while (nread) { - Utf8View { StringView { m_incomplete_data.data(), (size_t)nread } }.validate(valid_bytes); - if (valid_bytes) - break; - m_incomplete_data.take_first(); - --nread; - } + if (nread == 0) { + m_input_error = Error::Empty; + finish(); + return; + } - Utf8View input_view { StringView { m_incomplete_data.data(), valid_bytes } }; - size_t consumed_codepoints = 0; + auto reverse_tab = false; + auto ctrl_held = false; - for (auto codepoint : input_view) { - if (m_finish) - break; + // Discard starting bytes until they make sense as utf-8. + size_t valid_bytes = 0; + while (nread) { + Utf8View { StringView { m_incomplete_data.data(), (size_t)nread } }.validate(valid_bytes); + if (valid_bytes) + break; + m_incomplete_data.take_first(); + --nread; + } - ++consumed_codepoints; + Utf8View input_view { StringView { m_incomplete_data.data(), valid_bytes } }; + size_t consumed_codepoints = 0; - if (codepoint == 0) - continue; + for (auto codepoint : input_view) { + if (m_finish) + break; - switch (m_state) { - case InputState::ExpectBracket: - if (codepoint == '[') { - m_state = InputState::ExpectFinal; - continue; + ++consumed_codepoints; + + if (codepoint == 0) + continue; + + switch (m_state) { + case InputState::ExpectBracket: + if (codepoint == '[') { + m_state = InputState::ExpectFinal; + continue; + } else { + m_state = InputState::Free; + break; + } + case InputState::ExpectFinal: + switch (codepoint) { + case 'O': // mod_ctrl + ctrl_held = true; + continue; + case 'A': // up + { + m_searching_backwards = true; + auto inline_search_cursor = m_inline_search_cursor; + StringBuilder builder; + builder.append(Utf32View { m_buffer.data(), inline_search_cursor }); + String search_phrase = builder.to_string(); + if (search(search_phrase, true, true)) { + ++m_search_offset; } else { - m_state = InputState::Free; - break; + insert(search_phrase); } - case InputState::ExpectFinal: - switch (codepoint) { - case 'O': // mod_ctrl - ctrl_held = true; - continue; - case 'A': // up - { - m_searching_backwards = true; - auto inline_search_cursor = m_inline_search_cursor; - StringBuilder builder; - builder.append(Utf32View { m_buffer.data(), inline_search_cursor }); - String search_phrase = builder.to_string(); - if (search(search_phrase, true, true)) { - ++m_search_offset; - } else { + m_inline_search_cursor = inline_search_cursor; + m_state = InputState::Free; + ctrl_held = false; + continue; + } + case 'B': // down + { + auto inline_search_cursor = m_inline_search_cursor; + StringBuilder builder; + builder.append(Utf32View { m_buffer.data(), inline_search_cursor }); + String search_phrase = builder.to_string(); + auto search_changed_directions = m_searching_backwards; + m_searching_backwards = false; + if (m_search_offset > 0) { + m_search_offset -= 1 + search_changed_directions; + if (!search(search_phrase, true, true)) { insert(search_phrase); } - m_inline_search_cursor = inline_search_cursor; - m_state = InputState::Free; - ctrl_held = false; - continue; + } else { + m_search_offset = 0; + m_cursor = 0; + m_buffer.clear(); + insert(search_phrase); + m_refresh_needed = true; } - case 'B': // down - { - auto inline_search_cursor = m_inline_search_cursor; - StringBuilder builder; - builder.append(Utf32View { m_buffer.data(), inline_search_cursor }); - String search_phrase = builder.to_string(); - auto search_changed_directions = m_searching_backwards; - m_searching_backwards = false; - if (m_search_offset > 0) { - m_search_offset -= 1 + search_changed_directions; - if (!search(search_phrase, true, true)) { - insert(search_phrase); + m_inline_search_cursor = inline_search_cursor; + m_state = InputState::Free; + ctrl_held = false; + continue; + } + case 'D': // left + if (m_cursor > 0) { + if (ctrl_held) { + auto skipped_at_least_one_character = false; + for (;;) { + if (m_cursor == 0) + break; + if (skipped_at_least_one_character && isspace(m_buffer[m_cursor - 1])) // stop *after* a space, but only if it changes the position + break; + skipped_at_least_one_character = true; + --m_cursor; } } else { - m_search_offset = 0; - m_cursor = 0; - m_buffer.clear(); - insert(search_phrase); - m_refresh_needed = true; + --m_cursor; } - m_inline_search_cursor = inline_search_cursor; - m_state = InputState::Free; - ctrl_held = false; - continue; } - case 'D': // left - if (m_cursor > 0) { - if (ctrl_held) { - auto skipped_at_least_one_character = false; - for (;;) { - if (m_cursor == 0) - break; - if (skipped_at_least_one_character && isspace(m_buffer[m_cursor - 1])) // stop *after* a space, but only if it changes the position - break; - skipped_at_least_one_character = true; - --m_cursor; - } - } else { - --m_cursor; - } - } - m_inline_search_cursor = m_cursor; - m_state = InputState::Free; - ctrl_held = false; - continue; - case 'C': // right - if (m_cursor < m_buffer.size()) { - if (ctrl_held) { - // Temporarily put a space at the end of our buffer, - // doing this greatly simplifies the logic below. - m_buffer.append(' '); - for (;;) { - if (m_cursor >= m_buffer.size()) - break; - if (isspace(m_buffer[++m_cursor])) - break; - } - m_buffer.take_last(); - } else { - ++m_cursor; + m_inline_search_cursor = m_cursor; + m_state = InputState::Free; + ctrl_held = false; + continue; + case 'C': // right + if (m_cursor < m_buffer.size()) { + if (ctrl_held) { + // Temporarily put a space at the end of our buffer, + // doing this greatly simplifies the logic below. + m_buffer.append(' '); + for (;;) { + if (m_cursor >= m_buffer.size()) + break; + if (isspace(m_buffer[++m_cursor])) + break; } + m_buffer.take_last(); + } else { + ++m_cursor; } - m_inline_search_cursor = m_cursor; - m_search_offset = 0; - m_state = InputState::Free; - ctrl_held = false; - continue; - case 'H': - m_cursor = 0; - m_inline_search_cursor = m_cursor; - m_search_offset = 0; - m_state = InputState::Free; - ctrl_held = false; - continue; - case 'F': - m_cursor = m_buffer.size(); - m_state = InputState::Free; - m_inline_search_cursor = m_cursor; - m_search_offset = 0; - ctrl_held = false; - continue; - case 'Z': // shift+tab - reverse_tab = true; - m_state = InputState::Free; - ctrl_held = false; - break; - case '3': - if (m_cursor == m_buffer.size()) { - fputc('\a', stdout); - fflush(stdout); - continue; - } - remove_at_index(m_cursor); - m_refresh_needed = true; - m_search_offset = 0; - m_state = InputState::ExpectTerminator; - ctrl_held = false; - continue; - default: - dbgprintf("Shell: Unhandled final: %02x (%c)\r\n", codepoint, codepoint); - m_state = InputState::Free; - ctrl_held = false; - continue; } - break; - case InputState::ExpectTerminator: + m_inline_search_cursor = m_cursor; + m_search_offset = 0; + m_state = InputState::Free; + ctrl_held = false; + continue; + case 'H': + m_cursor = 0; + m_inline_search_cursor = m_cursor; + m_search_offset = 0; m_state = InputState::Free; + ctrl_held = false; continue; - case InputState::Free: - if (codepoint == 27) { - m_state = InputState::ExpectBracket; + case 'F': + m_cursor = m_buffer.size(); + m_state = InputState::Free; + m_inline_search_cursor = m_cursor; + m_search_offset = 0; + ctrl_held = false; + continue; + case 'Z': // shift+tab + reverse_tab = true; + m_state = InputState::Free; + ctrl_held = false; + break; + case '3': + if (m_cursor == m_buffer.size()) { + fputc('\a', stdout); + fflush(stdout); continue; } - break; + remove_at_index(m_cursor); + m_refresh_needed = true; + m_search_offset = 0; + m_state = InputState::ExpectTerminator; + ctrl_held = false; + continue; + default: + dbgprintf("Shell: Unhandled final: %02x (%c)\r\n", codepoint, codepoint); + m_state = InputState::Free; + ctrl_held = false; + continue; } + break; + case InputState::ExpectTerminator: + m_state = InputState::Free; + continue; + case InputState::Free: + if (codepoint == 27) { + m_state = InputState::ExpectBracket; + continue; + } + break; + } - auto cb = m_key_callbacks.get(codepoint); - if (cb.has_value()) { - if (!cb.value()->callback(*this)) { - continue; - } + auto cb = m_key_callbacks.get(codepoint); + if (cb.has_value()) { + if (!cb.value()->callback(*this)) { + continue; } + } - m_search_offset = 0; // reset search offset on any key + m_search_offset = 0; // reset search offset on any key - if (codepoint == '\t' || reverse_tab) { - if (!on_tab_complete) - continue; + if (codepoint == '\t' || reverse_tab) { + 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_prompt_lines_at_suggestion_initiation = num_lines(); - if (m_suggestion_manager.count() == 0) { - // There are no suggestions, beep. - putchar('\a'); - fflush(stdout); - } - } + // Reverse tab can count as regular tab here. + m_times_tab_pressed++; - // 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; + 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_prompt_lines_at_suggestion_initiation = num_lines(); + if (m_suggestion_manager.count() == 0) { + // There are no suggestions, beep. + putchar('\a'); + fflush(stdout); } - reverse_tab = false; + } - auto completion_mode = m_times_tab_pressed == 1 ? SuggestionManager::CompletePrefix : m_times_tab_pressed == 2 ? SuggestionManager::ShowSuggestions : SuggestionManager::CycleSuggestions; + // 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; - auto completion_result = m_suggestion_manager.attempt_completion(completion_mode, token_start); + auto completion_mode = m_times_tab_pressed == 1 ? SuggestionManager::CompletePrefix : m_times_tab_pressed == 2 ? SuggestionManager::ShowSuggestions : SuggestionManager::CycleSuggestions; - auto new_cursor = m_cursor + completion_result.new_cursor_offset; - for (size_t i = completion_result.offset_region_to_remove.start; i < completion_result.offset_region_to_remove.end; ++i) - remove_at_index(new_cursor); + auto completion_result = m_suggestion_manager.attempt_completion(completion_mode, token_start); - m_cursor = new_cursor; - m_inline_search_cursor = new_cursor; - m_refresh_needed = true; + auto new_cursor = m_cursor + completion_result.new_cursor_offset; + for (size_t i = completion_result.offset_region_to_remove.start; i < completion_result.offset_region_to_remove.end; ++i) + remove_at_index(new_cursor); - for (auto& view : completion_result.insert) - insert(view); + m_cursor = new_cursor; + m_inline_search_cursor = new_cursor; + m_refresh_needed = true; - 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()); - } + for (auto& view : completion_result.insert) + insert(view); - switch (completion_result.new_completion_mode) { - case SuggestionManager::DontComplete: - m_times_tab_pressed = 0; - break; - case SuggestionManager::CompletePrefix: - break; - default: - ++m_times_tab_pressed; - break; - } + if (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()); + } - if (m_times_tab_pressed > 1) { - if (m_suggestion_manager.count() > 0) { - if (m_suggestion_display->cleanup()) - reposition_cursor(); + switch (completion_result.new_completion_mode) { + case SuggestionManager::DontComplete: + m_times_tab_pressed = 0; + break; + case SuggestionManager::CompletePrefix: + break; + default: + ++m_times_tab_pressed; + break; + } - m_suggestion_display->set_initial_prompt_lines(m_prompt_lines_at_suggestion_initiation); + if (m_times_tab_pressed > 1) { + if (m_suggestion_manager.count() > 0) { + if (m_suggestion_display->cleanup()) + reposition_cursor(); - m_suggestion_display->display(m_suggestion_manager); + m_suggestion_display->set_initial_prompt_lines(m_prompt_lines_at_suggestion_initiation); - m_origin_x = m_suggestion_display->origin_x(); - } - } + m_suggestion_display->display(m_suggestion_manager); - if (m_times_tab_pressed > 2) { - if (m_tab_direction == TabDirection::Forward) - m_suggestion_manager.next(); - else - m_suggestion_manager.previous(); + m_origin_x = m_suggestion_display->origin_x(); } + } - if (m_suggestion_manager.count() < 2) { - // We have none, or just one suggestion, - // we should just commit that and continue - // after it, as if it were auto-completed. - suggest(0, 0, Span::CodepointOriented); - m_times_tab_pressed = 0; - m_suggestion_manager.reset(); - m_suggestion_display->finish(); - } - continue; + if (m_times_tab_pressed > 2) { + if (m_tab_direction == TabDirection::Forward) + m_suggestion_manager.next(); + else + m_suggestion_manager.previous(); } - if (m_times_tab_pressed) { - // 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 (m_suggestion_display->cleanup()) { - reposition_cursor(); - m_refresh_needed = true; - } - m_suggestion_manager.reset(); + if (m_suggestion_manager.count() < 2) { + // We have none, or just one suggestion, + // we should just commit that and continue + // after it, as if it were auto-completed. suggest(0, 0, Span::CodepointOriented); + m_times_tab_pressed = 0; + 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 - - auto do_backspace = [&] { - if (m_is_searching) { - return; - } - if (m_cursor == 0) { - fputc('\a', stdout); - fflush(stdout); - return; - } - remove_at_index(m_cursor - 1); - --m_cursor; - m_inline_search_cursor = m_cursor; - // We will have to redraw :( - m_refresh_needed = true; - }; + continue; + } - if (codepoint == 8 || codepoint == m_termios.c_cc[VERASE]) { - do_backspace(); - continue; - } - if (codepoint == m_termios.c_cc[VWERASE]) { - bool has_seen_nonspace = false; - while (m_cursor > 0) { - if (isspace(m_buffer[m_cursor - 1])) { - if (has_seen_nonspace) - break; - } else { - has_seen_nonspace = true; - } - do_backspace(); - } - continue; - } - if (codepoint == m_termios.c_cc[VKILL]) { - for (size_t i = 0; i < m_cursor; ++i) - remove_at_index(0); - m_cursor = 0; + if (m_times_tab_pressed) { + // 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 (m_suggestion_display->cleanup()) { + reposition_cursor(); m_refresh_needed = true; - continue; } - // ^L - if (codepoint == 0xc) { - printf("\033[3J\033[H\033[2J"); // Clear screen. - VT::move_absolute(1, 1); - set_origin(1, 1); - m_refresh_needed = true; - continue; + m_suggestion_manager.reset(); + suggest(0, 0, Span::CodepointOriented); + m_suggestion_display->finish(); + } + m_times_tab_pressed = 0; // Safe to say if we get here, the user didn't press TAB + + auto do_backspace = [&] { + if (m_is_searching) { + return; } - // ^A - if (codepoint == 0x01) { - m_cursor = 0; - continue; + if (m_cursor == 0) { + fputc('\a', stdout); + fflush(stdout); + return; } - // ^R - if (codepoint == 0x12) { - if (m_is_searching) { - // how did we get here? - ASSERT_NOT_REACHED(); + remove_at_index(m_cursor - 1); + --m_cursor; + m_inline_search_cursor = m_cursor; + // We will have to redraw :( + m_refresh_needed = true; + }; + + if (codepoint == 8 || codepoint == m_termios.c_cc[VERASE]) { + do_backspace(); + continue; + } + if (codepoint == m_termios.c_cc[VWERASE]) { + bool has_seen_nonspace = false; + while (m_cursor > 0) { + if (isspace(m_buffer[m_cursor - 1])) { + if (has_seen_nonspace) + break; } else { - m_is_searching = true; - m_search_offset = 0; - m_pre_search_buffer.clear(); - for (auto codepoint : m_buffer) - m_pre_search_buffer.append(codepoint); - m_pre_search_cursor = m_cursor; - m_search_editor = make(Configuration { Configuration::Eager, m_configuration.split_mechanism }); // Has anyone seen 'Inception'? - m_search_editor->on_display_refresh = [this](Editor& search_editor) { - StringBuilder builder; - builder.append(Utf32View { search_editor.buffer().data(), search_editor.buffer().size() }); - search(builder.build()); - refresh_display(); - return; - }; - - // Whenever the search editor gets a ^R, cycle between history entries. - m_search_editor->register_character_input_callback(0x12, [this](Editor& search_editor) { - ++m_search_offset; + has_seen_nonspace = true; + } + do_backspace(); + } + continue; + } + if (codepoint == m_termios.c_cc[VKILL]) { + for (size_t i = 0; i < m_cursor; ++i) + remove_at_index(0); + m_cursor = 0; + m_refresh_needed = true; + continue; + } + // ^L + if (codepoint == 0xc) { + printf("\033[3J\033[H\033[2J"); // Clear screen. + VT::move_absolute(1, 1); + set_origin(1, 1); + m_refresh_needed = true; + continue; + } + // ^A + if (codepoint == 0x01) { + m_cursor = 0; + continue; + } + // ^R + if (codepoint == 0x12) { + if (m_is_searching) { + // how did we get here? + ASSERT_NOT_REACHED(); + } else { + m_is_searching = true; + m_search_offset = 0; + m_pre_search_buffer.clear(); + for (auto codepoint : m_buffer) + m_pre_search_buffer.append(codepoint); + m_pre_search_cursor = m_cursor; + + // Disable our own notifier so as to avoid interfering with the search editor. + m_notifier->set_enabled(false); + + m_search_editor = Editor::construct(Configuration { Configuration::Eager, m_configuration.split_mechanism }); // Has anyone seen 'Inception'? + add_child(*m_search_editor); + + m_search_editor->on_display_refresh = [this](Editor& search_editor) { + StringBuilder builder; + builder.append(Utf32View { search_editor.buffer().data(), search_editor.buffer().size() }); + search(builder.build()); + refresh_display(); + return; + }; + + // Whenever the search editor gets a ^R, cycle between history entries. + m_search_editor->register_character_input_callback(0x12, [this](Editor& search_editor) { + ++m_search_offset; + search_editor.m_refresh_needed = true; + return false; // Do not process this key event + }); + + // Whenever the search editor gets a backspace, cycle back between history entries + // unless we're at the zeroth entry, in which case, allow the deletion. + m_search_editor->register_character_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 - }); - - // 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_character_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 - } - return true; - }); - - // ^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_character_input_callback(0x0c, [this](auto& search_editor) { - printf("\033[3J\033[H\033[2J"); // Clear screen. - - // refresh our own prompt - set_origin(1, 1); - m_refresh_needed = true; - refresh_display(); - - // move the search prompt below ours - // and tell it to redraw itself - search_editor.set_origin(2, 1); - search_editor.m_refresh_needed = true; + } + return true; + }); + + // ^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_character_input_callback(0x0c, [this](auto& search_editor) { + printf("\033[3J\033[H\033[2J"); // Clear screen. + + // refresh our own prompt + set_origin(1, 1); + m_refresh_needed = true; + refresh_display(); - return false; - }); + // move the search prompt below ours + // and tell it to redraw itself + search_editor.set_origin(2, 1); + search_editor.m_refresh_needed = true; - // quit without clearing the current buffer - m_search_editor->register_character_input_callback('\t', [this](Editor& search_editor) { - search_editor.finish(); - m_reset_buffer_on_search_end = false; - return false; - }); + return false; + }); - printf("\n"); - fflush(stdout); + // quit without clearing the current buffer + m_search_editor->register_character_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 "; - auto search_string_result = m_search_editor->get_line(search_prompt); + printf("\n"); + fflush(stdout); - m_search_editor = nullptr; - m_is_searching = false; - m_search_offset = 0; + auto search_prompt = "\x1b[32msearch:\x1b[0m "; + auto search_string_result = m_search_editor->get_line(search_prompt); - if (search_string_result.is_error()) { - // Somethine broke, fail - return search_string_result; - } + remove_child(*m_search_editor); + m_search_editor = nullptr; + m_is_searching = false; + m_search_offset = 0; - auto& search_string = search_string_result.value(); + // Re-enable the notifier after discarding the search editor. + m_notifier->set_enabled(true); - // Manually cleanup the search line. - reposition_cursor(); - auto search_string_codepoint_length = Utf8View { search_string }.length_in_codepoints(); - VT::clear_lines(0, (search_string_codepoint_length + actual_rendered_string_length(search_prompt) + m_num_columns - 1) / m_num_columns); + if (search_string_result.is_error()) { + // Somethine broke, fail + m_input_error = search_string_result.error(); + finish(); + return; + } - reposition_cursor(); + auto& search_string = search_string_result.value(); - if (!m_reset_buffer_on_search_end || search_string_codepoint_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(); - continue; - } + // Manually cleanup the search line. + reposition_cursor(); + auto search_string_codepoint_length = Utf8View { search_string }.length_in_codepoints(); + VT::clear_lines(0, (search_string_codepoint_length + actual_rendered_string_length(search_prompt) + m_num_columns - 1) / m_num_columns); - // Return the string, - finish(); + reposition_cursor(); + + if (!m_reset_buffer_on_search_end || search_string_codepoint_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(); continue; } - continue; - } - // Normally ^D - if (codepoint == m_termios.c_cc[VEOF]) { - if (m_buffer.is_empty()) { - printf("\n"); - if (!m_always_refresh) { - return Error::Eof; - } - } - continue; - } - // ^E - if (codepoint == 0x05) { - m_cursor = m_buffer.size(); - continue; - } - if (codepoint == '\n') { + // Return the string, finish(); continue; } - - insert(codepoint); + continue; } - - if (consumed_codepoints == m_incomplete_data.size()) { - m_incomplete_data.clear(); - } else { - for (size_t i = 0; i < consumed_codepoints; ++i) - m_incomplete_data.take_first(); + // Normally ^D + if (codepoint == m_termios.c_cc[VEOF]) { + if (m_buffer.is_empty()) { + printf("\n"); + if (!m_always_refresh) { + m_input_error = Error::Eof; + finish(); + continue; + } + } + continue; + } + // ^E + if (codepoint == 0x05) { + m_cursor = m_buffer.size(); + continue; } + if (codepoint == '\n') { + finish(); + continue; + } + + insert(codepoint); + } + + if (consumed_codepoints == m_incomplete_data.size()) { + m_incomplete_data.clear(); + } else { + for (size_t i = 0; i < consumed_codepoints; ++i) + m_incomplete_data.take_first(); } } @@ -1304,6 +1351,7 @@ Vector Editor::vt_dsr() auto nread = read(0, buf, 16); if (nread < 0) { m_input_error = Error::ReadFailure; + finish(); break; } @@ -1330,10 +1378,12 @@ Vector Editor::vt_dsr() } dbg() << "Error while reading DSR: " << strerror(errno); m_input_error = Error::ReadFailure; + finish(); return { 1, 1 }; } if (nread == 0) { m_input_error = Error::Empty; + finish(); dbg() << "Terminal DSR issue; received no response"; return { 1, 1 }; } diff --git a/Libraries/LibLine/Editor.h b/Libraries/LibLine/Editor.h index 07fe8f8797..785af92dfa 100644 --- a/Libraries/LibLine/Editor.h +++ b/Libraries/LibLine/Editor.h @@ -38,6 +38,8 @@ #include #include #include +#include +#include #include #include #include @@ -82,7 +84,9 @@ struct Configuration { OperationMode operation_mode { OperationMode::Full }; }; -class Editor { +class Editor : public Core::Object { + C_OBJECT(Editor); + public: enum class Error { ReadFailure, @@ -90,7 +94,6 @@ public: Eof, }; - explicit Editor(Configuration configuration = {}); ~Editor(); Result get_line(const String& prompt); @@ -160,6 +163,8 @@ public: bool is_editing() const { return m_is_editing; } private: + explicit Editor(Configuration configuration = {}); + struct KeyCallback { KeyCallback(Function cb) : callback(move(cb)) @@ -168,6 +173,8 @@ private: Function callback; }; + void handle_read_event(); + Vector vt_dsr(); void remove_at_index(size_t); @@ -208,6 +215,7 @@ private: m_prompt_lines_at_suggestion_initiation = 0; m_refresh_needed = true; m_input_error.clear(); + m_returned_line = String::empty(); } void refresh_display(); @@ -266,7 +274,7 @@ private: bool m_finish { false }; - OwnPtr m_search_editor; + RefPtr m_search_editor; bool m_is_searching { false }; bool m_reset_buffer_on_search_end { true }; size_t m_search_offset { 0 }; @@ -278,6 +286,7 @@ private: ByteBuffer m_pending_chars; Vector m_incomplete_data; Optional m_input_error; + String m_returned_line; size_t m_cursor { 0 }; size_t m_drawn_cursor { 0 }; @@ -336,6 +345,8 @@ private: HashMap> m_anchored_spans_starting; HashMap> m_anchored_spans_ending; + RefPtr m_notifier; + bool m_initialized { false }; bool m_refresh_needed { false }; diff --git a/Shell/Shell.cpp b/Shell/Shell.cpp index 05dbdcd64d..aa13648db5 100644 --- a/Shell/Shell.cpp +++ b/Shell/Shell.cpp @@ -55,7 +55,7 @@ // if we want to be more sh-like, we should do that some day static constexpr bool HighlightVariablesInsideStrings = false; static bool s_disable_hyperlinks = false; -extern Line::Editor editor; +extern RefPtr editor; //#define SH_DEBUG @@ -126,7 +126,7 @@ String Shell::prompt() const }; auto the_prompt = build_prompt(); - auto prompt_length = editor.actual_rendered_string_length(the_prompt); + auto prompt_length = editor->actual_rendered_string_length(the_prompt); if (m_should_continue != ExitCodeOrContinuationRequest::Nothing) { const auto format_string = "\033[34m%.*-s\033[m"; @@ -511,8 +511,8 @@ int Shell::builtin_disown(int argc, const char** argv) int Shell::builtin_history(int, const char**) { - for (size_t i = 0; i < editor.history().size(); ++i) { - printf("%6zu %s\n", i, editor.history()[i].characters()); + for (size_t i = 0; i < editor->history().size(); ++i) { + printf("%6zu %s\n", i, editor->history()[i].characters()); } return 0; } @@ -1385,7 +1385,7 @@ void Shell::load_history() while (history_file->can_read_line()) { auto b = history_file->read_line(1024); // skip the newline and terminating bytes - editor.add_to_history(String(reinterpret_cast(b.data()), b.size() - 2)); + editor->add_to_history(String(reinterpret_cast(b.data()), b.size() - 2)); } } @@ -1395,7 +1395,7 @@ void Shell::save_history() if (file_or_error.is_error()) return; auto& file = *file_or_error.value(); - for (const auto& line : editor.history()) { + for (const auto& line : editor->history()) { file.write(line); file.write("\n"); } @@ -1484,7 +1484,7 @@ void Shell::cache_path() quick_sort(cached_path); } -void Shell::highlight(Line::Editor&) const +void Shell::highlight(Line::Editor& editor) const { StringBuilder builder; if (m_should_continue == ExitCodeOrContinuationRequest::DoubleQuotedString) { @@ -1703,7 +1703,7 @@ Vector Shell::complete(const Line::Editor& editor) bool Shell::read_single_line() { - auto line_result = editor.get_line(prompt()); + auto line_result = editor->get_line(prompt()); if (line_result.is_error()) { m_complete_line_builder.clear(); @@ -1737,7 +1737,7 @@ bool Shell::read_single_line() if (!complete_or_exit_code.has_value()) return true; - editor.add_to_history(m_complete_line_builder.build()); + editor->add_to_history(m_complete_line_builder.build()); m_complete_line_builder.clear(); return true; } diff --git a/Shell/main.cpp b/Shell/main.cpp index 713c3bc738..2ec8118018 100644 --- a/Shell/main.cpp +++ b/Shell/main.cpp @@ -35,7 +35,7 @@ #include #include -Line::Editor editor { Line::Configuration { Line::Configuration::UnescapedSpaces } }; +RefPtr editor; Shell* s_shell; void FileDescriptionCollector::collect() @@ -58,13 +58,13 @@ void FileDescriptionCollector::add(int fd) int main(int argc, char** argv) { Core::EventLoop loop; - + signal(SIGINT, [](int) { - editor.interrupted(); + editor->interrupted(); }); signal(SIGWINCH, [](int) { - editor.resized(); + editor->resized(); }); signal(SIGHUP, [](int) { @@ -92,18 +92,20 @@ int main(int argc, char** argv) return 1; } + editor = Line::Editor::construct(Line::Configuration { Line::Configuration::UnescapedSpaces }); + auto shell = Shell::construct(); s_shell = shell.ptr(); - editor.initialize(); - shell->termios = editor.termios(); - shell->default_termios = editor.default_termios(); + editor->initialize(); + shell->termios = editor->termios(); + shell->default_termios = editor->default_termios(); - editor.on_display_refresh = [&](auto& editor) { + editor->on_display_refresh = [&](auto& editor) { editor.strip_styles(); shell->highlight(editor); }; - editor.on_tab_complete = [&](const Line::Editor& editor) { + editor->on_tab_complete = [&](const Line::Editor& editor) { return shell->complete(editor); }; @@ -128,13 +130,15 @@ int main(int argc, char** argv) return 0; } - editor.on_interrupt_handled = [&] { + editor->on_interrupt_handled = [&] { if (!shell->should_read_more()) { shell->finish_command(); - editor.finish(); + editor->finish(); } }; + shell->add_child(*editor); + Core::EventLoop::current().post_event(*shell, make(Shell::ShellEventType::ReadLine)); return loop.exec(); diff --git a/Userland/js.cpp b/Userland/js.cpp index 24b8ff8e60..54c3bb20c9 100644 --- a/Userland/js.cpp +++ b/Userland/js.cpp @@ -65,7 +65,7 @@ private: static bool s_dump_ast = false; static bool s_print_last_result = false; -static OwnPtr s_editor; +static RefPtr s_editor; static int s_repl_line_level = 0; static bool s_fail_repl = false; @@ -510,7 +510,7 @@ int main(int argc, char** argv) if (test_mode) enable_test_mode(*interpreter); - s_editor = make(); + s_editor = Line::Editor::construct(); signal(SIGINT, [](int) { if (!s_editor->is_editing()) diff --git a/Userland/test-crypto.cpp b/Userland/test-crypto.cpp index a209f473cd..6c701f7247 100644 --- a/Userland/test-crypto.cpp +++ b/Userland/test-crypto.cpp @@ -110,10 +110,9 @@ Core::EventLoop loop; int run(Function fn) { if (interactive) { - Line::Editor editor; - editor.initialize(); + auto editor = Line::Editor::construct(); for (;;) { - auto line_result = editor.get_line("> "); + auto line_result = editor->get_line("> "); if (line_result.is_error()) break; -- cgit v1.2.3