From aa8b40dce68eedaf1816ca9d1d1869c50c7bcab4 Mon Sep 17 00:00:00 2001 From: William McPherson Date: Tue, 3 Dec 2019 17:36:46 +1100 Subject: [PATCH] Shell: Cache PATH for faster tab completion This patch reduces the O(n) tab completion to something like O(log(n)). The cache is just a sorted vector of strings and we binary search it to get a string matching our input, and then check the surrounding strings to see if we need to remove any characters. Also we no longer stat each file every time. Also added an #include in BinarySearch since it was using size_t. Oops. If `export` is called, we recache. Need to implement the `hash` builtin for when an executable has been added to a directory in PATH. --- AK/BinarySearch.h | 2 ++ Shell/LineEditor.cpp | 70 ++++++++++++++++++++++++++------------------ Shell/LineEditor.h | 7 +++++ Shell/main.cpp | 9 +++++- 4 files changed, 58 insertions(+), 30 deletions(-) diff --git a/AK/BinarySearch.h b/AK/BinarySearch.h index 16cd9784dc5a2e..c3f00ce04db432 100644 --- a/AK/BinarySearch.h +++ b/AK/BinarySearch.h @@ -1,5 +1,7 @@ #pragma once +#include + namespace AK { template diff --git a/Shell/LineEditor.cpp b/Shell/LineEditor.cpp index 053c6230d94136..7a7f404f552936 100644 --- a/Shell/LineEditor.cpp +++ b/Shell/LineEditor.cpp @@ -37,58 +37,70 @@ void LineEditor::append(const String& string) m_cursor = m_buffer.size(); } -void LineEditor::tab_complete_first_token() +void LineEditor::cache_path() { - auto input = String::copy(m_buffer); + if (!m_path.is_empty()) + m_path.clear_with_capacity(); String path = getenv("PATH"); if (path.is_empty()) return; - auto directories = path.split(':'); - - String match; - // Go through the files in PATH. + auto directories = path.split(':'); for (const auto& directory : directories) { CDirIterator programs(directory.characters(), CDirIterator::SkipDots); while (programs.has_next()) { - String program = programs.next_path(); - if (!program.starts_with(input)) - continue; + auto program = programs.next_path(); - // Check that the file is an executable program. - struct stat program_status; StringBuilder program_path; program_path.append(directory.characters()); program_path.append('/'); program_path.append(program.characters()); + struct stat program_status; int stat_error = stat(program_path.to_string().characters(), &program_status); if (stat_error || !(program_status.st_mode & S_IXUSR)) continue; - // Set `match` to the first one that starts with `input`. - if (match.is_empty()) { - match = program; - } else { - // Remove characters from the end of `match` if they're - // different from another `program` starting with `input`. - int i = input.length(); - while (i < match.length() && i < program.length() && match[i] == program[i]) - ++i; - match = match.substring(0, i); - } - - if (match.length() == input.length()) - return; + m_path.append(program.characters()); } } - if (match.is_empty()) + quick_sort(m_path.begin(), m_path.end(), AK::is_less_than); +} + +void LineEditor::cut_mismatching_chars(String& completion, const String& program, int token_length) +{ + int i = token_length; + while (i < completion.length() && i < program.length() && completion[i] == program[i]) + ++i; + completion = completion.substring(0, i); +} + +void LineEditor::tab_complete_first_token() +{ + String token = String::copy(m_buffer); + + 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; - // Then append `match` to the buffer, excluding the `input` part which is - // already in the buffer. - append(match.substring(input.length(), match.length() - input.length()).characters()); + String completion = *match; + + // 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. + + int index = match - m_path.data(); + for (int i = index - 1; i >= 0 && m_path[i].starts_with(token); --i) + cut_mismatching_chars(completion, m_path[i], token.length()); + 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()); + + if (token.length() == completion.length()) + return; + append(completion.substring(token.length(), completion.length() - token.length()).characters()); } String LineEditor::get_line(const String& prompt) diff --git a/Shell/LineEditor.h b/Shell/LineEditor.h index b2a5b1c6dc219c..3c2f64d0d89c02 100644 --- a/Shell/LineEditor.h +++ b/Shell/LineEditor.h @@ -1,5 +1,7 @@ #pragma once +#include +#include #include #include #include @@ -16,9 +18,12 @@ class LineEditor { void add_to_history(const String&); const Vector& history() const { return m_history; } + void cache_path(); + private: void clear_line(); void append(const String&); + void cut_mismatching_chars(String& completion, const String& program, int token_length); void tab_complete_first_token(); void vt_save_cursor(); void vt_restore_cursor(); @@ -32,6 +37,8 @@ class LineEditor { int m_history_cursor { 0 }; int m_history_capacity { 100 }; + Vector m_path; + enum class InputState { Free, ExpectBracket, diff --git a/Shell/main.cpp b/Shell/main.cpp index 100fdfc889a043..43765eb11ce7ae 100644 --- a/Shell/main.cpp +++ b/Shell/main.cpp @@ -64,7 +64,12 @@ static int sh_export(int argc, char** argv) return 1; } - return setenv(parts[0].characters(), parts[1].characters(), 1); + int setenv_return = setenv(parts[0].characters(), parts[1].characters(), 1); + + if (setenv_return == 0 && parts[0] == "PATH") + editor.cache_path(); + + return setenv_return; } static int sh_unset(int argc, char** argv) @@ -910,6 +915,8 @@ int main(int argc, char** argv) load_history(); atexit(save_history); + editor.cache_path(); + for (;;) { auto line = editor.get_line(prompt()); if (line.is_empty())