mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-06-09 09:34:57 +09:00
Everywhere: Hoist the Libraries folder to the top-level
This commit is contained in:
parent
950e819ee7
commit
93712b24bf
Notes:
github-actions[bot]
2024-11-10 11:51:52 +00:00
Author: https://github.com/trflynn89
Commit: 93712b24bf
Pull-request: https://github.com/LadybirdBrowser/ladybird/pull/2256
Reviewed-by: https://github.com/sideshowbarker
4547 changed files with 104 additions and 113 deletions
10
Libraries/LibLine/CMakeLists.txt
Normal file
10
Libraries/LibLine/CMakeLists.txt
Normal file
|
@ -0,0 +1,10 @@
|
|||
set(SOURCES
|
||||
Editor.cpp
|
||||
InternalFunctions.cpp
|
||||
KeyCallbackMachine.cpp
|
||||
SuggestionManager.cpp
|
||||
XtermSuggestionDisplay.cpp
|
||||
)
|
||||
|
||||
serenity_lib(LibLine line)
|
||||
target_link_libraries(LibLine PRIVATE LibCore LibUnicode)
|
2366
Libraries/LibLine/Editor.cpp
Normal file
2366
Libraries/LibLine/Editor.cpp
Normal file
File diff suppressed because it is too large
Load diff
529
Libraries/LibLine/Editor.h
Normal file
529
Libraries/LibLine/Editor.h
Normal file
|
@ -0,0 +1,529 @@
|
|||
/*
|
||||
* Copyright (c) 2018-2020, Andreas Kling <andreas@ladybird.org>
|
||||
* Copyright (c) 2021, the SerenityOS developers.
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/BinarySearch.h>
|
||||
#include <AK/ByteBuffer.h>
|
||||
#include <AK/ByteString.h>
|
||||
#include <AK/Function.h>
|
||||
#include <AK/HashMap.h>
|
||||
#include <AK/OwnPtr.h>
|
||||
#include <AK/RedBlackTree.h>
|
||||
#include <AK/Result.h>
|
||||
#include <AK/Traits.h>
|
||||
#include <AK/Utf32View.h>
|
||||
#include <AK/Utf8View.h>
|
||||
#include <AK/Vector.h>
|
||||
#include <LibCore/DirIterator.h>
|
||||
#include <LibCore/EventLoop.h>
|
||||
#include <LibCore/EventReceiver.h>
|
||||
#include <LibCore/Notifier.h>
|
||||
#include <LibLine/KeyCallbackMachine.h>
|
||||
#include <LibLine/Span.h>
|
||||
#include <LibLine/StringMetrics.h>
|
||||
#include <LibLine/Style.h>
|
||||
#include <LibLine/SuggestionDisplay.h>
|
||||
#include <LibLine/SuggestionManager.h>
|
||||
#include <LibLine/VT.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/stat.h>
|
||||
#include <termios.h>
|
||||
|
||||
namespace Line {
|
||||
|
||||
static constexpr u32 ctrl(char c) { return c & 0x3f; }
|
||||
|
||||
struct KeyBinding {
|
||||
Vector<Key> 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<typename Arg, typename... Rest>
|
||||
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<KeyBinding> 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<ByteString, Error> 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<Key> keys, Function<bool(Editor&)> callback) { m_callback_machine.register_key_input_callback(move(keys), move(callback)); }
|
||||
void register_key_input_callback(Key key, Function<bool(Editor&)> callback) { register_key_input_callback(Vector<Key> { key }, move(callback)); }
|
||||
|
||||
static StringMetrics actual_rendered_string_metrics(StringView, RedBlackTree<u32, Optional<Style::Mask>> const& masks = {}, Optional<size_t> maximum_line_width = {});
|
||||
static StringMetrics actual_rendered_string_metrics(Utf32View const&, RedBlackTree<u32, Optional<Style::Mask>> const& masks = {}, Optional<size_t> maximum_line_width = {});
|
||||
|
||||
Function<Vector<CompletionSuggestion>(Editor const&)> on_tab_complete;
|
||||
Function<void(Utf32View, Editor&)> on_paste;
|
||||
Function<void()> on_interrupt_handled;
|
||||
Function<void(Editor&)> on_display_refresh;
|
||||
|
||||
static Function<bool(Editor&)> 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<void> interrupted();
|
||||
ErrorOr<void> 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<u32, 1024> 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<suggestion initiated>, on_tab_complete returns "barx", "bary", "barz"
|
||||
// ^ ^
|
||||
// +-|- static offset: the suggestions start here
|
||||
// +- invariant offset: the suggestions do not change up to here
|
||||
//
|
||||
void 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 { 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<void> try_update_once();
|
||||
void handle_interrupt_event();
|
||||
ErrorOr<void> handle_read_event();
|
||||
ErrorOr<void> handle_resize_event(bool reset_origin);
|
||||
|
||||
void ensure_free_lines_from_origin(size_t count);
|
||||
|
||||
Result<Vector<size_t, 2>, 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<void> refresh_display();
|
||||
ErrorOr<void> cleanup();
|
||||
ErrorOr<void> cleanup_suggestions();
|
||||
ErrorOr<void> 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<void> 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<Editor> m_search_editor;
|
||||
bool m_is_searching { false };
|
||||
bool m_reset_buffer_on_search_end { true };
|
||||
size_t m_search_offset { 0 };
|
||||
enum class SearchOffsetState {
|
||||
Unbiased,
|
||||
Backwards,
|
||||
Forwards,
|
||||
} m_search_offset_state { SearchOffsetState::Unbiased };
|
||||
size_t m_pre_search_cursor { 0 };
|
||||
Vector<u32, 1024> m_pre_search_buffer;
|
||||
|
||||
Vector<u32, 1024> m_buffer;
|
||||
ByteBuffer m_pending_chars;
|
||||
Vector<char, 512> m_incomplete_data;
|
||||
Optional<Error> m_input_error;
|
||||
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<SuggestionDisplay> m_suggestion_display;
|
||||
Vector<u32, 32> 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<HistoryEntry> m_history;
|
||||
size_t m_history_cursor { 0 };
|
||||
size_t m_history_capacity { 1024 };
|
||||
bool m_history_dirty { false };
|
||||
static ErrorOr<Vector<HistoryEntry>> 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<u32, HashMap<u32, Style>> m_spans_starting;
|
||||
HashMap<u32, HashMap<u32, Style>> m_spans_ending;
|
||||
HashMap<u32, HashMap<u32, Style>> m_anchored_spans_starting;
|
||||
HashMap<u32, HashMap<u32, Style>> m_anchored_spans_ending;
|
||||
|
||||
bool contains_up_to_offset(Spans const& other, size_t offset) const;
|
||||
} m_drawn_spans, m_current_spans;
|
||||
|
||||
RedBlackTree<u32, Optional<Style::Mask>> m_current_masks;
|
||||
|
||||
RefPtr<Core::Notifier> m_notifier;
|
||||
|
||||
Vector<u32> m_paste_buffer;
|
||||
Vector<u32> m_last_erased;
|
||||
|
||||
bool m_initialized { false };
|
||||
bool m_refresh_needed { false };
|
||||
Vector<int, 2> m_signal_handlers;
|
||||
|
||||
bool m_is_editing { false };
|
||||
bool m_prohibit_input_processing { false };
|
||||
bool m_have_unprocessed_read_event { false };
|
||||
|
||||
Configuration m_configuration;
|
||||
};
|
||||
|
||||
}
|
771
Libraries/LibLine/InternalFunctions.cpp
Normal file
771
Libraries/LibLine/InternalFunctions.cpp
Normal file
|
@ -0,0 +1,771 @@
|
|||
/*
|
||||
* Copyright (c) 2020, the SerenityOS developers.
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <AK/CharacterTypes.h>
|
||||
#include <AK/ScopeGuard.h>
|
||||
#include <AK/ScopedValueRollback.h>
|
||||
#include <AK/StringBuilder.h>
|
||||
#include <AK/TemporaryChange.h>
|
||||
#include <LibCore/File.h>
|
||||
#include <LibLine/Editor.h>
|
||||
#include <stdio.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace Line {
|
||||
|
||||
Function<bool(Editor&)> 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, "<EOF>\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();
|
||||
add_child(*m_search_editor);
|
||||
|
||||
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;
|
||||
|
||||
remove_child(*m_search_editor);
|
||||
m_search_editor = nullptr;
|
||||
m_is_searching = false;
|
||||
m_is_editing = true;
|
||||
m_search_offset = 0;
|
||||
|
||||
// Re-enable the notifier after discarding the search editor.
|
||||
m_notifier->set_enabled(true);
|
||||
|
||||
if (search_string_result.is_error()) {
|
||||
// Somethine broke, fail
|
||||
m_input_error = search_string_result.error();
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
auto& search_string = search_string_result.value();
|
||||
|
||||
// Manually cleanup the search line.
|
||||
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<u32> 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<char const*> args { editor_command, file_path, nullptr };
|
||||
auto pid = fork();
|
||||
|
||||
if (pid == -1) {
|
||||
perror("fork");
|
||||
return;
|
||||
}
|
||||
|
||||
if (pid == 0) {
|
||||
execvp(editor_command, const_cast<char* const*>(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
88
Libraries/LibLine/KeyCallbackMachine.cpp
Normal file
88
Libraries/LibLine/KeyCallbackMachine.cpp
Normal file
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* Copyright (c) 2020, the SerenityOS developers.
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <AK/Debug.h>
|
||||
#include <LibLine/Editor.h>
|
||||
|
||||
namespace Line {
|
||||
|
||||
void KeyCallbackMachine::register_key_input_callback(Vector<Key> keys, Function<bool(Editor&)> callback)
|
||||
{
|
||||
m_key_callbacks.set(keys, make<KeyCallback>(move(callback)));
|
||||
}
|
||||
|
||||
void KeyCallbackMachine::key_pressed(Editor& editor, Key key)
|
||||
{
|
||||
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<Vector<Key>> 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;
|
||||
}
|
||||
|
||||
}
|
86
Libraries/LibLine/KeyCallbackMachine.h
Normal file
86
Libraries/LibLine/KeyCallbackMachine.h
Normal file
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* Copyright (c) 2020, the SerenityOS developers.
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/Function.h>
|
||||
#include <AK/HashMap.h>
|
||||
#include <AK/Vector.h>
|
||||
|
||||
namespace Line {
|
||||
|
||||
class Editor;
|
||||
|
||||
struct Key {
|
||||
enum Modifier : int {
|
||||
None = 0,
|
||||
Alt = 1,
|
||||
};
|
||||
|
||||
int modifiers { None };
|
||||
unsigned key { 0 };
|
||||
|
||||
Key(unsigned c)
|
||||
: modifiers(None)
|
||||
, key(c) {};
|
||||
|
||||
Key(unsigned c, int modifiers)
|
||||
: modifiers(modifiers)
|
||||
, key(c)
|
||||
{
|
||||
}
|
||||
|
||||
bool operator==(Key const& other) const
|
||||
{
|
||||
return other.key == key && other.modifiers == modifiers;
|
||||
}
|
||||
};
|
||||
|
||||
struct KeyCallback {
|
||||
KeyCallback(Function<bool(Editor&)> cb)
|
||||
: callback(move(cb))
|
||||
{
|
||||
}
|
||||
Function<bool(Editor&)> callback;
|
||||
};
|
||||
|
||||
class KeyCallbackMachine {
|
||||
public:
|
||||
void register_key_input_callback(Vector<Key>, Function<bool(Editor&)> callback);
|
||||
void key_pressed(Editor&, Key);
|
||||
void interrupted(Editor&);
|
||||
bool should_process_last_pressed_key() const { return m_should_process_this_key; }
|
||||
|
||||
private:
|
||||
HashMap<Vector<Key>, NonnullOwnPtr<KeyCallback>> m_key_callbacks;
|
||||
Vector<Vector<Key>> m_current_matching_keys;
|
||||
size_t m_sequence_length { 0 };
|
||||
bool m_should_process_this_key { true };
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
namespace AK {
|
||||
|
||||
template<>
|
||||
struct Traits<Line::Key> : public DefaultTraits<Line::Key> {
|
||||
static constexpr bool is_trivial() { return true; }
|
||||
static unsigned hash(Line::Key k) { return pair_int_hash(k.key, k.modifiers); }
|
||||
};
|
||||
|
||||
template<>
|
||||
struct Traits<Vector<Line::Key>> : public DefaultTraits<Vector<Line::Key>> {
|
||||
static constexpr bool is_trivial() { return false; }
|
||||
static unsigned hash(Vector<Line::Key> const& ks)
|
||||
{
|
||||
unsigned h = 0;
|
||||
for (auto& k : ks)
|
||||
h ^= Traits<Line::Key>::hash(k);
|
||||
return h;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
42
Libraries/LibLine/Span.h
Normal file
42
Libraries/LibLine/Span.h
Normal file
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright (c) 2020, the SerenityOS developers.
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
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 };
|
||||
};
|
||||
|
||||
}
|
45
Libraries/LibLine/StringMetrics.h
Normal file
45
Libraries/LibLine/StringMetrics.h
Normal file
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright (c) 2020-2021, the SerenityOS developers.
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/Types.h>
|
||||
#include <AK/Vector.h>
|
||||
|
||||
namespace Line {
|
||||
|
||||
struct StringMetrics {
|
||||
struct MaskedChar {
|
||||
size_t position { 0 };
|
||||
size_t original_length { 0 };
|
||||
size_t masked_length { 0 };
|
||||
};
|
||||
struct LineMetrics {
|
||||
Vector<MaskedChar> masked_chars;
|
||||
size_t length { 0 };
|
||||
size_t visible_length { 0 };
|
||||
Optional<size_t> bit_length { 0 };
|
||||
|
||||
size_t total_length() const { return length; }
|
||||
};
|
||||
|
||||
Vector<LineMetrics> line_metrics;
|
||||
Vector<size_t> 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 });
|
||||
}
|
||||
};
|
||||
|
||||
}
|
197
Libraries/LibLine/Style.h
Normal file
197
Libraries/LibLine/Style.h
Normal file
|
@ -0,0 +1,197 @@
|
|||
/*
|
||||
* Copyright (c) 2020-2022, the SerenityOS developers.
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/ByteString.h>
|
||||
#include <AK/Types.h>
|
||||
#include <AK/Utf8View.h>
|
||||
#include <AK/Vector.h>
|
||||
|
||||
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<int, 3> m_rgb_color;
|
||||
bool m_is_rgb { false };
|
||||
};
|
||||
|
||||
struct Background : public Color {
|
||||
explicit Background(XtermColor color)
|
||||
: Color(color)
|
||||
{
|
||||
}
|
||||
Background(u8 r, u8 g, u8 b)
|
||||
: Color(r, g, b)
|
||||
{
|
||||
}
|
||||
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<typename T, typename... Rest>
|
||||
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> 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<Mask> m_mask;
|
||||
|
||||
bool m_is_anchored { false };
|
||||
|
||||
bool m_is_empty { true };
|
||||
};
|
||||
}
|
99
Libraries/LibLine/SuggestionDisplay.h
Normal file
99
Libraries/LibLine/SuggestionDisplay.h
Normal file
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* Copyright (c) 2020-2022, the SerenityOS developers.
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/Forward.h>
|
||||
#include <LibLine/StringMetrics.h>
|
||||
#include <LibLine/SuggestionManager.h>
|
||||
|
||||
namespace Line {
|
||||
|
||||
class Editor;
|
||||
|
||||
class SuggestionDisplay {
|
||||
public:
|
||||
virtual ~SuggestionDisplay() = default;
|
||||
virtual ErrorOr<void> display(SuggestionManager const&) = 0;
|
||||
virtual ErrorOr<bool> cleanup() = 0;
|
||||
virtual void finish() = 0;
|
||||
virtual void set_initial_prompt_lines(size_t) = 0;
|
||||
|
||||
ErrorOr<void> 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<Editor>)
|
||||
{
|
||||
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<void> display(SuggestionManager const&) override;
|
||||
virtual ErrorOr<bool> cleanup() override;
|
||||
virtual void finish() override
|
||||
{
|
||||
m_pages.clear();
|
||||
}
|
||||
|
||||
virtual void set_initial_prompt_lines(size_t lines) override
|
||||
{
|
||||
m_prompt_lines_at_suggestion_initiation = lines;
|
||||
}
|
||||
|
||||
virtual void set_vt_size(size_t lines, size_t columns) override
|
||||
{
|
||||
m_num_lines = lines;
|
||||
m_num_columns = columns;
|
||||
m_pages.clear();
|
||||
}
|
||||
|
||||
private:
|
||||
size_t fit_to_page_boundary(size_t selection_index);
|
||||
size_t m_lines_used_for_last_suggestions { 0 };
|
||||
size_t m_num_lines { 0 };
|
||||
size_t m_num_columns { 0 };
|
||||
size_t m_prompt_lines_at_suggestion_initiation { 0 };
|
||||
|
||||
struct PageRange {
|
||||
size_t start;
|
||||
size_t end;
|
||||
};
|
||||
Vector<PageRange> m_pages;
|
||||
};
|
||||
|
||||
}
|
187
Libraries/LibLine/SuggestionManager.cpp
Normal file
187
Libraries/LibLine/SuggestionManager.cpp
Normal file
|
@ -0,0 +1,187 @@
|
|||
/*
|
||||
* Copyright (c) 2020, the SerenityOS developers.
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <AK/Assertions.h>
|
||||
#include <AK/Function.h>
|
||||
#include <LibLine/SuggestionManager.h>
|
||||
|
||||
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<CompletionSuggestion>&& 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<size_t> SuggestionManager::for_each_suggestion(Function<ErrorOr<IterationDecision>(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;
|
||||
}
|
||||
|
||||
}
|
143
Libraries/LibLine/SuggestionManager.h
Normal file
143
Libraries/LibLine/SuggestionManager.h
Normal file
|
@ -0,0 +1,143 @@
|
|||
/*
|
||||
* Copyright (c) 2020, the SerenityOS developers.
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/ByteString.h>
|
||||
#include <AK/Forward.h>
|
||||
#include <AK/String.h>
|
||||
#include <AK/Utf32View.h>
|
||||
#include <AK/Utf8View.h>
|
||||
#include <LibLine/Style.h>
|
||||
|
||||
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 { return text.code_points(); }
|
||||
Utf8View trivia_view() const { return trailing_trivia.code_points(); }
|
||||
Utf8View display_trivia_view() const { return display_trivia.code_points(); }
|
||||
StringView text_string() const { return text.bytes_as_string_view(); }
|
||||
StringView display_trivia_string() const { return display_trivia.bytes_as_string_view(); }
|
||||
bool is_valid { false };
|
||||
};
|
||||
|
||||
class SuggestionManager {
|
||||
friend class Editor;
|
||||
|
||||
public:
|
||||
void set_suggestions(Vector<CompletionSuggestion>&& suggestions);
|
||||
void set_current_suggestion_initiation_index(size_t start_index);
|
||||
|
||||
size_t count() const { return m_suggestions.size(); }
|
||||
size_t display_length() const { return m_last_shown_suggestion_display_length; }
|
||||
size_t start_index() const { return m_last_displayed_suggestion_index; }
|
||||
size_t next_index() const { return m_next_suggestion_index; }
|
||||
void set_start_index(size_t index) const { m_last_displayed_suggestion_index = index; }
|
||||
|
||||
ErrorOr<size_t> for_each_suggestion(Function<ErrorOr<IterationDecision>(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<Utf8View> insert {};
|
||||
|
||||
Optional<Style> style_to_apply {};
|
||||
|
||||
bool avoid_committing_to_single_suggestion { false };
|
||||
};
|
||||
|
||||
CompletionAttemptResult attempt_completion(CompletionMode, size_t initiation_start_index);
|
||||
|
||||
void next();
|
||||
void previous();
|
||||
|
||||
CompletionSuggestion const& suggest();
|
||||
CompletionSuggestion const& current_suggestion() const { return m_last_shown_suggestion; }
|
||||
bool is_current_suggestion_complete() const { return m_last_shown_suggestion_was_complete; }
|
||||
|
||||
void reset()
|
||||
{
|
||||
m_last_shown_suggestion = ByteString::empty();
|
||||
m_last_shown_suggestion_display_length = 0;
|
||||
m_suggestions.clear();
|
||||
m_last_displayed_suggestion_index = 0;
|
||||
m_next_suggestion_index = 0;
|
||||
}
|
||||
|
||||
private:
|
||||
SuggestionManager()
|
||||
{
|
||||
}
|
||||
|
||||
Vector<CompletionSuggestion> m_suggestions;
|
||||
CompletionSuggestion m_last_shown_suggestion { ByteString::empty() };
|
||||
size_t m_last_shown_suggestion_display_length { 0 };
|
||||
bool m_last_shown_suggestion_was_complete { false };
|
||||
mutable size_t m_next_suggestion_index { 0 };
|
||||
size_t m_largest_common_suggestion_prefix_length { 0 };
|
||||
mutable size_t m_last_displayed_suggestion_index { 0 };
|
||||
size_t m_selected_suggestion_index { 0 };
|
||||
};
|
||||
|
||||
}
|
22
Libraries/LibLine/VT.h
Normal file
22
Libraries/LibLine/VT.h
Normal file
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright (c) 2020, the SerenityOS developers.
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AK/Types.h>
|
||||
#include <LibLine/Style.h>
|
||||
|
||||
namespace Line::VT {
|
||||
|
||||
ErrorOr<void> save_cursor(Stream&);
|
||||
ErrorOr<void> restore_cursor(Stream&);
|
||||
ErrorOr<void> clear_to_end_of_line(Stream&);
|
||||
ErrorOr<void> clear_lines(size_t count_above, size_t count_below, Stream&);
|
||||
ErrorOr<void> move_relative(int x, int y, Stream&);
|
||||
ErrorOr<void> move_absolute(u32 x, u32 y, Stream&);
|
||||
ErrorOr<void> apply_style(Style const&, Stream&, bool is_starting = true);
|
||||
|
||||
}
|
195
Libraries/LibLine/XtermSuggestionDisplay.cpp
Normal file
195
Libraries/LibLine/XtermSuggestionDisplay.cpp
Normal file
|
@ -0,0 +1,195 @@
|
|||
/*
|
||||
* Copyright (c) 2020-2021, the SerenityOS developers.
|
||||
*
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*/
|
||||
|
||||
#include <AK/BinarySearch.h>
|
||||
#include <AK/Function.h>
|
||||
#include <AK/StringBuilder.h>
|
||||
#include <LibCore/File.h>
|
||||
#include <LibLine/SuggestionDisplay.h>
|
||||
#include <LibLine/VT.h>
|
||||
#include <stdio.h>
|
||||
|
||||
namespace Line {
|
||||
|
||||
ErrorOr<void> XtermSuggestionDisplay::display(SuggestionManager const& manager)
|
||||
{
|
||||
did_display();
|
||||
|
||||
auto stderr_stream = TRY(Core::File::standard_error());
|
||||
|
||||
size_t longest_suggestion_length = 0;
|
||||
size_t longest_suggestion_byte_length = 0;
|
||||
size_t longest_suggestion_byte_length_without_trivia = 0;
|
||||
|
||||
manager.set_start_index(0);
|
||||
TRY(manager.for_each_suggestion([&](auto& suggestion, auto) {
|
||||
longest_suggestion_length = max(longest_suggestion_length, suggestion.text_view().length() + suggestion.display_trivia_view().length());
|
||||
longest_suggestion_byte_length = max(longest_suggestion_byte_length, suggestion.text_string().length() + suggestion.display_trivia_string().length());
|
||||
longest_suggestion_byte_length_without_trivia = max(longest_suggestion_byte_length_without_trivia, suggestion.text_string().length());
|
||||
return IterationDecision::Continue;
|
||||
}));
|
||||
|
||||
size_t num_printed = 0;
|
||||
size_t lines_used = 1;
|
||||
|
||||
TRY(VT::save_cursor(*stderr_stream));
|
||||
TRY(VT::clear_lines(0, m_lines_used_for_last_suggestions, *stderr_stream));
|
||||
TRY(VT::restore_cursor(*stderr_stream));
|
||||
|
||||
auto spans_entire_line { false };
|
||||
Vector<StringMetrics::LineMetrics> lines;
|
||||
for (size_t i = 0; i < m_prompt_lines_at_suggestion_initiation - 1; ++i)
|
||||
lines.append({ {}, 0 });
|
||||
lines.append({ {}, longest_suggestion_length });
|
||||
auto max_line_count = StringMetrics { move(lines) }.lines_with_addition({ { { {}, 0 } } }, m_num_columns);
|
||||
if (longest_suggestion_length >= m_num_columns - 2) {
|
||||
spans_entire_line = true;
|
||||
// We should make enough space for the biggest entry in
|
||||
// the suggestion list to fit in the prompt line.
|
||||
auto start = max_line_count - m_prompt_lines_at_suggestion_initiation;
|
||||
for (size_t i = start; i < max_line_count; ++i)
|
||||
TRY(stderr_stream->write_until_depleted("\n"sv.bytes()));
|
||||
lines_used += max_line_count;
|
||||
longest_suggestion_length = 0;
|
||||
}
|
||||
|
||||
TRY(VT::move_absolute(max_line_count + m_origin_row, 1, *stderr_stream));
|
||||
|
||||
if (m_pages.is_empty()) {
|
||||
size_t num_printed = 0;
|
||||
size_t lines_used = 1;
|
||||
// Cache the pages.
|
||||
manager.set_start_index(0);
|
||||
size_t page_start = 0;
|
||||
TRY(manager.for_each_suggestion([&](auto& suggestion, auto index) {
|
||||
size_t next_column = num_printed + suggestion.text_view().length() + longest_suggestion_length + 2;
|
||||
if (next_column > m_num_columns) {
|
||||
auto lines = (suggestion.text_view().length() + m_num_columns - 1) / m_num_columns;
|
||||
lines_used += lines;
|
||||
num_printed = 0;
|
||||
}
|
||||
|
||||
if (lines_used + m_prompt_lines_at_suggestion_initiation >= m_num_lines) {
|
||||
m_pages.append({ page_start, index });
|
||||
page_start = index;
|
||||
lines_used = 1;
|
||||
num_printed = 0;
|
||||
}
|
||||
|
||||
if (spans_entire_line)
|
||||
num_printed += m_num_columns;
|
||||
else
|
||||
num_printed += longest_suggestion_length + 2;
|
||||
|
||||
return IterationDecision::Continue;
|
||||
}));
|
||||
// Append the last page.
|
||||
m_pages.append({ page_start, manager.count() });
|
||||
}
|
||||
|
||||
auto page_index = fit_to_page_boundary(manager.next_index());
|
||||
|
||||
manager.set_start_index(m_pages[page_index].start);
|
||||
TRY(manager.for_each_suggestion([&](auto& suggestion, auto index) -> ErrorOr<IterationDecision> {
|
||||
size_t next_column = num_printed + suggestion.text_view().length() + longest_suggestion_length + 2;
|
||||
|
||||
if (next_column > m_num_columns) {
|
||||
auto lines = (suggestion.text_view().length() + m_num_columns - 1) / m_num_columns;
|
||||
lines_used += lines;
|
||||
TRY(stderr_stream->write_until_depleted("\n"sv.bytes()));
|
||||
num_printed = 0;
|
||||
}
|
||||
|
||||
// Show just enough suggestions to fill up the screen
|
||||
// without moving the prompt out of view.
|
||||
if (lines_used + m_prompt_lines_at_suggestion_initiation >= m_num_lines)
|
||||
return IterationDecision::Break;
|
||||
|
||||
// Only apply color to the selection if something is *actually* added to the buffer.
|
||||
if (manager.is_current_suggestion_complete() && index == manager.next_index()) {
|
||||
TRY(VT::apply_style({ Style::Foreground(Style::XtermColor::Blue) }, *stderr_stream));
|
||||
}
|
||||
|
||||
if (spans_entire_line) {
|
||||
num_printed += m_num_columns;
|
||||
TRY(stderr_stream->write_until_depleted(suggestion.text_string().bytes()));
|
||||
TRY(stderr_stream->write_until_depleted(suggestion.display_trivia_string().bytes()));
|
||||
} else {
|
||||
auto field = ByteString::formatted("{: <{}} {}", suggestion.text_string(), longest_suggestion_byte_length_without_trivia, suggestion.display_trivia_string());
|
||||
TRY(stderr_stream->write_until_depleted(ByteString::formatted("{: <{}}", field, longest_suggestion_byte_length + 2)));
|
||||
num_printed += longest_suggestion_length + 2;
|
||||
}
|
||||
|
||||
if (manager.is_current_suggestion_complete() && index == manager.next_index())
|
||||
TRY(VT::apply_style(Style::reset_style(), *stderr_stream));
|
||||
return IterationDecision::Continue;
|
||||
}));
|
||||
|
||||
m_lines_used_for_last_suggestions = lines_used;
|
||||
|
||||
// The last line of the prompt is the same line as the first line of the buffer, so we need to subtract one here.
|
||||
lines_used += m_prompt_lines_at_suggestion_initiation - 1;
|
||||
|
||||
// If we filled the screen, move back the origin.
|
||||
if (m_origin_row + lines_used >= m_num_lines) {
|
||||
m_origin_row = m_num_lines - lines_used;
|
||||
}
|
||||
|
||||
if (m_pages.size() > 1) {
|
||||
auto left_arrow = page_index > 0 ? '<' : ' ';
|
||||
auto right_arrow = page_index < m_pages.size() - 1 ? '>' : ' ';
|
||||
auto string = ByteString::formatted("{:c} page {} of {} {:c}", left_arrow, page_index + 1, m_pages.size(), right_arrow);
|
||||
|
||||
if (string.length() > m_num_columns - 1) {
|
||||
// This would overflow into the next line, so just don't print an indicator.
|
||||
return {};
|
||||
}
|
||||
|
||||
TRY(VT::move_absolute(m_origin_row + lines_used, m_num_columns - string.length() - 1, *stderr_stream));
|
||||
TRY(VT::apply_style({ Style::Background(Style::XtermColor::Green) }, *stderr_stream));
|
||||
TRY(stderr_stream->write_until_depleted(string.bytes()));
|
||||
TRY(VT::apply_style(Style::reset_style(), *stderr_stream));
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
ErrorOr<bool> XtermSuggestionDisplay::cleanup()
|
||||
{
|
||||
did_cleanup();
|
||||
|
||||
if (m_lines_used_for_last_suggestions) {
|
||||
auto stderr_stream = TRY(Core::File::standard_error());
|
||||
TRY(VT::clear_lines(0, m_lines_used_for_last_suggestions, *stderr_stream));
|
||||
m_lines_used_for_last_suggestions = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t XtermSuggestionDisplay::fit_to_page_boundary(size_t selection_index)
|
||||
{
|
||||
VERIFY(m_pages.size() > 0);
|
||||
size_t index = 0;
|
||||
|
||||
auto* match = binary_search(
|
||||
m_pages.span(),
|
||||
PageRange { selection_index, selection_index },
|
||||
&index,
|
||||
[](auto& a, auto& b) -> int {
|
||||
if (a.start >= b.start && a.start < b.end)
|
||||
return 0;
|
||||
return a.start - b.start;
|
||||
});
|
||||
|
||||
if (!match)
|
||||
return m_pages.size() - 1;
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue