1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-25 02:07:34 +00:00

Shell: Highlight commands with a hyperlink to open their help pages

This commit is contained in:
ForLoveOfCats 2022-04-20 15:36:00 -04:00 committed by Ali Mohammad Pur
parent 85152d2f7f
commit 99c0b895fe
4 changed files with 142 additions and 27 deletions

View file

@ -620,8 +620,22 @@ RefPtr<Value> BarewordLiteral::run(RefPtr<Shell>)
void BarewordLiteral::highlight_in_editor(Line::Editor& editor, Shell& shell, HighlightMetadata metadata) void BarewordLiteral::highlight_in_editor(Line::Editor& editor, Shell& shell, HighlightMetadata metadata)
{ {
if (metadata.is_first_in_list) { if (metadata.is_first_in_list) {
if (shell.is_runnable(m_text)) { auto runnable = shell.runnable_path_for(m_text);
editor.stylize({ m_position.start_offset, m_position.end_offset }, { Line::Style::Bold }); if (runnable.has_value()) {
Line::Style bold = { Line::Style::Bold };
Line::Style style = bold;
#ifdef __serenity__
if (runnable->kind == Shell::RunnablePath::Kind::Executable || runnable->kind == Shell::RunnablePath::Kind::Alias) {
auto name = shell.help_path_for({}, *runnable);
if (name.has_value()) {
auto url = URL::create_with_help_scheme(name.release_value(), shell.hostname);
style = bold.unified_with(Line::Style::Hyperlink(url.to_string()));
}
}
#endif
editor.stylize({ m_position.start_offset, m_position.end_offset }, style);
} else { } else {
editor.stylize({ m_position.start_offset, m_position.end_offset }, { Line::Style::Foreground(Line::Style::XtermColor::Red) }); editor.stylize({ m_position.start_offset, m_position.end_offset }, { Line::Style::Foreground(Line::Style::XtermColor::Red) });
} }

View file

@ -67,7 +67,7 @@ int Shell::builtin_alias(int argc, char const** argv)
} }
} else { } else {
m_aliases.set(parts[0], parts[1]); m_aliases.set(parts[0], parts[1]);
add_entry_to_cache(parts[0]); add_entry_to_cache({ RunnablePath::Kind::Alias, parts[0] });
} }
} }

View file

@ -421,7 +421,7 @@ void Shell::unset_local_variable(StringView name, bool only_in_current_frame)
void Shell::define_function(String name, Vector<String> argnames, RefPtr<AST::Node> body) void Shell::define_function(String name, Vector<String> argnames, RefPtr<AST::Node> body)
{ {
add_entry_to_cache(name); add_entry_to_cache({ RunnablePath::Kind::Function, name });
m_functions.set(name, { name, move(argnames), move(body) }); m_functions.set(name, { name, move(argnames), move(body) });
} }
@ -517,14 +517,47 @@ String Shell::resolve_alias(StringView name) const
return m_aliases.get(name).value_or({}); return m_aliases.get(name).value_or({});
} }
bool Shell::is_runnable(StringView name) Optional<Shell::RunnablePath> Shell::runnable_path_for(StringView name)
{ {
auto parts = name.split_view('/'); auto parts = name.split_view('/');
auto path = name.to_string(); auto path = name.to_string();
if (parts.size() > 1 && access(path.characters(), X_OK) == 0) if (parts.size() > 1) {
return true; auto file = Core::File::open(path.characters(), Core::OpenMode::ReadOnly);
if (!file.is_error() && !file.value()->is_directory() && access(path.characters(), X_OK) == 0)
return RunnablePath { RunnablePath::Kind::Executable, name };
}
return binary_search(cached_path.span(), path, nullptr); auto* found = binary_search(cached_path.span(), path, nullptr, RunnablePathComparator {});
if (!found)
return {};
return *found;
}
Optional<String> Shell::help_path_for(Vector<RunnablePath> visited, Shell::RunnablePath const& runnable_path)
{
switch (runnable_path.kind) {
case RunnablePath::Kind::Executable: {
LexicalPath lexical_path(runnable_path.path);
return lexical_path.basename();
}
case RunnablePath::Kind::Alias: {
if (visited.contains_slow(runnable_path))
return {}; // Break out of an alias loop
auto resolved = resolve_alias(runnable_path.path);
auto* runnable = binary_search(cached_path.span(), resolved, nullptr, RunnablePathComparator {});
if (!runnable)
return {};
visited.append(runnable_path);
return help_path_for(visited, *runnable);
}
default:
return {};
}
} }
int Shell::run_command(StringView cmd, Optional<SourcePosition> source_position_override) int Shell::run_command(StringView cmd, Optional<SourcePosition> source_position_override)
@ -1336,14 +1369,14 @@ void Shell::cache_path()
// Add shell builtins to the cache. // Add shell builtins to the cache.
for (auto const& builtin_name : builtin_names) for (auto const& builtin_name : builtin_names)
cached_path.append(escape_token(builtin_name)); cached_path.append({ RunnablePath::Kind::Builtin, escape_token(builtin_name) });
// Add functions to the cache. // Add functions to the cache.
for (auto& function : m_functions) { for (auto& function : m_functions) {
auto name = escape_token(function.key); auto name = escape_token(function.key);
if (cached_path.contains_slow(name)) if (cached_path.contains_slow(name))
continue; continue;
cached_path.append(name); cached_path.append({ RunnablePath::Kind::Function, name });
} }
// Add aliases to the cache. // Add aliases to the cache.
@ -1351,7 +1384,7 @@ void Shell::cache_path()
auto name = escape_token(alias.key); auto name = escape_token(alias.key);
if (cached_path.contains_slow(name)) if (cached_path.contains_slow(name))
continue; continue;
cached_path.append(name); cached_path.append({ RunnablePath::Kind::Alias, name });
} }
String path = getenv("PATH"); String path = getenv("PATH");
@ -1366,7 +1399,7 @@ void Shell::cache_path()
if (cached_path.contains_slow(escaped_name)) if (cached_path.contains_slow(escaped_name))
continue; continue;
if (access(program_path.characters(), X_OK) == 0) if (access(program_path.characters(), X_OK) == 0)
cached_path.append(escaped_name); cached_path.append({ RunnablePath::Kind::Executable, escaped_name });
} }
} }
} }
@ -1374,15 +1407,15 @@ void Shell::cache_path()
quick_sort(cached_path); quick_sort(cached_path);
} }
void Shell::add_entry_to_cache(String const& entry) void Shell::add_entry_to_cache(RunnablePath const& entry)
{ {
size_t index = 0; size_t index = 0;
auto match = binary_search(cached_path.span(), entry, &index); auto match = binary_search(cached_path.span(), entry, &index, RunnablePathComparator {});
if (match) if (match)
return; return;
while (index < cached_path.size() && strcmp(cached_path[index].characters(), entry.characters()) < 0) { while (index < cached_path.size() && strcmp(cached_path[index].path.characters(), entry.path.characters()) < 0) {
index++; index++;
} }
cached_path.insert(index, entry); cached_path.insert(index, entry);
@ -1391,7 +1424,7 @@ void Shell::add_entry_to_cache(String const& entry)
void Shell::remove_entry_from_cache(StringView entry) void Shell::remove_entry_from_cache(StringView entry)
{ {
size_t index { 0 }; size_t index { 0 };
auto match = binary_search(cached_path.span(), entry, &index); auto match = binary_search(cached_path.span(), entry, &index, RunnablePathComparator {});
if (match) if (match)
cached_path.remove(index); cached_path.remove(index);
@ -1518,14 +1551,14 @@ Vector<Line::CompletionSuggestion> Shell::complete_program_name(StringView name,
[](auto& name, auto& program) { [](auto& name, auto& program) {
return strncmp( return strncmp(
name.characters_without_null_termination(), name.characters_without_null_termination(),
program.characters(), program.path.characters(),
name.length()); name.length());
}); });
if (!match) if (!match)
return complete_path("", name, offset, ExecutableOnly::Yes, nullptr, nullptr, escape_mode); return complete_path("", name, offset, ExecutableOnly::Yes, nullptr, nullptr, escape_mode);
String completion = *match; String completion = match->path;
auto token_length = escape_token(name, escape_mode).length(); auto token_length = escape_token(name, escape_mode).length();
auto invariant_offset = token_length; auto invariant_offset = token_length;
size_t static_offset = 0; size_t static_offset = 0;
@ -1539,11 +1572,11 @@ Vector<Line::CompletionSuggestion> Shell::complete_program_name(StringView name,
Vector<Line::CompletionSuggestion> suggestions; Vector<Line::CompletionSuggestion> suggestions;
int index = match - cached_path.data(); int index = match - cached_path.data();
for (int i = index - 1; i >= 0 && cached_path[i].starts_with(name); --i) for (int i = index - 1; i >= 0 && cached_path[i].path.starts_with(name); --i)
suggestions.append({ cached_path[i], " " }); suggestions.append({ cached_path[i].path, " " });
for (size_t i = index + 1; i < cached_path.size() && cached_path[i].starts_with(name); ++i) for (size_t i = index + 1; i < cached_path.size() && cached_path[i].path.starts_with(name); ++i)
suggestions.append({ cached_path[i], " " }); suggestions.append({ cached_path[i].path, " " });
suggestions.append({ cached_path[index], " " }); suggestions.append({ cached_path[index].path, " " });
for (auto& entry : suggestions) { for (auto& entry : suggestions) {
entry.input_offset = token_length; entry.input_offset = token_length;
@ -1670,7 +1703,7 @@ ErrorOr<Vector<Line::CompletionSuggestion>> Shell::complete_via_program_itself(s
completion_command = expand_aliases({ completion_command }).last(); completion_command = expand_aliases({ completion_command }).last();
auto completion_utility_name = String::formatted("_complete_{}", completion_command.argv[0]); auto completion_utility_name = String::formatted("_complete_{}", completion_command.argv[0]);
if (binary_search(cached_path, completion_utility_name)) if (binary_search(cached_path.span(), completion_utility_name, nullptr, RunnablePathComparator {}) != nullptr)
completion_command.argv[0] = completion_utility_name; completion_command.argv[0] = completion_utility_name;
else if (!options.invoke_program_for_autocomplete) else if (!options.invoke_program_for_autocomplete)
return Error::from_string_literal("Refusing to use the program itself as completion source"); return Error::from_string_literal("Refusing to use the program itself as completion source");

View file

@ -93,8 +93,52 @@ public:
Optional<AST::Position> position; Optional<AST::Position> position;
}; };
struct RunnablePath {
enum class Kind {
Builtin,
Function,
Alias,
Executable,
};
Kind kind;
String path;
bool operator<(RunnablePath const& other) const
{
return path < other.path;
}
bool operator==(RunnablePath const&) const = default;
};
struct RunnablePathComparator {
int operator()(RunnablePath const& lhs, RunnablePath const& rhs)
{
if (lhs.path > rhs.path)
return 1;
if (lhs.path < rhs.path)
return -1;
return 0;
}
int operator()(StringView lhs, RunnablePath const& rhs)
{
if (lhs > rhs.path)
return 1;
if (lhs < rhs.path)
return -1;
return 0;
}
};
int run_command(StringView, Optional<SourcePosition> = {}); int run_command(StringView, Optional<SourcePosition> = {});
bool is_runnable(StringView); Optional<RunnablePath> runnable_path_for(StringView);
Optional<String> help_path_for(Vector<RunnablePath> visited, RunnablePath const& runnable_path);
ErrorOr<RefPtr<Job>> run_command(const AST::Command&); ErrorOr<RefPtr<Job>> run_command(const AST::Command&);
NonnullRefPtrVector<Job> run_commands(Vector<AST::Command>&); NonnullRefPtrVector<Job> run_commands(Vector<AST::Command>&);
bool run_file(String const&, bool explicitly_invoked = true); bool run_file(String const&, bool explicitly_invoked = true);
@ -269,7 +313,7 @@ public:
Vector<String> directory_stack; Vector<String> directory_stack;
CircularQueue<String, 8> cd_history; // FIXME: have a configurable cd history length CircularQueue<String, 8> cd_history; // FIXME: have a configurable cd history length
HashMap<u64, NonnullRefPtr<Job>> jobs; HashMap<u64, NonnullRefPtr<Job>> jobs;
Vector<String, 256> cached_path; Vector<RunnablePath, 256> cached_path;
String current_script; String current_script;
@ -346,7 +390,7 @@ private:
void bring_cursor_to_beginning_of_a_line() const; void bring_cursor_to_beginning_of_a_line() const;
Optional<int> resolve_job_spec(StringView); Optional<int> resolve_job_spec(StringView);
void add_entry_to_cache(String const&); void add_entry_to_cache(RunnablePath const&);
void remove_entry_from_cache(StringView); void remove_entry_from_cache(StringView);
void stop_all_jobs(); void stop_all_jobs();
Job const* m_current_job { nullptr }; Job const* m_current_job { nullptr };
@ -489,3 +533,27 @@ inline size_t find_offset_into_node(StringView unescaped_text, size_t escaped_of
} }
} }
namespace AK {
template<>
struct Traits<Shell::Shell::RunnablePath> : public GenericTraits<Shell::Shell::RunnablePath> {
static constexpr bool is_trivial() { return false; }
static bool equals(Shell::Shell::RunnablePath const& self, Shell::Shell::RunnablePath const& other)
{
return self == other;
}
static bool equals(Shell::Shell::RunnablePath const& self, StringView other)
{
return self.path == other;
}
static bool equals(Shell::Shell::RunnablePath const& self, String const& other)
{
return self.path == other;
}
};
}