Skip to content

Commit

Permalink
feat: add dynamic shell completions (#583)
Browse files Browse the repository at this point in the history
  • Loading branch information
kaspar030 authored Dec 13, 2024
2 parents 6f50f06 + 079ed56 commit f24ab41
Show file tree
Hide file tree
Showing 9 changed files with 236 additions and 29 deletions.
34 changes: 34 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ homepage = "https://laze-build.org"
license = "Apache-2.0"
readme = "README.md"
include = ["src/**/*", "LICENSE", "README.md", "CHANGELOG.md", "!**/tests/**/*", "assets/**/*"]
rust-version = "1.74"
rust-version = "1.80"

[dependencies]
anyhow = "1.0.94"
bincode = "1.3.3"
clap = { version = "4.5.23", features = ["cargo", "env" ] }
clap = { version = "4.5.23", features = ["cargo", "env", "unstable-ext" ] }
derive_builder = "0.20.2"
indexmap = { version = "2.7.0", features = ["serde"] }
itertools = "0.13.0"
Expand All @@ -36,7 +36,7 @@ solvent = { version = "0.8.3", features = ["deterministic"] }
rust-embed = "8.5.0"
task_partitioner = "0.1.1"

clap_complete = "4.5.38"
clap_complete = { version = "4.5.38", features = [ "unstable-dynamic" ] }
clap_mangen = "0.2.24"
camino = { version = "1.1.9", features = ["serde1"] }
evalexpr = "11.3.1"
Expand Down
16 changes: 13 additions & 3 deletions src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
use camino::Utf8PathBuf;

use clap::{crate_version, value_parser, Arg, ArgAction, Command, ValueHint};
use clap_complete::engine::{ArgValueCandidates, SubcommandCandidates};

mod completer;
pub use completer::completing;
use completer::{app_completer, builder_completer, module_completer, task_completer};

pub fn clap() -> clap::Command {
fn build_dir() -> Arg {
Expand Down Expand Up @@ -34,6 +39,7 @@ pub fn clap() -> clap::Command {
.env("LAZE_SELECT")
.action(ArgAction::Append)
.value_delimiter(',')
.add(ArgValueCandidates::new(module_completer))
}

fn disable() -> Arg {
Expand All @@ -44,6 +50,7 @@ pub fn clap() -> clap::Command {
.env("LAZE_DISABLE")
.action(ArgAction::Append)
.value_delimiter(',')
.add(ArgValueCandidates::new(module_completer))
}

fn define() -> Arg {
Expand Down Expand Up @@ -170,7 +177,8 @@ pub fn clap() -> clap::Command {
.help("builders to configure")
.env("LAZE_BUILDERS")
.action(ArgAction::Append)
.value_delimiter(','),
.value_delimiter(',')
.add(ArgValueCandidates::new(builder_completer)),
)
.arg(
Arg::new("apps")
Expand All @@ -179,13 +187,15 @@ pub fn clap() -> clap::Command {
.help("apps to configure")
.env("LAZE_APPS")
.action(ArgAction::Append)
.value_delimiter(','),
.value_delimiter(',')
.add(ArgValueCandidates::new(app_completer)),
)
.arg(partition())
.next_help_heading("Extra build settings")
.arg(select())
.arg(disable())
.arg(define()),
.arg(define())
.add(SubcommandCandidates::new(task_completer)),
)
.subcommand(
Command::new("clean")
Expand Down
125 changes: 125 additions & 0 deletions src/cli/completer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
use std::{
env,
sync::{atomic::AtomicBool, LazyLock, Mutex},
};

use camino::Utf8PathBuf;
use clap_complete::CompletionCandidate;

use crate::model::ContextBag;

static COMPLETING: AtomicBool = AtomicBool::new(false);
static STATE: LazyLock<Mutex<CompleterState>> = LazyLock::new(|| {
COMPLETING.store(true, std::sync::atomic::Ordering::Release);
Mutex::new(CompleterState::new())
});

pub fn completing() -> bool {
COMPLETING.load(std::sync::atomic::Ordering::Acquire)
}

#[derive(Default)]
struct CompleterState {
contexts: Option<ContextBag>,
}

impl CompleterState {
pub fn new() -> Self {
let cwd = Utf8PathBuf::try_from(env::current_dir().unwrap()).expect("cwd not UTF8");
let project_root = crate::determine_project_root(&cwd);
if let Ok((project_root, project_file)) = project_root {
let build_dir = project_root.join("build");
let project_file = project_root.join(project_file);
let res = crate::data::load(&project_file, &build_dir);
// TODO: this is where the error is eaten, when this fails. log?
let contexts = res.ok().map(|(contexts, _, _)| contexts);
Self { contexts }
} else {
Self { contexts: None }
}
}

pub fn builders(&self) -> Vec<CompletionCandidate> {
if let Some(contexts) = self.contexts.as_ref() {
contexts
.builders()
.map(|builder| {
CompletionCandidate::new(&builder.name)
.help(builder.help.as_ref().map(|help| help.into()))
})
.collect()
} else {
Vec::new()
}
}

pub fn apps(&self) -> Vec<CompletionCandidate> {
if let Some(contexts) = self.contexts.as_ref() {
contexts
.modules()
.filter(|(_, module)| module.is_binary)
.map(|(name, module)| {
CompletionCandidate::new(name)
.help(module.help.as_ref().map(|help| help.into()))
})
.collect()
} else {
Vec::new()
}
}

pub fn modules(&self) -> Vec<CompletionCandidate> {
if let Some(contexts) = self.contexts.as_ref() {
contexts
.modules()
.map(|(name, module)| {
CompletionCandidate::new(name)
.help(module.help.as_ref().map(|help| help.into()))
})
.collect()
} else {
Vec::new()
}
}

pub fn tasks(&self) -> Vec<CompletionCandidate> {
if let Some(contexts) = self.contexts.as_ref() {
contexts
.modules()
.flat_map(|(_name, module)| module.tasks.iter())
.chain(
contexts
.contexts
.iter()
.filter_map(|c| c.tasks.as_ref())
.flat_map(|tasks| tasks.iter()),
)
.map(|(name, task)| {
CompletionCandidate::new(name).help(task.help.as_ref().map(|help| help.into()))
})
.collect()
} else {
Vec::new()
}
}
}

pub fn app_completer() -> Vec<CompletionCandidate> {
let state = STATE.lock().unwrap();
state.apps()
}

pub fn builder_completer() -> Vec<CompletionCandidate> {
let state = STATE.lock().unwrap();
state.builders()
}

pub fn module_completer() -> Vec<CompletionCandidate> {
let state = STATE.lock().unwrap();
state.modules()
}

pub fn task_completer() -> Vec<CompletionCandidate> {
let state = STATE.lock().unwrap();
state.tasks()
}
35 changes: 20 additions & 15 deletions src/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use itertools::Itertools;
use serde_yaml::Value;
use std::collections::{HashMap, HashSet};
use std::fs::read_to_string;
use std::time::Instant;
use std::time::{Duration, Instant};

use anyhow::{Context as _, Error, Result};
use camino::{Utf8Path, Utf8PathBuf};
Expand All @@ -33,6 +33,12 @@ use import::ImportEntry;

pub type FileTreeState = TreeState<FileState, std::path::PathBuf>;

pub struct LoadStats {
pub files: usize,
pub parsing_time: Duration,
pub stat_time: Duration,
}

// Any value that is present is considered Some value, including null.
fn deserialize_some<'de, T, D>(deserializer: D) -> Result<Option<T>, D::Error>
where
Expand Down Expand Up @@ -329,7 +335,10 @@ impl FileInclude {
}
}

pub fn load(filename: &Utf8Path, build_dir: &Utf8Path) -> Result<(ContextBag, FileTreeState)> {
pub fn load(
filename: &Utf8Path,
build_dir: &Utf8Path,
) -> Result<(ContextBag, FileTreeState, LoadStats)> {
let mut contexts = ContextBag::new();
let start = Instant::now();

Expand Down Expand Up @@ -938,12 +947,7 @@ pub fn load(filename: &Utf8Path, build_dir: &Utf8Path) -> Result<(ContextBag, Fi

contexts.merge_provides();

println!(
"laze: reading {} files took {:?}",
filenames.len(),
start.elapsed(),
);

let parsing_time = start.elapsed();
let start = Instant::now();

// convert Utf8PathBufs to PathBufs
Expand All @@ -954,13 +958,14 @@ pub fn load(filename: &Utf8Path, build_dir: &Utf8Path) -> Result<(ContextBag, Fi
.collect_vec();

let treestate = FileTreeState::new(filenames.iter());
println!(
"laze: stat'ing {} files took {:?}",
filenames.len(),
start.elapsed(),
);

Ok((contexts, treestate))
let stat_time = start.elapsed();

let stats = LoadStats {
parsing_time,
stat_time,
files: filenames.len(),
};
Ok((contexts, treestate, stats))
}

fn convert_tasks(
Expand Down
22 changes: 16 additions & 6 deletions src/data/import/download.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,28 @@ use crate::download::{Download, Git, Source};
#[folder = "assets/imports"]
struct Asset;

fn git_cloner(url: &str, target_path: &Utf8Path) -> GitCacheClonerBuilder {
let git_cache = crate::GIT_CACHE.get().expect("this has been set earlier");
fn git_cloner(url: &str, target_path: &Utf8Path) -> Result<GitCacheClonerBuilder, Error> {
let git_cache = crate::GIT_CACHE
.get()
.ok_or(anyhow!("git cache not available"))?;

let mut git_cache_builder = git_cache.cloner();

git_cache_builder
.repository_url(url.to_string())
.target_path(Some(target_path.to_path_buf()));

git_cache_builder
Ok(git_cache_builder)
}

fn git_clone_commit(url: &str, target_path: &Utf8Path, commit: &str) -> Result<(), Error> {
git_cloner(url, target_path)
git_cloner(url, target_path)?
.commit(Some(commit.into()))
.do_clone()
}

fn git_clone_branch(url: &str, target_path: &Utf8Path, branch: &str) -> Result<(), Error> {
git_cloner(url, target_path)
git_cloner(url, target_path)?
.update(true)
.extra_clone_args(Some(vec!["--branch".into(), branch.into()]))
.do_clone()
Expand All @@ -47,6 +49,14 @@ impl Import for Download {
skip_download = self.compare_with_tagfile(&tagfile).unwrap_or_default();
}
if !skip_download {
if crate::cli::completing() {
// TODO: downloading causes output (e.g., from git), need to
// silence that somehow. Also, as completions don't (yet) take
// `--build-dir` or `--chdir` into account, we shouldn't be
// writing into hardcoded `build`.
return Err(anyhow!("cannot download when completing"));
}

if target_path.exists() {
remove_dir_all(&target_path)
.with_context(|| format!("removing path \"{target_path}\""))?;
Expand Down Expand Up @@ -81,7 +91,7 @@ impl Import for Download {
Source::Git(Git::Default { url }) => {
println!("IMPORT Git {url} -> {target_path}");

git_cloner(url, &target_path)
git_cloner(url, &target_path)?
.do_clone()
.with_context(|| format!("cloning git url: \"{url}\""))?;

Expand Down
Loading

0 comments on commit f24ab41

Please sign in to comment.