diff --git a/Libraries/LibLine/Editor.cpp b/Libraries/LibLine/Editor.cpp index ea402e4b9d..15ee4c9b04 100644 --- a/Libraries/LibLine/Editor.cpp +++ b/Libraries/LibLine/Editor.cpp @@ -280,25 +280,43 @@ String Editor::get_line(const String& prompt) } String token = is_empty_token ? String() : String(&m_buffer[token_start], m_cursor - token_start); - Vector suggestions; - if (is_first_token) - suggestions = on_tab_complete_first_token(token); - else - suggestions = on_tab_complete_other_token(token); + // ask for completions only on the first tab + // further tabs simply show the cached completions + if (m_times_tab_pressed == 1) { + if (is_first_token) + m_suggestions = on_tab_complete_first_token(token); + else + m_suggestions = on_tab_complete_other_token(token); + } - if (m_times_tab_pressed > 1 && !suggestions.is_empty()) { + auto current_suggestion_index = m_next_suggestion_index; + if (m_next_suggestion_index < m_suggestions.size()) { + if (!m_last_shown_suggestion.is_null()) { + auto actual_offset = m_times_tab_pressed == 1 ? m_cursor : m_cursor - m_last_shown_suggestion.length() + m_next_suggestion_invariant_offset; + for (size_t i = m_next_suggestion_invariant_offset; i < m_last_shown_suggestion.length(); ++i) + m_buffer.remove(actual_offset); + m_cursor = actual_offset; + m_refresh_needed = true; + } + m_last_shown_suggestion = m_suggestions[m_next_suggestion_index]; + insert(m_last_shown_suggestion.substring_view(m_next_suggestion_invariant_offset, m_last_shown_suggestion.length() - m_next_suggestion_invariant_offset)); + m_next_suggestion_index = (m_next_suggestion_index + 1) % m_suggestions.size(); + } else { + m_next_suggestion_index = 0; + } + + if (m_times_tab_pressed > 1 && !m_suggestions.is_empty()) { size_t longest_suggestion_length = 0; - for (auto& suggestion : suggestions) + for (auto& suggestion : m_suggestions) longest_suggestion_length = max(longest_suggestion_length, suggestion.length()); size_t num_printed = 0; size_t lines_used { 1 }; + size_t index { 0 }; putchar('\n'); - // FIXME: what if we use more lines than the terminal has? - // this would put the actual prompt out of view - for (auto& suggestion : suggestions) { + for (auto& suggestion : m_suggestions) { size_t next_column = num_printed + suggestion.length() + longest_suggestion_length + 2; if (next_column > m_num_columns) { @@ -307,9 +325,25 @@ String Editor::get_line(const String& prompt) num_printed = 0; } + // show just enough suggestions to fill up the screen + // without moving the prompt out of view + if (lines_used + num_lines() >= m_num_lines) + break; + + if (index == current_suggestion_index) { + vt_apply_style({ Style::Foreground(Style::Color::Blue) }); + fflush(stdout); + } + num_printed += fprintf(stderr, "%-*s", static_cast(longest_suggestion_length) + 2, suggestion.characters()); - m_lines_used_for_last_suggestions = lines_used; + + if (index == current_suggestion_index) { + vt_apply_style({}); + fflush(stdout); + } + ++index; } + m_lines_used_for_last_suggestions = lines_used; // adjust for the case that we scroll up after writing the suggestions if (m_origin_x + lines_used >= m_num_lines) { @@ -318,7 +352,6 @@ String Editor::get_line(const String& prompt) reposition_cursor(); } - suggestions.clear_with_capacity(); continue; } @@ -327,10 +360,13 @@ String Editor::get_line(const String& prompt) // let's clean them up if (m_lines_used_for_last_suggestions) { vt_clear_lines(0, m_lines_used_for_last_suggestions); - vt_move_relative(-m_lines_used_for_last_suggestions, 0); + reposition_cursor(); m_refresh_needed = true; m_lines_used_for_last_suggestions = 0; } + m_last_shown_suggestion = String::empty(); + m_suggestions.clear(); + suggest(0, 0); } m_times_tab_pressed = 0; // Safe to say if we get here, the user didn't press TAB diff --git a/Libraries/LibLine/Editor.h b/Libraries/LibLine/Editor.h index 6a6c4a000c..187ae7ac09 100644 --- a/Libraries/LibLine/Editor.h +++ b/Libraries/LibLine/Editor.h @@ -113,6 +113,11 @@ public: m_spans_ending.clear(); m_refresh_needed = true; } + void suggest(size_t invariant_offset = 0, size_t index = 0) + { + m_next_suggestion_index = index; + m_next_suggestion_invariant_offset = invariant_offset; + } const struct termios& termios() const { return m_termios; } const struct termios& default_termios() const { return m_default_termios; } @@ -190,6 +195,10 @@ private: size_t m_origin_y { 0 }; String m_new_prompt; + Vector m_suggestions; + String m_last_shown_suggestion; + size_t m_next_suggestion_index { 0 }; + size_t m_next_suggestion_invariant_offset { 0 }; HashMap> m_key_callbacks; diff --git a/Shell/main.cpp b/Shell/main.cpp index dbcb3c45be..fa8f1a25a6 100644 --- a/Shell/main.cpp +++ b/Shell/main.cpp @@ -1041,7 +1041,7 @@ int main(int argc, char** argv) return strncmp(token.characters(), program.characters(), token.length()); }); if (!match) - return Vector(); + return {}; String completion = *match; Vector suggestions; @@ -1054,22 +1054,24 @@ int main(int argc, char** argv) 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]); - editor.cut_mismatching_chars(completion, cached_path[i], token.length()); seen_others = true; } for (size_t i = index + 1; i < cached_path.size() && cached_path[i].starts_with(token); ++i) { - editor.cut_mismatching_chars(completion, cached_path[i], token.length()); suggestions.append(cached_path[i]); seen_others = true; } suggestions.append(cached_path[index]); - // If we have characters to add, add them. - if (completion.length() > token.length()) - editor.insert(completion.substring(token.length(), completion.length() - token.length())); // 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()) != ' ')) - editor.insert(' '); + if (!seen_others && (editor.cursor() == editor.buffer().size() || editor.buffer_at(editor.cursor()) != ' ')) { + suggestions[0] = String::format("%s ", suggestions[0].characters()); + } + + dbg() << "found " << suggestions.size() << " elements"; + for (auto& el : suggestions) + dbg() << ">>> '" << el << "'"; + + editor.suggest(token.length(), 0); return suggestions; }; @@ -1078,6 +1080,7 @@ int main(int argc, char** argv) String path; Vector suggestions; + editor.suggest(token.length(), 0); ssize_t last_slash = token.length() - 1; while (last_slash >= 0 && token[last_slash] != '/') --last_slash; diff --git a/Userland/js.cpp b/Userland/js.cpp index e1324e5b41..e06ea0d0bb 100644 --- a/Userland/js.cpp +++ b/Userland/js.cpp @@ -544,6 +544,9 @@ int main(int argc, char** argv) }; auto complete = [&interpreter, &editor = *editor](const String& token) -> Vector { + if (token.length() == 0) + return {}; // nyeh + StringView line { editor.buffer().data(), editor.cursor() }; // we're only going to complete either // - @@ -556,23 +559,18 @@ int main(int argc, char** argv) 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)) - if (!results.contains_slow(descriptor.key)) // hide duplicates - results.append(descriptor.key); + if (descriptor.key.view().starts_with(property_pattern)) { + auto completion = descriptor.key; + if (!results.contains_slow(completion)) { // hide duplicates + results.append(completion); + } + } } } if (const auto* prototype = shape.prototype()) { list_all_properties(prototype->shape(), property_pattern); } }; - auto complete = [&results, &editor](auto& property_pattern) { - if (results.size()) { - auto completion = results.first().view(); - completion = completion.substring_view(property_pattern.length(), completion.length() - property_pattern.length()); - if (completion.length()) - editor.insert(completion); - } - }; if (token.contains(".")) { auto parts = token.split('.', true); @@ -597,12 +595,14 @@ int main(int argc, char** argv) const auto* object = variable.to_object(interpreter->heap()); const auto& shape = object->shape(); list_all_properties(shape, property_pattern); - complete(property_pattern); + if (results.size()) + editor.suggest(property_pattern.length()); return results; } const auto& variable = interpreter->global_object(); list_all_properties(variable.shape(), token); - complete(token); + if (results.size()) + editor.suggest(token.length()); return results; }; editor->on_tab_complete_first_token = [complete](auto& value) { return complete(value); };