From cb3cf589ed8a5df38a801bbc34a21951924e9195 Mon Sep 17 00:00:00 2001 From: AnotherTest Date: Sun, 19 Apr 2020 18:32:28 +0430 Subject: [PATCH] LibLine: Allow suggestions to have trailing trivia strings These strings would be applied when inserted into the buffer, but are not shown as part of the suggestion. This commit also patches up Userland/js and Shell to use this functionality --- Libraries/LibLine/Editor.cpp | 31 ++++++++------- Libraries/LibLine/Editor.h | 30 +++++++++++++-- Shell/main.cpp | 75 ++++++++++++------------------------ Userland/js.cpp | 6 +-- 4 files changed, 72 insertions(+), 70 deletions(-) diff --git a/Libraries/LibLine/Editor.cpp b/Libraries/LibLine/Editor.cpp index 3958bec31d..071678d03f 100644 --- a/Libraries/LibLine/Editor.cpp +++ b/Libraries/LibLine/Editor.cpp @@ -299,17 +299,17 @@ String Editor::get_line(const String& prompt) m_suggestions = on_tab_complete_other_token(token); size_t common_suggestion_prefix { 0 }; if (m_suggestions.size() == 1) { - m_largest_common_suggestion_prefix_length = m_suggestions[0].length(); + m_largest_common_suggestion_prefix_length = m_suggestions[0].text.length(); } else if (m_suggestions.size()) { char last_valid_suggestion_char; for (;; ++common_suggestion_prefix) { - if (m_suggestions[0].length() <= common_suggestion_prefix) + if (m_suggestions[0].text.length() <= common_suggestion_prefix) goto no_more_commons; - last_valid_suggestion_char = m_suggestions[0][common_suggestion_prefix]; + last_valid_suggestion_char = m_suggestions[0].text[common_suggestion_prefix]; for (const auto& suggestion : m_suggestions) { - if (suggestion.length() < common_suggestion_prefix || suggestion[common_suggestion_prefix] != last_valid_suggestion_char) { + if (suggestion.text.length() < common_suggestion_prefix || suggestion.text[common_suggestion_prefix] != last_valid_suggestion_char) { goto no_more_commons; } } @@ -341,7 +341,7 @@ String Editor::get_line(const String& prompt) auto current_suggestion_index = m_next_suggestion_index; if (m_next_suggestion_index < m_suggestions.size()) { auto can_complete = m_next_suggestion_invariant_offset < m_largest_common_suggestion_prefix_length; - if (!m_last_shown_suggestion.is_null()) { + if (!m_last_shown_suggestion.text.is_null()) { size_t actual_offset; size_t shown_length = m_last_shown_suggestion_display_length; switch (m_times_tab_pressed) { @@ -367,17 +367,20 @@ String Editor::get_line(const String& prompt) m_refresh_needed = true; } m_last_shown_suggestion = m_suggestions[m_next_suggestion_index]; - m_last_shown_suggestion_display_length = m_last_shown_suggestion.length(); + m_last_shown_suggestion_display_length = m_last_shown_suggestion.text.length(); m_last_shown_suggestion_was_complete = true; if (m_times_tab_pressed == 1) { // This is the first time, so only auto-complete *if possible* if (can_complete) { - insert(m_last_shown_suggestion.substring_view(m_next_suggestion_invariant_offset, m_largest_common_suggestion_prefix_length - m_next_suggestion_invariant_offset)); + insert(m_last_shown_suggestion.text.substring_view(m_next_suggestion_invariant_offset, m_largest_common_suggestion_prefix_length - m_next_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 m_times_tab_pressed = 0; + // add in the trivia of the last selected suggestion + insert(m_last_shown_suggestion.trailing_trivia); + m_last_shown_suggestion_display_length += m_last_shown_suggestion.trailing_trivia.length(); } } else { m_last_shown_suggestion_display_length = 0; @@ -385,7 +388,9 @@ String Editor::get_line(const String& prompt) ++m_times_tab_pressed; m_last_shown_suggestion_was_complete = false; } else { - insert(m_last_shown_suggestion.substring_view(m_next_suggestion_invariant_offset, m_last_shown_suggestion.length() - m_next_suggestion_invariant_offset)); + insert(m_last_shown_suggestion.text.substring_view(m_next_suggestion_invariant_offset, m_last_shown_suggestion.text.length() - m_next_suggestion_invariant_offset)); + // add in the trivia of the last selected suggestion + insert(m_last_shown_suggestion.trailing_trivia); if (m_tab_direction == TabDirection::Forward) increment_suggestion_index(); else @@ -399,7 +404,7 @@ String Editor::get_line(const String& prompt) size_t longest_suggestion_length = 0; for (auto& suggestion : m_suggestions) { - longest_suggestion_length = max(longest_suggestion_length, suggestion.length()); + longest_suggestion_length = max(longest_suggestion_length, suggestion.text.length()); } size_t num_printed = 0; @@ -423,10 +428,10 @@ String Editor::get_line(const String& prompt) } vt_move_absolute(max_line_count + m_origin_x, 1); for (auto& suggestion : m_suggestions) { - size_t next_column = num_printed + suggestion.length() + longest_suggestion_length + 2; + size_t next_column = num_printed + suggestion.text.length() + longest_suggestion_length + 2; if (next_column > m_num_columns) { - auto lines = (suggestion.length() + m_num_columns - 1) / m_num_columns; + auto lines = (suggestion.text.length() + m_num_columns - 1) / m_num_columns; lines_used += lines; putchar('\n'); num_printed = 0; @@ -445,9 +450,9 @@ String Editor::get_line(const String& prompt) if (spans_entire_line) { num_printed += m_num_columns; - fprintf(stderr, "%s", suggestion.characters()); + fprintf(stderr, "%s", suggestion.text.characters()); } else { - num_printed += fprintf(stderr, "%-*s", static_cast(longest_suggestion_length) + 2, suggestion.characters()); + num_printed += fprintf(stderr, "%-*s", static_cast(longest_suggestion_length) + 2, suggestion.text.characters()); } if (m_last_shown_suggestion_was_complete && index == current_suggestion_index) { diff --git a/Libraries/LibLine/Editor.h b/Libraries/LibLine/Editor.h index f5085cfcbd..ce7813d31a 100644 --- a/Libraries/LibLine/Editor.h +++ b/Libraries/LibLine/Editor.h @@ -53,6 +53,28 @@ struct KeyCallback { Function callback; }; +struct CompletionSuggestion { + // intentionally not explicit (allows suggesting bare strings) + CompletionSuggestion(const String& completion) + : text(completion) + , trailing_trivia("") + { + } + CompletionSuggestion(const StringView& completion, const StringView& trailing_trivia) + : text(completion) + , trailing_trivia(trailing_trivia) + { + } + + bool operator==(const CompletionSuggestion& suggestion) const + { + return suggestion.text == text; + } + + String text; + String trailing_trivia; +}; + class Editor { public: Editor(); @@ -79,8 +101,8 @@ public: void register_character_input_callback(char ch, Function callback); - Function(const String&)> on_tab_complete_first_token; - Function(const String&)> on_tab_complete_other_token; + Function(const String&)> on_tab_complete_first_token; + Function(const String&)> on_tab_complete_other_token; Function on_display_refresh; // FIXME: we will have to kindly ask our instantiators to set our signal handlers @@ -195,8 +217,8 @@ private: size_t m_origin_y { 0 }; String m_new_prompt; - Vector m_suggestions; - String m_last_shown_suggestion { String::empty() }; + Vector m_suggestions; + CompletionSuggestion m_last_shown_suggestion { String::empty() }; size_t m_last_shown_suggestion_display_length { 0 }; bool m_last_shown_suggestion_was_complete { false }; size_t m_next_suggestion_index { 0 }; diff --git a/Shell/main.cpp b/Shell/main.cpp index 576b1e373c..0e70e5a14b 100644 --- a/Shell/main.cpp +++ b/Shell/main.cpp @@ -1042,7 +1042,7 @@ int main(int argc, char** argv) g.termios = editor.termios(); g.default_termios = editor.default_termios(); - editor.on_tab_complete_first_token = [&](const String& token) -> Vector { + editor.on_tab_complete_first_token = [&](const String& token) -> Vector { auto match = binary_search(cached_path.data(), cached_path.size(), token, [](const String& token, const String& program) -> int { return strncmp(token.characters(), program.characters(), token.length()); }); @@ -1052,7 +1052,7 @@ int main(int argc, char** argv) // Suggest local executables and directories auto mut_token = token; // copy it :( String path; - Vector local_suggestions; + Vector local_suggestions; bool suggest_executables = true; ssize_t last_slash = token.length() - 1; @@ -1090,6 +1090,7 @@ int main(int argc, char** argv) // manually skip `.' and `..' if (file == "." || file == "..") continue; + auto trivia = " "; if (file.starts_with(mut_token)) { String file_path = String::format("%s/%s", path.characters(), file.characters()); struct stat program_status; @@ -1098,25 +1099,14 @@ int main(int argc, char** argv) continue; if (!(program_status.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH))) continue; - if (!S_ISDIR(program_status.st_mode) && !suggest_executables) - continue; + if (S_ISDIR(program_status.st_mode)) { + if (suggest_executables) + continue; + else + trivia = "/"; + } - local_suggestions.append(file); - } - } - - // If we have a single match and it's a directory, we add a slash. If it's - // a regular file, we add a space, unless we already have one. - if (local_suggestions.size() == 1) { - auto& completion = local_suggestions[0]; - String file_path = String::format("%s/%s", path.characters(), completion.characters()); - struct stat program_status; - int stat_error = stat(file_path.characters(), &program_status); - if (!stat_error) { - if (S_ISDIR(program_status.st_mode)) - completion = String::format("%s/", local_suggestions[0].characters()); - else if (editor.cursor() == editor.buffer().size() || editor.buffer_at(editor.cursor()) != ' ') - completion = String::format("%s ", local_suggestions[0].characters()); + local_suggestions.append({ file, trivia }); } } @@ -1124,37 +1114,29 @@ int main(int argc, char** argv) } String completion = *match; - Vector suggestions; + Vector suggestions; // Now that we have a program name starting with our token, we look at // other program names starting with our token and cut off any mismatching // characters. - bool seen_others = false; int index = match - cached_path.data(); for (int i = index - 1; i >= 0 && cached_path[i].starts_with(token); --i) { - suggestions.append(cached_path[i]); - seen_others = true; + suggestions.append({ cached_path[i], " " }); } for (size_t i = index + 1; i < cached_path.size() && cached_path[i].starts_with(token); ++i) { - suggestions.append(cached_path[i]); - seen_others = true; - } - suggestions.append(cached_path[index]); - - // If we have a single match, we add a space, unless we already have one. - if (!seen_others && (editor.cursor() == editor.buffer().size() || editor.buffer_at(editor.cursor()) != ' ')) { - suggestions[0] = String::format("%s ", suggestions[0].characters()); + suggestions.append({ cached_path[i], " " }); } + suggestions.append({ cached_path[index], " " }); editor.suggest(token.length(), 0); return suggestions; }; - editor.on_tab_complete_other_token = [&](const String& vtoken) -> Vector { + editor.on_tab_complete_other_token = [&](const String& vtoken) -> Vector { auto token = vtoken; // copy it :( String path; - Vector suggestions; + Vector suggestions; ssize_t last_slash = token.length() - 1; while (last_slash >= 0 && token[last_slash] != '/') @@ -1190,22 +1172,15 @@ int main(int argc, char** argv) if (file == "." || file == "..") continue; if (file.starts_with(token)) { - suggestions.append(file); - } - } - - // If we have a single match and it's a directory, we add a slash. If it's - // a regular file, we add a space, unless we already have one. - if (suggestions.size() == 1) { - auto& completion = suggestions[0]; - String file_path = String::format("%s/%s", path.characters(), completion.characters()); - struct stat program_status; - int stat_error = stat(file_path.characters(), &program_status); - if (!stat_error) { - if (S_ISDIR(program_status.st_mode)) - completion = String::format("%s/", suggestions[0].characters()); - else if (editor.cursor() == editor.buffer().size() || editor.buffer_at(editor.cursor()) != ' ') - completion = String::format("%s ", suggestions[0].characters()); + struct stat program_status; + String file_path = String::format("%s/%s", path.characters(), file.characters()); + int stat_error = stat(file_path.characters(), &program_status); + if (!stat_error) { + if (S_ISDIR(program_status.st_mode)) + suggestions.append({ file, "/" }); + else + suggestions.append({ file, " " }); + } } } diff --git a/Userland/js.cpp b/Userland/js.cpp index 06e11ac6de..0806089381 100644 --- a/Userland/js.cpp +++ b/Userland/js.cpp @@ -553,7 +553,7 @@ int main(int argc, char** argv) editor.set_prompt(prompt_for_level(open_indents)); }; - auto complete = [&interpreter, &editor = *editor](const String& token) -> Vector { + auto complete = [&interpreter, &editor = *editor](const String& token) -> Vector { if (token.length() == 0) return {}; // nyeh @@ -564,13 +564,13 @@ int main(int argc, char** argv) // - .

// where N is the complete name of a variable and // P is part of the name of one of its properties - Vector results; + Vector results; Function list_all_properties = [&results, &list_all_properties](const JS::Shape& shape, auto& property_pattern) { for (const auto& descriptor : shape.property_table()) { if (descriptor.value.attributes & JS::Attribute::Enumerable) { if (descriptor.key.view().starts_with(property_pattern)) { - auto completion = descriptor.key; + Line::CompletionSuggestion completion { descriptor.key }; if (!results.contains_slow(completion)) { // hide duplicates results.append(completion); }