Skip to main content

flake_edit/app/
complete.rs

1//! Dynamic shell completions.
2
3use clap::CommandFactory;
4use clap_complete::CompleteEnv;
5use clap_complete::engine::{ArgValueCandidates, CompletionCandidate};
6
7use crate::cache::{Cache, DEFAULT_URI_TYPES};
8use crate::cli::CliArgs;
9use crate::edit::{FlakeEdit, sorted_input_ids};
10use crate::input::Follows;
11
12use super::editor::Editor;
13use super::handler::root::Root;
14
15/// Handle a completion request and exit, or return for a normal run.
16///
17/// Must be called before any other output.
18pub fn complete() {
19    CompleteEnv::with_factory(command).complete();
20}
21
22fn command() -> clap::Command {
23    let mut cmd = CliArgs::command();
24    for &(sub, arg, candidates) in COMPLETERS {
25        cmd = attach(cmd, sub, arg, candidates);
26    }
27    cmd
28}
29
30type Completer = fn() -> Vec<CompletionCandidate>;
31
32/// Mapping of dynamic completions to their trigger.
33const COMPLETERS: &[(&str, &str, Completer)] = &[
34    ("remove", "id", input_candidates),
35    ("change", "id", input_candidates),
36    ("pin", "id", toplevel_candidates),
37    ("unpin", "id", toplevel_candidates),
38    ("update", "id", toplevel_candidates),
39    ("toggle", "input", toggle_candidates),
40    ("add-follow", "input", follow_candidates),
41    ("add-follow", "target", toplevel_candidates),
42    ("add", "id", uri_candidates),
43    ("add", "uri", uri_candidates),
44];
45
46/// Attach `candidates` to `arg` of `sub` without reordering its arguments.
47fn attach(
48    cmd: clap::Command,
49    sub: &str,
50    arg: &'static str,
51    candidates: Completer,
52) -> clap::Command {
53    cmd.mut_subcommand(sub, move |c| {
54        c.mut_args(move |a| {
55            if a.get_id().as_str() == arg {
56                a.add(ArgValueCandidates::new(candidates))
57            } else {
58                a
59            }
60        })
61    })
62}
63
64/// Load the current directory's flake.{nix,lock}.
65///
66/// This is the source of many dynamic completions.
67fn load() -> Option<FlakeEdit> {
68    let root = Root::from_path("flake.nix").ok()?;
69    let editor = Editor::from_path(root.path().to_path_buf()).ok()?;
70    editor.create_flake_edit().ok()
71}
72
73fn input_candidates() -> Vec<CompletionCandidate> {
74    let Some(mut flake_edit) = load() else {
75        return Vec::new();
76    };
77    let inputs = flake_edit.list();
78    let mut out = Vec::new();
79    for key in sorted_input_ids(inputs) {
80        let input = &inputs[key];
81        out.push(CompletionCandidate::new(input.id().as_str()));
82        for follows in input.follows() {
83            if let Follows::Indirect { path, .. } = follows {
84                out.push(CompletionCandidate::new(format!(
85                    "{}.{}",
86                    input.id().as_str(),
87                    path
88                )));
89            }
90        }
91    }
92    out
93}
94
95fn toplevel_candidates() -> Vec<CompletionCandidate> {
96    let Some(mut flake_edit) = load() else {
97        return Vec::new();
98    };
99    let inputs = flake_edit.list();
100    sorted_input_ids(inputs)
101        .into_iter()
102        .map(|id| CompletionCandidate::new(id.as_str()))
103        .collect()
104}
105
106/// Known URI scheme prefixes plus previously seen URIs from the cache.
107fn uri_candidates() -> Vec<CompletionCandidate> {
108    let mut out: Vec<CompletionCandidate> = DEFAULT_URI_TYPES
109        .iter()
110        .map(|uri_type| CompletionCandidate::new(*uri_type))
111        .collect();
112    for uri in Cache::load().list_uris() {
113        out.push(CompletionCandidate::new(uri));
114    }
115    out
116}
117
118/// Nested input paths discovered in `flake.lock`, for manual follows targets.
119fn follow_candidates() -> Vec<CompletionCandidate> {
120    let Ok(lock) = crate::lock::FlakeLock::from_default_path() else {
121        return Vec::new();
122    };
123    lock.nested_inputs()
124        .into_iter()
125        .map(|nested| CompletionCandidate::new(nested.path.to_string()))
126        .collect()
127}
128
129fn toggle_candidates() -> Vec<CompletionCandidate> {
130    let Some(mut flake_edit) = load() else {
131        return Vec::new();
132    };
133    let Ok(states) = flake_edit.toggle_states() else {
134        return Vec::new();
135    };
136    states
137        .into_iter()
138        .filter(|(_, state)| !state.alternates.is_empty())
139        .map(|(id, _)| CompletionCandidate::new(id))
140        .collect()
141}