diff --git a/Shell/LineEditor.cpp b/Shell/LineEditor.cpp index 5e36ca16c5..ebfaf10150 100644 --- a/Shell/LineEditor.cpp +++ b/Shell/LineEditor.cpp @@ -37,6 +37,60 @@ void LineEditor::append(const String& string) m_cursor = m_buffer.size(); } +void LineEditor::tab_complete_first_token() +{ + auto input = String::copy(m_buffer); + + String path = getenv("PATH"); + if (path.is_empty()) + return; + auto directories = path.split(':'); + + String match; + + // Go through the files in PATH. + 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; + + // 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()); + 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; + } + } + + if (match.is_empty()) + 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 LineEditor::get_line(const String& prompt) { fputs(prompt.characters(), stdout); @@ -174,7 +228,21 @@ String LineEditor::get_line(const String& prompt) } if (ch == '\t') { - // FIXME: Implement tab-completion. + if (m_buffer.is_empty()) + continue; + + bool is_first_token = true; + for (const auto& character : m_buffer) { + if (isspace(character)) { + is_first_token = false; + break; + } + } + + // FIXME: Implement tab-completion for other tokens (paths). + if (is_first_token) + tab_complete_first_token(); + continue; } diff --git a/Shell/LineEditor.h b/Shell/LineEditor.h index 5f5a4344d7..b2a5b1c6dc 100644 --- a/Shell/LineEditor.h +++ b/Shell/LineEditor.h @@ -1,7 +1,10 @@ #pragma once #include +#include #include +#include +#include class LineEditor { public: @@ -16,6 +19,7 @@ public: private: void clear_line(); void append(const String&); + void tab_complete_first_token(); void vt_save_cursor(); void vt_restore_cursor(); void vt_clear_to_end_of_line();