diff --git a/Shell/LineEditor.cpp b/Shell/LineEditor.cpp index dac63a4d65..c3e28d02cf 100644 --- a/Shell/LineEditor.cpp +++ b/Shell/LineEditor.cpp @@ -2,10 +2,15 @@ #include "GlobalState.h" #include #include +#include #include LineEditor::LineEditor() { + struct winsize ws; + int rc = ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws); + ASSERT(rc == 0); + m_num_columns = ws.ws_col; } LineEditor::~LineEditor() @@ -106,15 +111,18 @@ void LineEditor::cut_mismatching_chars(String& completion, const String& other, completion = completion.substring(0, i); } -void LineEditor::tab_complete_first_token(const String& token) +// Function returns Vector as assignment is made from return value at callsite +// (instead of StringView) +Vector LineEditor::tab_complete_first_token(const String& token) { auto match = binary_search(m_path.data(), m_path.size(), token, [](const String& token, const String& program) -> int { return strncmp(token.characters(), program.characters(), token.length()); }); if (!match) - return; + return Vector(); String completion = *match; + 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 @@ -123,13 +131,16 @@ void LineEditor::tab_complete_first_token(const String& token) bool seen_others = false; int index = match - m_path.data(); for (int i = index - 1; i >= 0 && m_path[i].starts_with(token); --i) { + suggestions.append(m_path[i]); cut_mismatching_chars(completion, m_path[i], token.length()); seen_others = true; } for (int i = index + 1; i < m_path.size() && m_path[i].starts_with(token); ++i) { cut_mismatching_chars(completion, m_path[i], token.length()); + suggestions.append(m_path[i]); seen_others = true; } + suggestions.append(m_path[index]); // If we have characters to add, add them. if (completion.length() > token.length()) @@ -137,11 +148,14 @@ void LineEditor::tab_complete_first_token(const String& token) // If we have a single match, we add a space, unless we already have one. if (!seen_others && (m_cursor == (size_t)m_buffer.size() || m_buffer[(int)m_cursor] != ' ')) insert(' '); + + return suggestions; } -void LineEditor::tab_complete_other_token(String& token) +Vector LineEditor::tab_complete_other_token(String& token) { String path; + Vector suggestions; int last_slash = (int)token.length() - 1; while (last_slash >= 0 && token[last_slash] != '/') @@ -161,6 +175,17 @@ void LineEditor::tab_complete_other_token(String& token) path = g.cwd; } + // This is a bit naughty, but necessary without reordering the loop + // below. The loop terminates early, meaning that + // the suggestions list is incomplete. + // We only do this if the token is empty though. + if (token.is_empty()) { + CDirIterator suggested_files(path, CDirIterator::SkipDots); + while (suggested_files.has_next()) { + suggestions.append(suggested_files.next_path()); + } + } + String completion; bool seen_others = false; @@ -168,18 +193,20 @@ void LineEditor::tab_complete_other_token(String& token) while (files.has_next()) { auto file = files.next_path(); if (file.starts_with(token)) { + if (!token.is_empty()) + suggestions.append(file); if (completion.is_empty()) { completion = file; // Will only be set once. } else { cut_mismatching_chars(completion, file, token.length()); if (completion.is_empty()) // We cut everything off! - return; + return suggestions; seen_others = true; } } } if (completion.is_empty()) - return; + return suggestions; // If we have characters to add, add them. if (completion.length() > token.length()) @@ -197,6 +224,8 @@ void LineEditor::tab_complete_other_token(String& token) insert(' '); } } + + return {}; // Return an empty vector } String LineEditor::get_line(const String& prompt) @@ -223,6 +252,12 @@ String LineEditor::get_line(const String& prompt) g.was_resized = false; printf("\033[2K\r"); m_buffer.clear(); + + struct winsize ws; + int rc = ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws); + ASSERT(rc == 0); + m_num_columns = ws.ws_col; + return String::empty(); } m_buffer.clear(); @@ -337,6 +372,7 @@ String LineEditor::get_line(const String& prompt) if (ch == '\t') { bool is_empty_token = m_cursor == 0 || m_buffer[(int)m_cursor - 1] == ' '; + m_times_tab_pressed++; int token_start = (int)m_cursor - 1; if (!is_empty_token) { @@ -354,15 +390,46 @@ String LineEditor::get_line(const String& prompt) } String token = is_empty_token ? String() : String(&m_buffer[token_start], m_cursor - (size_t)token_start); + Vector suggestions; if (is_first_token) - tab_complete_first_token(token); + suggestions = tab_complete_first_token(token); else - tab_complete_other_token(token); + suggestions = tab_complete_other_token(token); + if (m_times_tab_pressed > 1 && !suggestions.is_empty()) { + size_t longest_suggestion_length = 0; + + for (auto& suggestion : suggestions) + longest_suggestion_length = max(longest_suggestion_length, suggestion.length()); + + size_t num_printed = 0; + putchar('\n'); + for (auto& suggestion : suggestions) { + int next_column = num_printed + suggestion.length() + longest_suggestion_length + 2; + + if (next_column > m_num_columns) { + putchar('\n'); + num_printed = 0; + } + + num_printed += fprintf(stderr, "%-*s", static_cast(longest_suggestion_length) + 2, suggestion.characters()); + } + + putchar('\n'); + write(STDOUT_FILENO, prompt.characters(), prompt.length()); + write(STDOUT_FILENO, m_buffer.data(), m_cursor); + // Prevent not printing characters in case the user has moved the cursor and then pressed tab + write(STDOUT_FILENO, m_buffer.data() + m_cursor, m_buffer.size() - m_cursor); + m_cursor = m_buffer.size(); // bash doesn't do this, but it makes a little bit more sense + } + + suggestions.clear_with_capacity(); continue; } + m_times_tab_pressed = 0; // Safe to say if we get here, the user didn't press TAB + auto do_backspace = [&] { if (m_cursor == 0) { fputc('\a', stdout); @@ -401,7 +468,7 @@ String LineEditor::get_line(const String& prompt) do_backspace(); continue; } - if (ch == 0xc) { // ^L + if (ch == 0xc) { // ^L printf("\033[3J\033[H\033[2J"); // Clear screen. fputs(prompt.characters(), stdout); for (int i = 0; i < m_buffer.size(); ++i) diff --git a/Shell/LineEditor.h b/Shell/LineEditor.h index 1158d35e1b..f11e24cc3a 100644 --- a/Shell/LineEditor.h +++ b/Shell/LineEditor.h @@ -25,14 +25,16 @@ private: void insert(const String&); void insert(const char); void cut_mismatching_chars(String& completion, const String& other, size_t start_compare); - void tab_complete_first_token(const String&); - void tab_complete_other_token(String&); + Vector tab_complete_first_token(const String&); + Vector tab_complete_other_token(String&); void vt_save_cursor(); void vt_restore_cursor(); void vt_clear_to_end_of_line(); Vector m_buffer; size_t m_cursor { 0 }; + int m_times_tab_pressed { 0 }; + int m_num_columns { 0 }; // FIXME: This should be something more take_first()-friendly. Vector m_history;