mirror of
				https://github.com/LadybirdBrowser/ladybird.git
				synced 2025-11-03 23:00:58 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			771 lines
		
	
	
	
		
			24 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			771 lines
		
	
	
	
		
			24 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
/*
 | 
						|
 * 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);
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 | 
						|
}
 |