Skip to content

Commit

Permalink
Implement more of compgen (#8)
Browse files Browse the repository at this point in the history
Adds naive implementation for file, alias, builtin, and function.
  • Loading branch information
reubeno authored May 13, 2024
1 parent cf53bf2 commit 62f9451
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 72 deletions.
3 changes: 3 additions & 0 deletions cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ enum TraceEvent {
Expand,
#[clap(name = "parse")]
Parse,
#[clap(name = "pattern")]
Pattern,
#[clap(name = "tokenize")]
Tokenize,
}
Expand Down Expand Up @@ -125,6 +127,7 @@ fn main() {
TraceEvent::Complete => vec!["shell::completion", "shell::builtins::complete"],
TraceEvent::Expand => vec![],
TraceEvent::Parse => vec!["parse"],
TraceEvent::Pattern => vec!["shell::pattern"],
TraceEvent::Tokenize => vec!["tokenize"],
};

Expand Down
9 changes: 1 addition & 8 deletions shell/src/builtins/help.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
use crate::builtin::{BuiltinCommand, BuiltinExitCode};
use clap::Parser;
use itertools::Itertools;
use std::io::Write;
use std::iter::Iterator;

#[derive(Parser)]
pub(crate) struct HelpCommand {}
Expand All @@ -22,12 +20,7 @@ impl BuiltinCommand for HelpCommand {
"The following commands are implemented as shell built-ins:"
)?;

let builtin_names: Vec<_> = super::SPECIAL_BUILTINS
.iter()
.chain(super::BUILTINS.iter())
.map(|(name, _)| *name)
.sorted()
.collect();
let builtin_names: Vec<_> = crate::builtins::get_all_builtin_names();

const COLUMN_COUNT: usize = 3;

Expand Down
10 changes: 10 additions & 0 deletions shell/src/builtins/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use futures::future::BoxFuture;
use itertools::Itertools;
use std::collections::{HashMap, HashSet};
use std::io::Write;

Expand Down Expand Up @@ -130,6 +131,15 @@ lazy_static::lazy_static! {
pub(crate) static ref DECLARATION_BUILTINS: HashSet<&'static str> = get_declaration_builtin_names();
}

pub(crate) fn get_all_builtin_names() -> Vec<String> {
SPECIAL_BUILTINS
.iter()
.chain(BUILTINS.iter())
.map(|(name, _)| (*name).to_owned())
.sorted()
.collect::<Vec<_>>()
}

fn get_declaration_builtin_names() -> HashSet<&'static str> {
let mut s = HashSet::new();
s.insert("alias");
Expand Down
140 changes: 82 additions & 58 deletions shell/src/completion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use std::{
path::{Path, PathBuf},
};

use crate::{env, error, patterns, variables::ShellValueLiteral, Shell};
use crate::{builtins, env, error, patterns, variables::ShellValueLiteral, Shell};

#[derive(Clone, Debug, ValueEnum)]
pub enum CompleteAction {
Expand Down Expand Up @@ -145,6 +145,7 @@ pub struct CompletionContext<'a> {
}

impl CompletionSpec {
#[allow(clippy::too_many_lines)]
pub async fn get_completions(
&self,
shell: &mut Shell,
Expand Down Expand Up @@ -177,19 +178,39 @@ impl CompletionSpec {

for action in &self.actions {
match action {
CompleteAction::Alias => tracing::debug!("UNIMPLEMENTED: complete -A alias"),
CompleteAction::Alias => {
for name in shell.aliases.keys() {
candidates.push(name.to_string());
}
}
CompleteAction::ArrayVar => tracing::debug!("UNIMPLEMENTED: complete -A arrayvar"),
CompleteAction::Binding => tracing::debug!("UNIMPLEMENTED: complete -A binding"),
CompleteAction::Builtin => tracing::debug!("UNIMPLEMENTED: complete -A builtin"),
CompleteAction::Builtin => {
let mut builtin_names = builtins::get_all_builtin_names();
candidates.append(&mut builtin_names);
}
CompleteAction::Command => tracing::debug!("UNIMPLEMENTED: complete -A command"),
CompleteAction::Directory => {
tracing::debug!("UNIMPLEMENTED: complete -A directory");
}
CompleteAction::Disabled => tracing::debug!("UNIMPLEMENTED: complete -A disabled"),
CompleteAction::Enabled => tracing::debug!("UNIMPLEMENTED: complete -A enabled"),
CompleteAction::Export => tracing::debug!("UNIMPLEMENTED: complete -A export"),
CompleteAction::File => tracing::debug!("UNIMPLEMENTED: complete -A file"),
CompleteAction::Function => tracing::debug!("UNIMPLEMENTED: complete -A function"),
CompleteAction::Export => {
for (key, value) in shell.env.iter() {
if value.is_exported() {
candidates.push(key.to_owned());
}
}
}
CompleteAction::File => {
let mut file_completions = get_file_completions(shell, context);
candidates.append(&mut file_completions);
}
CompleteAction::Function => {
for name in shell.funcs.keys() {
candidates.push(name.to_owned());
}
}
CompleteAction::Group => tracing::debug!("UNIMPLEMENTED: complete -A group"),
CompleteAction::HelpTopic => {
tracing::debug!("UNIMPLEMENTED: complete -A helptopic");
Expand All @@ -205,9 +226,9 @@ impl CompletionSpec {
CompleteAction::Stopped => tracing::debug!("UNIMPLEMENTED: complete -A stopped"),
CompleteAction::User => tracing::debug!("UNIMPLEMENTED: complete -A user"),
CompleteAction::Variable => {
shell.env.iter().for_each(|(key, _value)| {
candidates.push(key.to_string());
});
for (key, _) in shell.env.iter() {
candidates.push(key.to_owned());
}
}
}
}
Expand Down Expand Up @@ -502,64 +523,67 @@ impl CompletionConfig {
}
}

self.get_completions_using_basic_lookup(shell, &context)
get_completions_using_basic_lookup(shell, &context)
}
}

#[allow(clippy::unused_self)]
fn get_completions_using_basic_lookup(
&self,
shell: &Shell,
context: &CompletionContext,
) -> CompletionResult {
// TODO: Contextually generate different completions.
let glob = std::format!("{}*", context.token_to_complete);
// TODO: Pass through quoting.
let mut candidates = if let Ok(mut candidates) = patterns::Pattern::from(glob)
.expand(shell.working_dir.as_path(), shell.options.extended_globbing)
{
for candidate in &mut candidates {
if Path::new(candidate.as_str()).is_dir() {
candidate.push('/');
}
}
candidates
} else {
vec![]
};

// If this appears to be the command token (and if there's *some* prefix without
// a path separator) then also consider whether we should search the path for
// completions too.
// TODO: Do a better job than just checking if index == 0.
if context.token_index == 0
&& !context.token_to_complete.is_empty()
&& !context.token_to_complete.contains('/')
{
let glob_pattern = std::format!("{}*", context.token_to_complete);

for path in shell.find_executables_in_path(&glob_pattern) {
if let Some(file_name) = path.file_name() {
candidates.push(file_name.to_string_lossy().to_string());
}
fn get_file_completions(shell: &Shell, context: &CompletionContext) -> Vec<String> {
let glob = std::format!("{}*", context.token_to_complete);

// TODO: Pass through quoting.
if let Ok(mut candidates) = patterns::Pattern::from(glob)
.expand(shell.working_dir.as_path(), shell.options.extended_globbing)
{
for candidate in &mut candidates {
if Path::new(candidate.as_str()).is_dir() {
candidate.push('/');
}
}
candidates
} else {
vec![]
}
}

if context.token_index + 1 >= context.tokens.len() {
for candidate in &mut candidates {
if !candidate.ends_with('/') {
candidate.push(' ');
}
fn get_completions_using_basic_lookup(
shell: &Shell,
context: &CompletionContext,
) -> CompletionResult {
let mut candidates = get_file_completions(shell, context);

// TODO: Contextually generate different completions.
// If this appears to be the command token (and if there's *some* prefix without
// a path separator) then also consider whether we should search the path for
// completions too.
// TODO: Do a better job than just checking if index == 0.
if context.token_index == 0
&& !context.token_to_complete.is_empty()
&& !context.token_to_complete.contains('/')
{
let glob_pattern = std::format!("{}*", context.token_to_complete);

for path in shell.find_executables_in_path(&glob_pattern) {
if let Some(file_name) = path.file_name() {
candidates.push(file_name.to_string_lossy().to_string());
}
}
}

#[cfg(windows)]
{
candidates = candidates
.into_iter()
.map(|c| c.replace("\\", "/"))
.collect();
if context.token_index + 1 >= context.tokens.len() {
for candidate in &mut candidates {
if !candidate.ends_with('/') {
candidate.push(' ');
}
}
}

CompletionResult::Candidates(candidates)
#[cfg(windows)]
{
candidates = candidates
.into_iter()
.map(|c| c.replace("\\", "/"))
.collect();
}

CompletionResult::Candidates(candidates)
}
15 changes: 9 additions & 6 deletions shell/src/patterns.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ impl Pattern {
return Ok(vec![concatenated]);
}

tracing::debug!("expanding pattern: {self:?}");

let mut components: Vec<PatternWord> = vec![];
for piece in &self.pieces {
let mut split_result = piece
Expand All @@ -100,19 +102,18 @@ impl Pattern {
}

let is_absolute = if let Some(first_component) = components.first() {
if let Some(first_piece) = first_component.first() {
first_piece.as_str().starts_with(std::path::MAIN_SEPARATOR)
} else {
false
}
first_component
.iter()
.all(|piece| piece.as_str().is_empty())
} else {
false
};

let prefix_to_remove;
let mut paths_so_far = if is_absolute {
prefix_to_remove = None;
vec![PathBuf::new()]
// TODO: Figure out appropriate thing to do on non-Unix platforms.
vec![PathBuf::from("/")]
} else {
let mut working_dir_str = working_dir.to_string_lossy().to_string();
working_dir_str.push(std::path::MAIN_SEPARATOR);
Expand Down Expand Up @@ -172,6 +173,8 @@ impl Pattern {
})
.collect();

tracing::debug!(" => results: {results:?}");

Ok(results)
}

Expand Down

0 comments on commit 62f9451

Please sign in to comment.