mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2024-11-25 17:10:23 +00:00
LibLine: Implement ^R searching
This commit adds searching in the editor history with ^R. It does so by instantiating...another Line::Editor inside the current Line::Editor :^)
This commit is contained in:
parent
df6696f576
commit
58912994ab
Notes:
sideshowbarker
2024-07-19 07:26:33 +09:00
Author: https://github.com/alimpfard Commit: https://github.com/SerenityOS/serenity/commit/58912994ab4 Pull-request: https://github.com/SerenityOS/serenity/pull/1884 Reviewed-by: https://github.com/awesomekling
2 changed files with 167 additions and 20 deletions
|
@ -33,8 +33,9 @@
|
||||||
|
|
||||||
namespace Line {
|
namespace Line {
|
||||||
|
|
||||||
Editor::Editor()
|
Editor::Editor(bool always_refresh)
|
||||||
{
|
{
|
||||||
|
m_always_refresh = always_refresh;
|
||||||
m_pending_chars = ByteBuffer::create_uninitialized(0);
|
m_pending_chars = ByteBuffer::create_uninitialized(0);
|
||||||
struct winsize ws;
|
struct winsize ws;
|
||||||
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) < 0) {
|
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) < 0) {
|
||||||
|
@ -126,7 +127,17 @@ String Editor::get_line(const String& prompt)
|
||||||
|
|
||||||
m_history_cursor = m_history.size();
|
m_history_cursor = m_history.size();
|
||||||
for (;;) {
|
for (;;) {
|
||||||
|
if (m_always_refresh)
|
||||||
|
m_refresh_needed = true;
|
||||||
refresh_display();
|
refresh_display();
|
||||||
|
if (m_finish) {
|
||||||
|
m_finish = false;
|
||||||
|
printf("\n");
|
||||||
|
fflush(stdout);
|
||||||
|
auto string = String::copy(m_buffer);
|
||||||
|
m_buffer.clear();
|
||||||
|
return string;
|
||||||
|
}
|
||||||
char keybuf[16];
|
char keybuf[16];
|
||||||
ssize_t nread = read(0, keybuf, sizeof(keybuf));
|
ssize_t nread = read(0, keybuf, sizeof(keybuf));
|
||||||
// FIXME: exit()ing here is a bit off. Should communicate failure to caller somehow instead.
|
// FIXME: exit()ing here is a bit off. Should communicate failure to caller somehow instead.
|
||||||
|
@ -138,13 +149,17 @@ String Editor::get_line(const String& prompt)
|
||||||
m_was_interrupted = false;
|
m_was_interrupted = false;
|
||||||
if (!m_buffer.is_empty())
|
if (!m_buffer.is_empty())
|
||||||
printf("^C");
|
printf("^C");
|
||||||
|
|
||||||
|
if (m_is_searching) {
|
||||||
|
end_search();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (m_was_resized)
|
if (m_was_resized)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
m_buffer.clear();
|
finish();
|
||||||
putchar('\n');
|
continue;
|
||||||
return String::empty();
|
|
||||||
}
|
}
|
||||||
perror("read failed");
|
perror("read failed");
|
||||||
// FIXME: exit()ing here is a bit off. Should communicate failure to caller somehow instead.
|
// FIXME: exit()ing here is a bit off. Should communicate failure to caller somehow instead.
|
||||||
|
@ -500,6 +515,9 @@ String Editor::get_line(const String& prompt)
|
||||||
m_times_tab_pressed = 0; // Safe to say if we get here, the user didn't press TAB
|
m_times_tab_pressed = 0; // Safe to say if we get here, the user didn't press TAB
|
||||||
|
|
||||||
auto do_backspace = [&] {
|
auto do_backspace = [&] {
|
||||||
|
if (m_is_searching) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (m_cursor == 0) {
|
if (m_cursor == 0) {
|
||||||
fputc('\a', stdout);
|
fputc('\a', stdout);
|
||||||
fflush(stdout);
|
fflush(stdout);
|
||||||
|
@ -549,11 +567,111 @@ String Editor::get_line(const String& prompt)
|
||||||
m_cursor = 0;
|
m_cursor = 0;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
// ^R
|
||||||
|
if (ch == 0x12) {
|
||||||
|
if (m_is_searching) {
|
||||||
|
// how did we get here?
|
||||||
|
ASSERT_NOT_REACHED();
|
||||||
|
} else {
|
||||||
|
m_is_searching = true;
|
||||||
|
m_search_offset = 0;
|
||||||
|
m_pre_search_buffer = m_buffer;
|
||||||
|
m_pre_search_cursor = m_cursor;
|
||||||
|
m_search_editor = make<Editor>(true); // Has anyone seen 'Inception'?
|
||||||
|
m_search_editor->initialize();
|
||||||
|
m_search_editor->on_display_refresh = [this](Editor& search_editor) {
|
||||||
|
int last_matching_offset = -1;
|
||||||
|
|
||||||
|
// do not search for empty strings
|
||||||
|
if (search_editor.buffer().size() > 0) {
|
||||||
|
size_t search_offset = m_search_offset;
|
||||||
|
StringView search_term { search_editor.buffer().data(), search_editor.buffer().size() };
|
||||||
|
for (size_t i = m_history_cursor; i > 0; --i) {
|
||||||
|
if (m_history[i - 1].contains(search_term)) {
|
||||||
|
last_matching_offset = i - 1;
|
||||||
|
if (search_offset == 0)
|
||||||
|
break;
|
||||||
|
--search_offset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (last_matching_offset == -1) {
|
||||||
|
fputc('\a', stdout);
|
||||||
|
fflush(stdout);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_buffer.clear();
|
||||||
|
m_cursor = 0;
|
||||||
|
if (last_matching_offset >= 0)
|
||||||
|
insert(m_history[last_matching_offset]);
|
||||||
|
// always needed
|
||||||
|
m_refresh_needed = true;
|
||||||
|
refresh_display();
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
// whenever the search editor gets a ^R, cycle between history entries
|
||||||
|
m_search_editor->register_character_input_callback(0x12, [this](Editor& search_editor) {
|
||||||
|
++m_search_offset;
|
||||||
|
search_editor.m_refresh_needed = true;
|
||||||
|
return false; // Do not process this key event
|
||||||
|
});
|
||||||
|
|
||||||
|
// whenever the search editor gets a backspace, cycle back between history entries
|
||||||
|
// unless we're at the zeroth entry, in which case, allow the deletion
|
||||||
|
m_search_editor->register_character_input_callback(m_termios.c_cc[VERASE], [this](Editor& search_editor) {
|
||||||
|
if (m_search_offset > 0) {
|
||||||
|
--m_search_offset;
|
||||||
|
search_editor.m_refresh_needed = true;
|
||||||
|
return false; // Do not process this key event
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// quit without clearing the current buffer
|
||||||
|
m_search_editor->register_character_input_callback('\t', [this](Editor& search_editor) {
|
||||||
|
search_editor.finish();
|
||||||
|
m_reset_buffer_on_search_end = false;
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
printf("\n");
|
||||||
|
fflush(stdout);
|
||||||
|
|
||||||
|
auto search_prompt = "\x1b[32msearch:\x1b[0m ";
|
||||||
|
auto search_string = m_search_editor->get_line(search_prompt);
|
||||||
|
m_search_editor = nullptr;
|
||||||
|
m_is_searching = false;
|
||||||
|
m_search_offset = 0;
|
||||||
|
|
||||||
|
// manually cleanup the search line
|
||||||
|
reposition_cursor();
|
||||||
|
vt_clear_lines(0, (search_string.length() + actual_rendered_string_length(search_prompt) + m_num_columns - 1) / m_num_columns);
|
||||||
|
|
||||||
|
reposition_cursor();
|
||||||
|
|
||||||
|
if (!m_reset_buffer_on_search_end || search_string.length() == 0) {
|
||||||
|
// if the entry was empty, or we purposely quit without a newline,
|
||||||
|
// do not return anything
|
||||||
|
// instead, just end the search
|
||||||
|
end_search();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// return the string
|
||||||
|
finish();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
// Normally ^D
|
// Normally ^D
|
||||||
if (ch == m_termios.c_cc[VEOF]) {
|
if (ch == m_termios.c_cc[VEOF]) {
|
||||||
if (m_buffer.is_empty()) {
|
if (m_buffer.is_empty()) {
|
||||||
printf("<EOF>\n");
|
printf("<EOF>\n");
|
||||||
exit(0);
|
if (!m_always_refresh) // this is a little off, but it'll do for now
|
||||||
|
exit(0);
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -564,11 +682,8 @@ String Editor::get_line(const String& prompt)
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (ch == '\n') {
|
if (ch == '\n') {
|
||||||
putchar('\n');
|
finish();
|
||||||
fflush(stdout);
|
continue;
|
||||||
auto string = String::copy(m_buffer);
|
|
||||||
m_buffer.clear();
|
|
||||||
return string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
insert(ch);
|
insert(ch);
|
||||||
|
@ -591,15 +706,17 @@ void Editor::recalculate_origin()
|
||||||
// but that will be calculated and applied at the next
|
// but that will be calculated and applied at the next
|
||||||
// refresh cycle
|
// refresh cycle
|
||||||
}
|
}
|
||||||
|
void Editor::cleanup()
|
||||||
|
{
|
||||||
|
vt_move_relative(0, m_pending_chars.size() - m_chars_inserted_in_the_middle);
|
||||||
|
auto current_line = cursor_line();
|
||||||
|
|
||||||
|
vt_clear_lines(current_line - 1, num_lines() - current_line);
|
||||||
|
vt_move_relative(-num_lines() + 1, -offset_in_line() - m_old_prompt_length - m_pending_chars.size() + m_chars_inserted_in_the_middle);
|
||||||
|
};
|
||||||
|
|
||||||
void Editor::refresh_display()
|
void Editor::refresh_display()
|
||||||
{
|
{
|
||||||
auto cleanup = [&] {
|
|
||||||
vt_move_relative(0, m_pending_chars.size() - m_chars_inserted_in_the_middle);
|
|
||||||
auto current_line = cursor_line();
|
|
||||||
|
|
||||||
vt_clear_lines(current_line - 1, num_lines() - current_line);
|
|
||||||
vt_move_relative(-num_lines() + 1, -offset_in_line() - m_old_prompt_length - m_pending_chars.size() + m_chars_inserted_in_the_middle);
|
|
||||||
};
|
|
||||||
auto has_cleaned_up = false;
|
auto has_cleaned_up = false;
|
||||||
// someone changed the window size, figure it out
|
// someone changed the window size, figure it out
|
||||||
// and react to it, we might need to redraw
|
// and react to it, we might need to redraw
|
||||||
|
|
|
@ -77,7 +77,7 @@ struct CompletionSuggestion {
|
||||||
|
|
||||||
class Editor {
|
class Editor {
|
||||||
public:
|
public:
|
||||||
Editor();
|
explicit Editor(bool always_refresh = false);
|
||||||
~Editor();
|
~Editor();
|
||||||
|
|
||||||
void initialize()
|
void initialize()
|
||||||
|
@ -143,6 +143,11 @@ public:
|
||||||
const struct termios& termios() const { return m_termios; }
|
const struct termios& termios() const { return m_termios; }
|
||||||
const struct termios& default_termios() const { return m_default_termios; }
|
const struct termios& default_termios() const { return m_default_termios; }
|
||||||
|
|
||||||
|
void finish()
|
||||||
|
{
|
||||||
|
m_finish = true;
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void vt_save_cursor();
|
void vt_save_cursor();
|
||||||
void vt_restore_cursor();
|
void vt_restore_cursor();
|
||||||
|
@ -155,6 +160,19 @@ private:
|
||||||
|
|
||||||
Style find_applicable_style(size_t offset) const;
|
Style find_applicable_style(size_t offset) const;
|
||||||
|
|
||||||
|
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 = m_pre_search_buffer;
|
||||||
|
m_cursor = m_pre_search_cursor;
|
||||||
|
}
|
||||||
|
m_reset_buffer_on_search_end = false;
|
||||||
|
m_search_editor = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
void reset()
|
void reset()
|
||||||
{
|
{
|
||||||
m_origin_x = 0;
|
m_origin_x = 0;
|
||||||
|
@ -165,6 +183,7 @@ private:
|
||||||
}
|
}
|
||||||
|
|
||||||
void refresh_display();
|
void refresh_display();
|
||||||
|
void cleanup();
|
||||||
|
|
||||||
size_t current_prompt_length() const
|
size_t current_prompt_length() const
|
||||||
{
|
{
|
||||||
|
@ -197,6 +216,15 @@ private:
|
||||||
void recalculate_origin();
|
void recalculate_origin();
|
||||||
void reposition_cursor();
|
void reposition_cursor();
|
||||||
|
|
||||||
|
bool m_finish { false };
|
||||||
|
|
||||||
|
OwnPtr<Editor> m_search_editor;
|
||||||
|
bool m_is_searching { false };
|
||||||
|
bool m_reset_buffer_on_search_end { true };
|
||||||
|
size_t m_search_offset { 0 };
|
||||||
|
size_t m_pre_search_cursor { 0 };
|
||||||
|
Vector<char, 1024> m_pre_search_buffer;
|
||||||
|
|
||||||
Vector<char, 1024> m_buffer;
|
Vector<char, 1024> m_buffer;
|
||||||
ByteBuffer m_pending_chars;
|
ByteBuffer m_pending_chars;
|
||||||
size_t m_cursor { 0 };
|
size_t m_cursor { 0 };
|
||||||
|
@ -225,6 +253,8 @@ private:
|
||||||
size_t m_next_suggestion_invariant_offset { 0 };
|
size_t m_next_suggestion_invariant_offset { 0 };
|
||||||
size_t m_largest_common_suggestion_prefix_length { 0 };
|
size_t m_largest_common_suggestion_prefix_length { 0 };
|
||||||
|
|
||||||
|
bool m_always_refresh { false };
|
||||||
|
|
||||||
enum class TabDirection {
|
enum class TabDirection {
|
||||||
Forward,
|
Forward,
|
||||||
Backward,
|
Backward,
|
||||||
|
@ -235,8 +265,8 @@ private:
|
||||||
|
|
||||||
// TODO: handle signals internally
|
// TODO: handle signals internally
|
||||||
struct termios m_termios, m_default_termios;
|
struct termios m_termios, m_default_termios;
|
||||||
bool m_was_interrupted = false;
|
bool m_was_interrupted { false };
|
||||||
bool m_was_resized = false;
|
bool m_was_resized { false };
|
||||||
|
|
||||||
// FIXME: This should be something more take_first()-friendly.
|
// FIXME: This should be something more take_first()-friendly.
|
||||||
Vector<String> m_history;
|
Vector<String> m_history;
|
||||||
|
|
Loading…
Reference in a new issue