Utilities: Replace LibLine with libedit

Use libedit's readline-compatible interface for the JavaScript REPL so
line editing, history, and tab completion no longer depend on LibLine.
The REPL keeps its existing multiline input and completion behavior,
but no longer provides LibLine's live syntax highlighting.

Remove the unused LibLine dependency from the wasm utility and delete
the LibLine library from the build now that it has no remaining
consumers.
This commit is contained in:
Andreas Kling 2026-06-04 18:51:07 +02:00 committed by Andreas Kling
parent 08221aebab
commit dfb2656d9c
22 changed files with 259 additions and 5043 deletions

View file

@ -43,5 +43,3 @@ endif()
if (WIN32)
return()
endif()
add_subdirectory(LibLine)

View file

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

File diff suppressed because it is too large Load diff

View file

@ -1,527 +0,0 @@
/*
* 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 LIFETIME_BOUND { return { m_buffer.data(), m_buffer.size() }; }
auto prohibit_input()
{
auto previous_value = m_prohibit_input_processing;
m_prohibit_input_processing = true;
m_have_unprocessed_read_event = false;
return ScopeGuard {
[this, previous_value] {
m_prohibit_input_processing = previous_value;
if (!m_prohibit_input_processing && m_have_unprocessed_read_event)
handle_read_event().release_value_but_fixme_should_propagate_errors();
}
};
}
private:
explicit Editor(Configuration configuration = Configuration::from_config());
void set_default_keybinds();
enum LoopExitCode {
Exit = 0,
Retry
};
ErrorOr<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;
};
}

View file

@ -1,772 +0,0 @@
/*
* 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();
m_search_editor->on_display_refresh = [this](Editor& search_editor) {
// Remove the search editor prompt before updating ourselves (this avoids artifacts when we move the search editor around).
search_editor.cleanup().release_value_but_fixme_should_propagate_errors();
StringBuilder builder;
builder.append(Utf32View { search_editor.buffer().data(), search_editor.buffer().size() });
if (!search(builder.to_byte_string(), false, false)) {
m_chars_touched_in_the_middle = m_buffer.size();
m_refresh_needed = true;
m_buffer.clear();
m_cursor = 0;
}
refresh_display().release_value_but_fixme_should_propagate_errors();
// Move the search prompt below ours and tell it to redraw itself.
auto prompt_end_line = current_prompt_metrics().lines_with_addition(m_cached_buffer_metrics, m_num_columns);
search_editor.set_origin(prompt_end_line + m_origin_row, 1);
search_editor.m_refresh_needed = true;
};
// Whenever the search editor gets a ^R, cycle between history entries.
m_search_editor->register_key_input_callback(ctrl('R'), [this](Editor& search_editor) {
++m_search_offset;
search_editor.m_refresh_needed = true;
return false; // Do not process this key event
});
// ^C should cancel the search.
m_search_editor->register_key_input_callback(ctrl('C'), [this](Editor& search_editor) {
search_editor.finish();
m_reset_buffer_on_search_end = true;
search_editor.end_search();
search_editor.deferred_invoke([&search_editor] { search_editor.really_quit_event_loop().release_value_but_fixme_should_propagate_errors(); });
return false;
});
// Whenever the search editor gets a backspace, cycle back between history entries
// unless we're at the zeroth entry, in which case, allow the deletion.
m_search_editor->register_key_input_callback(m_termios.c_cc[VERASE], [this](Editor& search_editor) {
if (m_search_offset > 0) {
--m_search_offset;
search_editor.m_refresh_needed = true;
return false; // Do not process this key event
}
search_editor.erase_character_backwards();
return false;
});
// ^L - This is a source of issues, as the search editor refreshes first,
// and we end up with the wrong order of prompts, so we will first refresh
// ourselves, then refresh the search editor, and then tell him not to process
// this event.
m_search_editor->register_key_input_callback(ctrl('L'), [this](auto& search_editor) {
fprintf(stderr, "\033[3J\033[H\033[2J"); // Clear screen.
// refresh our own prompt
{
TemporaryChange refresh_change { m_always_refresh, true };
set_origin(1, 1);
m_refresh_needed = true;
refresh_display().release_value_but_fixme_should_propagate_errors();
}
// move the search prompt below ours
// and tell it to redraw itself
auto prompt_end_line = current_prompt_metrics().lines_with_addition(m_cached_buffer_metrics, m_num_columns);
search_editor.set_origin(prompt_end_line + 1, 1);
search_editor.m_refresh_needed = true;
return false;
});
// quit without clearing the current buffer
m_search_editor->register_key_input_callback('\t', [this](Editor& search_editor) {
search_editor.finish();
m_reset_buffer_on_search_end = false;
return false;
});
auto search_prompt = "\x1b[32msearch:\x1b[0m "sv;
// While the search editor is active, we do not want editing events.
m_is_editing = false;
auto search_string_result = m_search_editor->get_line(search_prompt);
// Grab where the search origin last was, anything up to this point will be cleared.
auto search_end_row = m_search_editor->m_origin_row;
m_search_editor = nullptr;
m_is_searching = false;
m_is_editing = true;
m_search_offset = 0;
// Re-enable the notifier after discarding the search editor.
m_notifier->set_enabled(true);
if (search_string_result.is_error()) {
// Somethine broke, fail
m_input_error = search_string_result.error();
finish();
return;
}
auto& search_string = search_string_result.value();
// Manually cleanup the search line.
auto stderr_stream = Core::File::standard_error().release_value_but_fixme_should_propagate_errors();
reposition_cursor(*stderr_stream).release_value_but_fixme_should_propagate_errors();
auto search_metrics = actual_rendered_string_metrics(search_string, {});
auto metrics = actual_rendered_string_metrics(search_prompt, {});
VT::clear_lines(0, metrics.lines_with_addition(search_metrics, m_num_columns) + search_end_row - m_origin_row - 1, *stderr_stream).release_value_but_fixme_should_propagate_errors();
reposition_cursor(*stderr_stream).release_value_but_fixme_should_propagate_errors();
m_refresh_needed = true;
m_cached_prompt_valid = false;
m_chars_touched_in_the_middle = 1;
if (!m_reset_buffer_on_search_end || search_metrics.total_length == 0) {
// If the entry was empty, or we purposely quit without a newline,
// do not return anything; instead, just end the search.
end_search();
return;
}
// Return the string,
finish();
}
}
namespace {
Optional<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);
}
}
}
}

View file

@ -1,88 +0,0 @@
/*
* 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;
}
}

View file

@ -1,88 +0,0 @@
/*
* 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;
}
};
}

View file

@ -1,42 +0,0 @@
/*
* 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 };
};
}

View file

@ -1,45 +0,0 @@
/*
* 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 });
}
};
}

View file

@ -1,198 +0,0 @@
/*
* 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 };
};
}

View file

@ -1,99 +0,0 @@
/*
* 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;
};
}

View file

@ -1,187 +0,0 @@
/*
* 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;
}
}

View file

@ -1,143 +0,0 @@
/*
* 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 LIFETIME_BOUND { return text.code_points(); }
Utf8View trivia_view() const LIFETIME_BOUND { return trailing_trivia.code_points(); }
Utf8View display_trivia_view() const LIFETIME_BOUND { return display_trivia.code_points(); }
StringView text_string() const LIFETIME_BOUND { return text.bytes_as_string_view(); }
StringView display_trivia_string() const LIFETIME_BOUND { return display_trivia.bytes_as_string_view(); }
bool is_valid { false };
};
class SuggestionManager {
friend class Editor;
public:
void set_suggestions(Vector<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 };
};
}

View file

@ -1,22 +0,0 @@
/*
* 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);
}

View file

@ -1,195 +0,0 @@
/*
* 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;
}
}

View file

@ -2,6 +2,10 @@ include_guard()
find_package(PkgConfig REQUIRED)
if (NOT WIN32 AND NOT ANDROID)
pkg_check_modules(LIBEDIT REQUIRED IMPORTED_TARGET libedit)
endif()
find_package(mimalloc CONFIG REQUIRED)
# AK dependencies

View file

@ -515,6 +515,21 @@
"./skia-install.sh"
]
},
{
"name": "libedit",
"buildsystem": "autotools",
"sources": [
{
"type": "archive",
"url": "https://thrysoee.dk/editline/libedit-20240808-3.1.tar.gz",
"sha256": "5f0573349d77c4a48967191cdd6634dd7aa5f6398c6a57fe037cc02696d6099f"
}
],
"config-opts": [
"--prefix=/app",
"--libdir=/app/lib"
]
},
{
"name": "libtommath",
"buildsystem": "cmake-ninja",

View file

@ -7,6 +7,7 @@
import glob
import json
import os
import re
import subprocess
import sys
import urllib.request
@ -124,7 +125,10 @@ def check_for_match(vcpkg: dict, vcpkg_baseline, name: str, identifier: str) ->
vcpkg[name] = version
if name in vcpkg:
if vcpkg[name] not in identifier:
version = vcpkg[name]
normalized_version = re.sub(r"[^A-Za-z0-9]", "", version)
normalized_identifier = re.sub(r"[^A-Za-z0-9]", "", identifier)
if version not in identifier and normalized_version not in normalized_identifier:
return DepMatch.VersionMismatch
else:
return DepMatch.Match
@ -190,6 +194,10 @@ def check_vcpkg_vs_flatpak_versioning():
mismatch_found |= match_and_update(name, branch)
break
elif "type" in source and source["type"] == "archive":
mismatch_found |= match_and_update(name, source["url"])
break
else:
if not (name == SELF or name in flatpak_build_tools or name in flatpak_transitive_deps):
flatpak.append(name)

View file

@ -1,10 +1,8 @@
if(WIN32)
# FIXME: Add support for LibLine on Windows
ladybird_utility(js SOURCES js.cpp LIBS LibCrypto LibJS LibUnicode LibMain LibTextCodec LibGC Threads::Threads)
ladybird_utility(wasm SOURCES wasm.cpp LIBS LibFileSystem LibWasm LibMain LibJS LibCrypto LibGC)
else()
ladybird_utility(js SOURCES js.cpp LIBS LibCrypto LibJS LibLine LibUnicode LibMain LibTextCodec LibGC Threads::Threads)
ladybird_utility(wasm SOURCES wasm.cpp LIBS LibFileSystem LibWasm LibLine LibMain LibJS LibCrypto LibGC)
ladybird_utility(js SOURCES js.cpp LIBS LibCrypto LibJS LibMain LibTextCodec LibGC)
ladybird_utility(wasm SOURCES wasm.cpp LIBS LibFileSystem LibWasm LibMain LibJS LibCrypto LibGC)
if (NOT WIN32 AND NOT ANDROID)
target_link_libraries(js PRIVATE PkgConfig::LIBEDIT)
endif()
ladybird_utility(xml SOURCES xml.cpp LIBS LibFileSystem LibMain LibXML LibURL)

View file

@ -9,6 +9,7 @@
#include <AK/NeverDestroyed.h>
#include <AK/Platform.h>
#include <AK/ScopeGuard.h>
#include <AK/StringBuilder.h>
#include <LibCore/ArgsParser.h>
#include <LibCore/ConfigFile.h>
@ -33,11 +34,12 @@
#include <LibJS/Token.h>
#include <LibMain/Main.h>
#include <LibTextCodec/Decoder.h>
#include <signal.h>
#if !defined(AK_OS_WINDOWS)
# include <LibLine/Editor.h>
#if !defined(AK_OS_WINDOWS) && !defined(AK_OS_ANDROID)
# include <editline/readline.h>
#endif
#include <stdlib.h>
#include <string.h>
// FIXME: https://github.com/LadybirdBrowser/ladybird/issues/2412
// We should be able to destroy the VM on process exit.
@ -98,8 +100,9 @@ static bool s_print_last_result = false;
static bool s_strip_ansi = false;
static bool s_raw_strings = false;
static bool s_disable_source_location_hints = false;
#if !defined(AK_OS_WINDOWS)
static RefPtr<Line::Editor> s_editor;
#if !defined(AK_OS_WINDOWS) && !defined(AK_OS_ANDROID)
static JS::Realm* s_repl_realm;
static JS::GlobalEnvironment* s_repl_global_environment;
#endif
static String s_history_path = String {};
[[maybe_unused]] static int s_repl_line_level = 0;
@ -163,13 +166,10 @@ static ErrorOr<void> print_all_arguments(JS::VM const& vm, PrintTarget target =
return {};
}
static size_t s_ctrl_c_hit_count = 0;
[[maybe_unused]] static ErrorOr<String> prompt_for_level(int level)
{
static StringBuilder prompt_builder;
prompt_builder.clear();
if (s_ctrl_c_hit_count > 0)
prompt_builder.append("(Use Ctrl+C again to exit)\n"sv);
prompt_builder.append("> "sv);
for (auto i = 0; i < level; ++i)
@ -506,7 +506,199 @@ private:
int m_group_stack_depth { 0 };
};
#if !defined(AK_OS_WINDOWS)
#if !defined(AK_OS_WINDOWS) && !defined(AK_OS_ANDROID)
static Vector<ByteString> complete_repl_line(StringView line)
{
auto& realm = *s_repl_realm;
auto& global_environment = *s_repl_global_environment;
auto source_code = JS::SourceCode::create({}, Utf16String::from_utf8(line));
auto const& code_view = source_code->code_view();
enum {
Initial,
CompleteVariable,
CompleteNullProperty,
CompleteProperty,
} mode { Initial };
Utf16FlyString variable_name;
Utf16FlyString property_name;
bool last_token_has_trivia = false;
struct CompleteState {
decltype(mode)* current_mode;
Utf16FlyString* variable_name;
Utf16FlyString* property_name;
bool* last_token_has_trivia;
Utf16View const* code_view;
} complete_state { &mode, &variable_name, &property_name, &last_token_has_trivia, &code_view };
// We're only going to complete either
// - <N>
// where N is part of the name of a variable
// - <N>.<P>
// where N is the complete name of a variable and
// P is part of the name of one of its properties
JS::FFI::rust_tokenize(source_code->utf16_data(), source_code->length_in_code_units(), &complete_state,
[](void* ctx, JS::FFI::FFIToken const* tok) {
auto& s = *static_cast<CompleteState*>(ctx);
auto type = static_cast<JS::TokenType>(tok->token_type);
auto category = static_cast<JS::TokenCategory>(tok->category);
if (type == JS::TokenType::Eof) {
*s.last_token_has_trivia = tok->trivia_length > 0;
return;
}
auto token_value = [&]() {
return Utf16FlyString::from_utf16(s.code_view->substring_view(tok->offset, tok->length));
};
bool is_identifier_name = type != JS::TokenType::PrivateIdentifier
&& (category == JS::TokenCategory::Identifier || category == JS::TokenCategory::Keyword || category == JS::TokenCategory::ControlKeyword);
switch (*s.current_mode) {
case CompleteVariable:
if (type == JS::TokenType::Period)
*s.current_mode = CompleteNullProperty;
else
*s.current_mode = Initial;
break;
case CompleteNullProperty:
if (is_identifier_name) {
*s.current_mode = CompleteProperty;
*s.property_name = token_value();
} else {
*s.current_mode = Initial;
}
break;
case CompleteProperty:
case Initial:
if (type == JS::TokenType::Identifier) {
*s.current_mode = CompleteVariable;
*s.variable_name = token_value();
} else {
*s.current_mode = Initial;
}
break;
}
});
if (mode == CompleteNullProperty) {
mode = CompleteProperty;
property_name = Utf16FlyString {};
last_token_has_trivia = false; // <name> <dot> [tab] is sensible to complete.
}
if (mode == Initial || last_token_has_trivia)
return {}; // we do not know how to complete this
Vector<ByteString> results;
Function<void(JS::Shape const&, Utf16FlyString const&)> list_all_properties = [&results, &list_all_properties](JS::Shape const& shape, Utf16FlyString const& property_pattern) {
shape.for_each_property_in_insertion_order([&](auto const& property_key, auto const&) {
if (!property_key.is_string())
return;
auto key = property_key.as_string().to_utf16_string();
if (key.starts_with(property_pattern.view())) {
auto completion = key.to_byte_string();
if (!results.contains_slow(completion)) // hide duplicates
results.append(move(completion));
}
});
if (auto const* prototype = shape.prototype()) {
list_all_properties(prototype->shape(), property_pattern);
}
};
switch (mode) {
case CompleteProperty: {
auto reference_or_error = g_vm->resolve_binding(variable_name, JS::Strict::No, &global_environment);
if (reference_or_error.is_error())
return {};
auto value_or_error = reference_or_error.value().get_value(*g_vm);
if (value_or_error.is_error())
return {};
auto variable = value_or_error.value();
VERIFY(!variable.is_special_empty_value());
if (auto object = variable.template as_if<JS::Object>()) {
auto const& shape = object->shape();
list_all_properties(shape, property_name);
for (auto& result : results) {
StringBuilder builder;
builder.append(MUST(variable_name.view().to_byte_string()));
builder.append('.');
builder.append(result);
result = builder.to_byte_string();
}
}
break;
}
case CompleteVariable: {
auto const& variable = realm.global_object();
list_all_properties(variable.shape(), variable_name);
for (auto const& name : global_environment.declarative_record().bindings()) {
if (name.view().starts_with(variable_name.view()))
results.empend(MUST(name.view().to_byte_string()));
}
break;
}
default:
VERIFY_NOT_REACHED();
}
return results;
}
static char** complete_repl_line_for_readline(char const*, int, int)
{
if (!rl_line_buffer)
return nullptr;
rl_attempted_completion_over = 1;
auto completions = complete_repl_line(StringView { rl_line_buffer, strlen(rl_line_buffer) });
if (completions.is_empty())
return nullptr;
auto common_prefix = StringView { completions[0] };
for (size_t i = 1; i < completions.size(); ++i) {
size_t prefix_length = 0;
auto completion = StringView { completions[i] };
while (prefix_length < common_prefix.length()
&& prefix_length < completion.length()
&& common_prefix[prefix_length] == completion[prefix_length]) {
++prefix_length;
}
common_prefix = common_prefix.substring_view(0, prefix_length);
}
auto** matches = static_cast<char**>(calloc(completions.size() + 2, sizeof(char*)));
if (!matches)
return nullptr;
matches[0] = strndup(common_prefix.characters_without_null_termination(), common_prefix.length());
if (!matches[0]) {
free(matches);
return nullptr;
}
for (size_t i = 0; i < completions.size(); ++i) {
matches[i + 1] = strdup(completions[i].characters());
if (!matches[i + 1]) {
for (size_t j = 0; j <= i; ++j)
free(matches[j]);
free(matches);
return nullptr;
}
}
return matches;
}
static ErrorOr<String> read_next_piece()
{
StringBuilder piece;
@ -514,18 +706,22 @@ static ErrorOr<String> read_next_piece()
auto line_level_delta_for_next_line { 0 };
do {
auto line_result = s_editor->get_line(TRY(prompt_for_level(s_repl_line_level)).to_byte_string());
auto prompt = TRY(prompt_for_level(s_repl_line_level)).to_byte_string();
auto* raw_line = readline(prompt.characters());
s_ctrl_c_hit_count = 0;
line_level_delta_for_next_line = 0;
if (line_result.is_error()) {
if (!raw_line) {
s_keep_running_repl = false;
return String {};
}
ArmedScopeGuard free_raw_line = [&] {
free(raw_line);
};
auto& line = line_result.value();
s_editor->add_to_history(line);
auto line = TRY(String::from_utf8(StringView { raw_line, strlen(raw_line) }));
if (!line.is_empty())
add_history(line.to_byte_string().characters());
piece.append(line);
piece.append('\n');
@ -601,7 +797,7 @@ static ErrorOr<void> repl(JS::Realm& realm)
return {};
}
static ErrorOr<int> run_repl(bool gc_on_every_allocation, bool syntax_highlight)
static ErrorOr<int> run_repl(bool gc_on_every_allocation, [[maybe_unused]] bool syntax_highlight)
{
s_print_last_result = true;
@ -614,239 +810,14 @@ static ErrorOr<int> run_repl(bool gc_on_every_allocation, bool syntax_highlight)
g_vm->heap().set_should_collect_on_every_allocation(gc_on_every_allocation);
auto& global_environment = realm.global_environment();
s_repl_realm = &realm;
s_repl_global_environment = &global_environment;
s_editor = Line::Editor::construct();
s_editor->load_history(s_history_path.to_byte_string());
read_history(s_history_path.to_byte_string().characters());
rl_attempted_completion_function = complete_repl_line_for_readline;
signal(SIGINT, [](int) {
if (!s_editor->is_editing())
exit(0);
s_editor->save_history(s_history_path.to_byte_string());
});
s_editor->register_key_input_callback(Line::ctrl('C'), [](Line::Editor& editor) -> bool {
if (editor.buffer_view().length() == 0 || s_ctrl_c_hit_count > 0) {
if (++s_ctrl_c_hit_count == 2) {
s_keep_running_repl = false;
editor.finish_edit();
return false;
}
}
return true;
});
s_editor->on_display_refresh = [syntax_highlight](Line::Editor& editor) {
auto stylize = [&](Line::Span span, Line::Style styles) {
if (syntax_highlight)
editor.stylize(span, styles);
};
editor.strip_styles();
size_t open_indents = s_repl_line_level;
auto line = editor.line();
auto source_code = JS::SourceCode::create({}, Utf16String::from_utf8(line));
struct HighlightState {
decltype(stylize)* stylize_fn;
size_t* open_indents;
bool indenters_starting_line { true };
} highlight_state { &stylize, &open_indents };
JS::FFI::rust_tokenize(source_code->utf16_data(), source_code->length_in_code_units(), &highlight_state,
[](void* ctx, JS::FFI::FFIToken const* tok) {
auto& state = *static_cast<HighlightState*>(ctx);
auto type = static_cast<JS::TokenType>(tok->token_type);
auto category = static_cast<JS::TokenCategory>(tok->category);
auto start = static_cast<size_t>(tok->offset);
auto end = start + tok->length;
if (type == JS::TokenType::Eof)
return;
if (state.indenters_starting_line) {
if (type != JS::TokenType::ParenClose && type != JS::TokenType::BracketClose && type != JS::TokenType::CurlyClose)
state.indenters_starting_line = false;
else
--(*state.open_indents);
}
switch (category) {
case JS::TokenCategory::Invalid:
(*state.stylize_fn)({ start, end, Line::Span::CodepointOriented }, { Line::Style::Foreground(Line::Style::XtermColor::Red), Line::Style::Underline });
break;
case JS::TokenCategory::Number:
(*state.stylize_fn)({ start, end, Line::Span::CodepointOriented }, { Line::Style::Foreground(Line::Style::XtermColor::Magenta) });
break;
case JS::TokenCategory::String:
(*state.stylize_fn)({ start, end, Line::Span::CodepointOriented }, { Line::Style::Foreground(Line::Style::XtermColor::Green), Line::Style::Bold });
break;
case JS::TokenCategory::Punctuation:
case JS::TokenCategory::Operator:
break;
case JS::TokenCategory::Keyword:
if (type == JS::TokenType::BoolLiteral || type == JS::TokenType::NullLiteral)
(*state.stylize_fn)({ start, end, Line::Span::CodepointOriented }, { Line::Style::Foreground(Line::Style::XtermColor::Yellow), Line::Style::Bold });
else
(*state.stylize_fn)({ start, end, Line::Span::CodepointOriented }, { Line::Style::Foreground(Line::Style::XtermColor::Blue), Line::Style::Bold });
break;
case JS::TokenCategory::ControlKeyword:
(*state.stylize_fn)({ start, end, Line::Span::CodepointOriented }, { Line::Style::Foreground(Line::Style::XtermColor::Cyan), Line::Style::Italic });
break;
case JS::TokenCategory::Identifier:
(*state.stylize_fn)({ start, end, Line::Span::CodepointOriented }, { Line::Style::Foreground(Line::Style::XtermColor::White), Line::Style::Bold });
break;
default:
break;
}
});
editor.set_prompt(prompt_for_level(open_indents).release_value_but_fixme_should_propagate_errors().to_byte_string());
};
auto complete = [&realm, &global_environment](Line::Editor const& editor) -> Vector<Line::CompletionSuggestion> {
auto line = editor.line(editor.cursor());
auto source_code = JS::SourceCode::create({}, Utf16String::from_utf8(line));
auto const& code_view = source_code->code_view();
enum {
Initial,
CompleteVariable,
CompleteNullProperty,
CompleteProperty,
} mode { Initial };
Utf16FlyString variable_name;
Utf16FlyString property_name;
bool last_token_has_trivia = false;
struct CompleteState {
decltype(mode)* current_mode;
Utf16FlyString* variable_name;
Utf16FlyString* property_name;
bool* last_token_has_trivia;
Utf16View const* code_view;
} complete_state { &mode, &variable_name, &property_name, &last_token_has_trivia, &code_view };
// we're only going to complete either
// - <N>
// where N is part of the name of a variable
// - <N>.<P>
// where N is the complete name of a variable and
// P is part of the name of one of its properties
JS::FFI::rust_tokenize(source_code->utf16_data(), source_code->length_in_code_units(), &complete_state,
[](void* ctx, JS::FFI::FFIToken const* tok) {
auto& s = *static_cast<CompleteState*>(ctx);
auto type = static_cast<JS::TokenType>(tok->token_type);
auto category = static_cast<JS::TokenCategory>(tok->category);
if (type == JS::TokenType::Eof) {
*s.last_token_has_trivia = tok->trivia_length > 0;
return;
}
auto token_value = [&]() {
return Utf16FlyString::from_utf16(s.code_view->substring_view(tok->offset, tok->length));
};
bool is_identifier_name = type != JS::TokenType::PrivateIdentifier
&& (category == JS::TokenCategory::Identifier || category == JS::TokenCategory::Keyword || category == JS::TokenCategory::ControlKeyword);
switch (*s.current_mode) {
case CompleteVariable:
if (type == JS::TokenType::Period)
*s.current_mode = CompleteNullProperty;
else
*s.current_mode = Initial;
break;
case CompleteNullProperty:
if (is_identifier_name) {
*s.current_mode = CompleteProperty;
*s.property_name = token_value();
} else {
*s.current_mode = Initial;
}
break;
case CompleteProperty:
case Initial:
if (type == JS::TokenType::Identifier) {
*s.current_mode = CompleteVariable;
*s.variable_name = token_value();
} else {
*s.current_mode = Initial;
}
break;
}
});
if (mode == CompleteNullProperty) {
mode = CompleteProperty;
property_name = Utf16FlyString {};
last_token_has_trivia = false; // <name> <dot> [tab] is sensible to complete.
}
if (mode == Initial || last_token_has_trivia)
return {}; // we do not know how to complete this
Vector<Line::CompletionSuggestion> results;
Function<void(JS::Shape const&, Utf16FlyString const&)> list_all_properties = [&results, &list_all_properties](JS::Shape const& shape, Utf16FlyString const& property_pattern) {
shape.for_each_property_in_insertion_order([&](auto const& property_key, auto const&) {
if (!property_key.is_string())
return;
auto key = property_key.as_string().to_utf16_string();
if (key.starts_with(property_pattern.view())) {
Line::CompletionSuggestion completion { key.to_utf8_but_should_be_ported_to_utf16(), Line::CompletionSuggestion::ForSearch };
if (!results.contains_slow(completion)) { // hide duplicates
results.append(key.to_byte_string());
results.last().invariant_offset = property_pattern.length_in_code_units();
}
}
});
if (auto const* prototype = shape.prototype()) {
list_all_properties(prototype->shape(), property_pattern);
}
};
switch (mode) {
case CompleteProperty: {
auto reference_or_error = g_vm->resolve_binding(variable_name, JS::Strict::No, &global_environment);
if (reference_or_error.is_error())
return {};
auto value_or_error = reference_or_error.value().get_value(*g_vm);
if (value_or_error.is_error())
return {};
auto variable = value_or_error.value();
VERIFY(!variable.is_special_empty_value());
if (auto object = variable.template as_if<JS::Object>()) {
auto const& shape = object->shape();
list_all_properties(shape, property_name);
}
break;
}
case CompleteVariable: {
auto const& variable = realm.global_object();
list_all_properties(variable.shape(), variable_name);
for (auto const& name : global_environment.declarative_record().bindings()) {
if (name.view().starts_with(variable_name.view())) {
results.empend(MUST(name.view().to_byte_string()));
results.last().invariant_offset = variable_name.length_in_code_units();
}
}
break;
}
default:
VERIFY_NOT_REACHED();
}
return results;
};
s_editor->on_tab_complete = move(complete);
TRY(repl(realm));
s_editor->save_history(s_history_path.to_byte_string());
write_history(s_history_path.to_byte_string().characters());
return s_exit_code;
}
@ -914,8 +885,8 @@ ErrorOr<int> ladybird_main(Main::Arguments arguments)
// FIXME: Figure out some way to interrupt the interpreter now that vm.exception() is gone.
if (evaluate_script.is_empty() && script_paths.is_empty()) {
#if defined(AK_OS_WINDOWS)
dbgln("REPL functionality is not supported on Windows");
#if defined(AK_OS_WINDOWS) || defined(AK_OS_ANDROID)
dbgln("REPL functionality is not supported on this platform");
VERIFY_NOT_REACHED();
#else
return run_repl(gc_on_every_allocation, syntax_highlight);

View file

@ -22,8 +22,6 @@
#include <LibJS/Script.h>
#if defined(AK_OS_WINDOWS)
# include <AK/Windows.h>
#else
# include <LibLine/Editor.h>
#endif
#include <LibMain/Main.h>
#include <LibWasm/AbstractMachine/AbstractMachine.h>

View file

@ -99,6 +99,10 @@
"name": "libproxy",
"platform": "!(android | bsd)"
},
{
"name": "libedit",
"platform": "!windows & !android"
},
"libtommath",
{
"name": "libwebp",
@ -325,6 +329,10 @@
"name": "libdwarf",
"version": "2.3.0#0"
},
{
"name": "libedit",
"version": "2024-08-08#0"
},
{
"name": "libjpeg-turbo",
"version": "3.1.1#0"