diff --git a/src/args.rs b/src/args.rs index 78ed79ee..f5196497 100644 --- a/src/args.rs +++ b/src/args.rs @@ -2,8 +2,9 @@ use std::ffi::OsString; pub(crate) use crate::arg::*; use crate::{ + complete_gen::{Comp, CompExtra}, error::{Message, MissingItem}, - item::Item, + item::{Item, ShortLong}, meta_help::Metavar, parsers::NamedArg, Error, @@ -55,6 +56,11 @@ impl Args<'_> { /// .unwrap_stdout(); /// assert_eq!(r, "-f"); /// ``` + /// + /// Note to self: shell passes "" as a parameter in situations like foo `--bar TAB`, bpaf + /// completion stubs adopt this conventions add pass it along. This is needed so completer can + /// tell the difference between `--bar` being completed or an argument to it in the example + /// above. #[cfg(feature = "autocomplete")] #[must_use] pub fn set_comp(mut self, rev: usize) -> Self { @@ -282,8 +288,8 @@ mod inner { impl State { /// Check if item at ixth position is still present (was not parsed) - pub(crate) fn present(&self, ix: usize) -> Option { - Some(self.item_state.get(ix)?.present()) + pub(crate) fn present(&self, ix: usize) -> bool { + self.item_state.get(ix).map_or(false, |arg| arg.present()) } pub(crate) fn depth(&self) -> usize { @@ -299,13 +305,15 @@ mod inner { impl State { #[cfg(feature = "autocomplete")] pub(crate) fn check_no_pos_ahead(&self) -> bool { - self.comp.as_ref().map_or(false, |c| c.no_pos_ahead) + self.comp + .as_ref() + .map_or(false, |c| c.consumed_as_positional) } #[cfg(feature = "autocomplete")] pub(crate) fn set_no_pos_ahead(&mut self) { if let Some(comp) = &mut self.comp { - comp.no_pos_ahead = true; + comp.consumed_as_positional = true; } } @@ -324,16 +332,18 @@ mod inner { let mut comp_scanner = crate::complete_run::ArgScanner { revision: args.c_rev, name: args.name.as_deref(), + current: String::new(), }; - for os in args.items { - if pos_only { - items.push(Arg::PosWord(os)); + let len = args.items.len(); + for (ix, os) in args.items.enumerate() { + #[cfg(feature = "autocomplete")] + if comp_scanner.check_next(&os, ix + 1 == len) { continue; } - #[cfg(feature = "autocomplete")] - if comp_scanner.check_next(&os) { + if pos_only { + items.push(Arg::PosWord(os)); continue; } @@ -381,15 +391,10 @@ mod inner { let mut item_state = vec![ItemState::Unparsed; items.len()]; let mut remaining = items.len(); + if let Some(ix) = double_dash_marker { item_state[ix] = ItemState::Parsed; remaining -= 1; - - #[cfg(feature = "autocomplete")] - if comp_scanner.revision.is_some() && ix == items.len() - 1 { - remaining += 1; - item_state[ix] = ItemState::Unparsed; - } } let mut path = Vec::new(); @@ -601,7 +606,8 @@ mod inner { return None; } self.cur += 1; - if !self.args.present(cur)? { + + if !self.args.present(cur) { continue; } if cur + self.width > self.args.items.len() { @@ -635,6 +641,111 @@ mod inner { } impl State { + #[cfg(feature = "autocomplete")] + #[inline(never)] + fn complete_named(&mut self, named: &NamedArg, metavar: Option) { + if let Some(comp) = self.comp_mut() { + let arg = comp.current_arg.as_str(); + + let name; + if arg.is_empty() { + if let Some(long) = named.long.first() { + name = ShortLong::Long(long); + } else if let Some(short) = named.short.first() { + name = ShortLong::Short(*short); + } else { + return; + } + } else if arg == "--" { + if let Some(long) = named.long.first() { + name = ShortLong::Long(long); + } else { + return; + } + } else if arg == "-" { + if let Some(long) = named.long.first() { + name = ShortLong::Long(long); + } else if let Some(short) = named.short.first() { + name = ShortLong::Short(*short); + } else { + return; + } + } else if let Some(prefix) = arg.strip_prefix("--") { + if let Some(long) = named.long.iter().find(|n| n.starts_with(prefix)) { + name = ShortLong::Long(long); + } else { + return; + } + } else if let Some(prefix) = arg.strip_prefix('-') { + if let Some(first) = named.long.first() { + if !named.short.is_empty() { + let mut chars = prefix.chars(); + if let (Some(c), None) = (chars.next(), chars.next()) { + if named.short.contains(&c) { + let extra = CompExtra { + depth: 0, + group: None, + help: None, + }; + comp.comps2.push(Comp::Value { + extra, + is_argument: true, + body: format!("--{}", first), + }); + } + } + } + } + return; + } else { + return; + } + let help = named.help.as_ref().and_then(crate::Doc::to_completion); + let extra = CompExtra { + depth: 0, + group: None, + help, + }; + match metavar { + Some(m) => comp.comps2.push(Comp::Argument { + extra, + name, + metavar: m.0, + }), + None => comp.comps2.push(Comp::Flag { extra, name }), + } + } + } + + #[cfg(feature = "autocomplete")] + #[inline(never)] + pub(crate) fn complete_command( + &mut self, + longs: &[&'static str], + shorts: &[char], + help: Option<&crate::Doc>, + ) { + if let Some(comp) = self.comp_mut() { + let arg = &comp.current_arg; + for long in longs { + if long.starts_with(arg) { + let help = help.and_then(crate::Doc::to_completion); + let extra = CompExtra { + depth: 0, + group: None, + help, + }; + comp.comps2.push(Comp::Command { + extra, + name: long, + short: None, + }); + return; + } + } + } + } + #[inline(never)] #[cfg(feature = "autocomplete")] pub(crate) fn swap_comps_with(&mut self, comps: &mut Vec) { @@ -646,6 +757,7 @@ impl State { /// Get a short or long flag: `-f` / `--flag` /// /// Returns false if value isn't present + #[inline(never)] pub(crate) fn take_flag(&mut self, named: &NamedArg) -> bool { if let Some((ix, _)) = self .items_iter() @@ -654,6 +766,9 @@ impl State { self.remove(ix); true } else { + #[cfg(feature = "autocomplete")] + self.complete_named(named, None); + false } } @@ -673,10 +788,37 @@ impl State { .find(|arg| named.matches_arg(arg.1, adjacent)) { Some(v) => v, - None => return Ok(None), + None => { + #[cfg(feature = "autocomplete")] + self.complete_named(named, Some(metavar)); + return Ok(None); + } }; let val_ix = key_ix + 1; + + #[cfg(feature = "autocomplete")] + if self.items.get(val_ix).is_none() { + // this covers only scenario when this argument is the one we are completing + // otherwise parsing behavior is unchanged + if let Some(comp) = self.comp_mut() { + // should insert metavariable here + if comp.meta.is_none() { + let cur = crate::complete_gen::CurrentMeta { + name: metavar, + help: None, + is_argument: true, + }; + comp.meta = Some(cur); + } + + let val = comp.current_arg.as_str().into(); + self.current = Some(val_ix); + self.remove(key_ix); + return Ok(Some(val)); + } + } + let val = match self.get(val_ix) { Some(Arg::Word(w) | Arg::ArgWord(w)) => w, _ => return Err(Error(Message::NoArgument(key_ix, metavar))), diff --git a/src/complete_gen.rs b/src/complete_gen.rs index 10ee608d..d8fbb3ad 100644 --- a/src/complete_gen.rs +++ b/src/complete_gen.rs @@ -14,32 +14,75 @@ // // complete short names to long names if possible +// instead + +// if we can't complete current value - don't make any suggestions at all! +// this behavior matches one from completions bash and zsh give for ls + use crate::{ args::{Arg, State}, complete_shell::{render_bash, render_fish, render_simple, render_test, render_zsh}, item::ShortLong, + meta_help::Metavar, parsers::NamedArg, Doc, ShellComp, }; use std::ffi::OsStr; +#[derive(Clone, Debug)] +pub(crate) struct CurrentMeta { + pub(crate) name: Metavar, + pub(crate) help: Option, + /// Is metavar belongs to an argument? + /// + /// The difference is that for not arguments in a scenario like "[-v] " + /// While completing FILE It is also valid to suggest -v, while for arguments + /// "-v -f " and we are completing FILE suggesting -v won't be valid. + /// + /// As a result any metavar or any value with is_argument set disables any + /// non argument values or metavars + pub(crate) is_argument: bool, +} + #[derive(Clone, Debug)] pub(crate) struct Complete { /// completions accumulated so far comps: Vec, + + pub(crate) comps2: Vec, + + /// Are we expanding a metavariable? + /// This takes priority over comps + pub(crate) meta: Option, + + /// Output revision + /// + /// This value will be used to decide how to render the generated completion info + /// for different shell. + /// + /// The only reason it is inside of Complete struct is that it gets created from arguments + /// and needs to be stored somewhere pub(crate) output_rev: usize, - /// don't try to suggest any more positional items after there's a positional item failure - /// or parsing in progress - pub(crate) no_pos_ahead: bool, + /// Argument that is being completed + /// + /// This argument can be either positional or named item, or even part of them like "--ver" + /// in a process of being complete to "--verbose" + pub(crate) current_arg: String, + + /// Current argument was consumed by a positional parser + pub(crate) consumed_as_positional: bool, } impl Complete { - pub(crate) fn new(output_rev: usize) -> Self { + pub(crate) fn new(output_rev: usize, current_arg: String) -> Self { Self { comps: Vec::new(), + meta: None, + comps2: Vec::new(), output_rev, - no_pos_ahead: false, + current_arg, + consumed_as_positional: false, } } } @@ -166,7 +209,7 @@ impl State { } impl Complete { - pub(crate) fn push_shell(&mut self, op: ShellComp, depth: usize) { + pub(crate) fn push_shell(&mut self, op: ShellComp, is_argument: bool, depth: usize) { self.comps.push(Comp::Shell { extra: CompExtra { depth, @@ -174,6 +217,7 @@ impl Complete { help: None, }, script: op, + is_argument, }); } @@ -197,15 +241,15 @@ impl Complete { } pub(crate) fn extend_comps(&mut self, comps: Vec) { - self.comps.extend(comps); + self.comps2.extend(comps); } pub(crate) fn drain_comps(&mut self) -> std::vec::Drain { - self.comps.drain(0..) + self.comps2.drain(0..) } pub(crate) fn swap_comps(&mut self, other: &mut Vec) { - std::mem::swap(other, &mut self.comps); + std::mem::swap(other, &mut self.comps2); } } @@ -224,10 +268,7 @@ pub(crate) struct CompExtra { #[derive(Clone, Debug)] pub(crate) enum Comp { /// short or long flag - Flag { - extra: CompExtra, - name: ShortLong, - }, + Flag { extra: CompExtra, name: ShortLong }, /// argument + metadata Argument { @@ -256,12 +297,15 @@ pub(crate) enum Comp { Metavariable { extra: CompExtra, meta: &'static str, + /// AKA not positional is_argument: bool, }, Shell { extra: CompExtra, script: ShellComp, + /// AKA not positional + is_argument: bool, }, } @@ -348,6 +392,9 @@ fn pair_to_os_string<'a>(pair: (&'a Arg, &'a OsStr)) -> Option<(&'a Arg, &'a str Some((pair.0, pair.1.to_str()?)) } +/// What is the preceeding item, if any +/// +/// Mostly is there to tell if we are trying to complete and argument or not... #[derive(Debug, Copy, Clone)] enum Prefix<'a> { NA, @@ -364,45 +411,53 @@ impl State { pub(crate) fn check_complete(&self) -> Option { let comp = self.comp_ref()?; - let mut items = self - .items - .iter() - .rev() - .filter_map(Arg::and_os_string) - .filter_map(pair_to_os_string); - + /* + let mut items = self + .items + .iter() + .rev() + .filter_map(Arg::and_os_string) + .filter_map(pair_to_os_string); + */ // try get a current item to complete - must be non-virtual right most one // value must be present here, and can fail only for non-utf8 values // can't do much completing with non-utf8 values since bpaf needs to print them to stdout - let (_, lit) = items.next()?; + // let (cur, lit) = items.next()?; // For cases like "-k=val", "-kval", "--key=val", "--key val" // last value is going to be either Arg::Word or Arg::ArgWord // so to perform full completion we look at the preceeding item // and use it's value if it was a composite short/long argument - let preceeding = items.next(); - let (pos_only, full_lit) = match preceeding { - Some((Arg::Short(_, true, _os) | Arg::Long(_, true, _os), full_lit)) => { - (false, full_lit) - } - Some((Arg::PosWord(_), _)) => (true, lit), - _ => (false, lit), - }; - - let prefix = match preceeding { - Some((Arg::Short(s, true, _os), _lit)) => Prefix::Short(*s), - Some((Arg::Long(l, true, _os), _lit)) => Prefix::Long(l.as_str()), - _ => Prefix::NA, - }; - - let (items, shell) = comp.complete(lit, pos_only, prefix); + // let preceeding = items.next(); + // let (pos_only, full_lit) = match preceeding { + // Some((Arg::Short(_, true, _os) | Arg::Long(_, true, _os), full_lit)) => { + // (false, full_lit) + // } + // Some((Arg::PosWord(_), _)) => (true, lit), + // _ => (false, lit), + // }; + + // let is_named = match cur { + // Arg::Short(_, _, _) | Arg::Long(_, _, _) => true, + // Arg::ArgWord(_) | Arg::Word(_) | Arg::PosWord(_) => false, + // }; + + // let prefix = match preceeding { + // Some((Arg::Short(s, true, _os), _lit)) => Prefix::Short(*s), + // Some((Arg::Long(l, true, _os), _lit)) => Prefix::Long(l.as_str()), + // _ => Prefix::NA, + // }; + + // println!("comps2: {:?}", comp.comps2); + + let (items, shell) = comp.complete("", false, false, Prefix::NA); Some(match comp.output_rev { - 0 => render_test(&items, &shell, full_lit), + 0 => render_test(&items, &shell ), 1 => render_simple(&items), // <- AKA elvish - 7 => render_zsh(&items, &shell, full_lit), - 8 => render_bash(&items, &shell, full_lit), - 9 => render_fish(&items, &shell, full_lit, self.path[0].as_str()), + 7 => render_zsh(&items, &shell ), + 8 => render_bash(&items, &shell ), + 9 => render_fish(&items, &shell , self.path[0].as_str()), unk => { #[cfg(debug_assertions)] { @@ -480,10 +535,9 @@ impl Comp { fn only_value(&self) -> bool { match self { Comp::Flag { .. } | Comp::Argument { .. } | Comp::Command { .. } => false, - Comp::Metavariable { is_argument, .. } | Comp::Value { is_argument, .. } => { - *is_argument - } - Comp::Shell { .. } => true, + Comp::Metavariable { is_argument, .. } + | Comp::Value { is_argument, .. } + | Comp::Shell { is_argument, .. } => *is_argument, } } fn is_pos(&self) -> bool { @@ -495,22 +549,56 @@ impl Comp { } } +impl State { + /// Move current metavariable contents into completions + /// + /// This requires &mut access to state + pub(crate) fn convert_current_metavar(&mut self) { + if let Some(comp) = self.comp_mut() { + if let Some(meta) = std::mem::take(&mut comp.meta) { + if meta.is_argument { + comp.comps2.clear(); + } + comp.comps2.push(Comp::Metavariable { + extra: CompExtra { + depth: 0, + group: None, + help: meta.help.clone(), + }, + meta: meta.name.0, + is_argument: meta.is_argument, + }); + } + } + } +} + impl Complete { fn complete( &self, arg: &str, pos_only: bool, + is_named: bool, prefix: Prefix, ) -> (Vec, Vec) { let mut items: Vec = Vec::new(); let mut shell = Vec::new(); - let max_depth = self.comps.iter().map(Comp::depth).max().unwrap_or(0); - let mut only_values = false; + // let max_depth = self.comps.iter().map(Comp::depth).max().unwrap_or(0); + let mut only_values = self.comps2.iter().any(|v| { + matches!( + v, + Comp::Value { + is_argument: true, + .. + } | Comp::Metavariable { + is_argument: true, + .. + } + ) + }); - for item in self - .comps - .iter() - .filter(|c| c.depth() == max_depth && (!pos_only || c.is_pos())) + for item in self.comps2.iter() + // .filter(|c| c.depth() == max_depth && (!pos_only || c.is_pos())) { match (only_values, item.only_value()) { (true, true) | (false, false) => {} @@ -588,7 +676,9 @@ impl Complete { } Comp::Shell { script, .. } => { - shell.push(*script); + if !is_named { + shell.push(*script); + } } } } diff --git a/src/complete_run.rs b/src/complete_run.rs index d7da79f8..6c73c1d3 100644 --- a/src/complete_run.rs +++ b/src/complete_run.rs @@ -69,10 +69,11 @@ set edit:completion:arg-completer[{name}] = {{ |@args| var args = $args[1..]; pub(crate) struct ArgScanner<'a> { pub(crate) revision: Option, pub(crate) name: Option<&'a str>, + pub(crate) current: String, } impl ArgScanner<'_> { - pub(crate) fn check_next(&mut self, arg: &OsStr) -> bool { + pub(crate) fn check_next(&mut self, arg: &OsStr, last: bool) -> bool { let arg = match arg.to_str() { Some(arg) => arg, None => return false, @@ -99,9 +100,15 @@ impl ArgScanner<'_> { } return true; } + if last && self.revision.is_some() { + self.current = arg.to_owned(); + return true; + } + false } - pub(crate) fn done(&self) -> Option { - Some(Complete::new(self.revision?)) + + pub(crate) fn done(self) -> Option { + Some(Complete::new(self.revision?, self.current)) } } diff --git a/src/complete_shell.rs b/src/complete_shell.rs index 91a83df9..f7c9418e 100644 --- a/src/complete_shell.rs +++ b/src/complete_shell.rs @@ -97,8 +97,8 @@ where let depth = args.depth(); if let Some(comp) = args.comp_mut() { for ci in comp_items { - if ci.is_metavar().is_some() { - comp.push_shell(self.op, depth); + if let Some(is_argument) = ci.is_metavar() { + comp.push_shell(self.op, is_argument, depth); } else { comp.push_comp(ci); } @@ -113,16 +113,12 @@ where } } -pub(crate) fn render_zsh( - items: &[ShowComp], - ops: &[ShellComp], - full_lit: &str, -) -> Result { +pub(crate) fn render_zsh(items: &[ShowComp], ops: &[ShellComp]) -> Result { use std::fmt::Write; let mut res = String::new(); if items.is_empty() && ops.is_empty() { - return Ok(format!("compadd -- {}\n", full_lit)); + return Ok(String::new()); } for op in ops { @@ -175,7 +171,6 @@ pub(crate) fn render_zsh( pub(crate) fn render_bash( items: &[ShowComp], ops: &[ShellComp], - full_lit: &str, ) -> Result { // Bash is strange when it comes to completion - rather than taking // a glob - _filedir takes an extension which it later to include uppercase @@ -204,7 +199,7 @@ pub(crate) fn render_bash( let mut res = String::new(); if items.is_empty() && ops.is_empty() { - return Ok(format!("COMPREPLY+=({})\n", Shell(full_lit))); + return Ok(String::new()); } let init = "local cur prev words cword ; _init_completion || return ;"; @@ -249,12 +244,11 @@ pub(crate) fn render_bash( pub(crate) fn render_test( items: &[ShowComp], ops: &[ShellComp], - lit: &str, ) -> Result { use std::fmt::Write; if items.is_empty() && ops.is_empty() { - return Ok(format!("{}\n", lit)); + return Ok(String::new()); } if items.len() == 1 && ops.is_empty() && !items[0].subst.is_empty() { @@ -283,13 +277,12 @@ pub(crate) fn render_test( pub(crate) fn render_fish( items: &[ShowComp], ops: &[ShellComp], - full_lit: &str, app: &str, ) -> Result { use std::fmt::Write; let mut res = String::new(); if items.is_empty() && ops.is_empty() { - return Ok(format!("complete -c {} --arguments={}", app, full_lit)); + return Ok(String::new()); } let shared = if ops.is_empty() { "-f " } else { "" }; for item in items.iter().rev().filter(|i| !i.subst.is_empty()) { diff --git a/src/info.rs b/src/info.rs index a90922e6..df46c597 100644 --- a/src/info.rs +++ b/src/info.rs @@ -247,6 +247,10 @@ impl OptionParser { if let Err(Error(Message::ParseFailure(failure))) = res { return Err(failure); } + + #[cfg(feature = "autocomplete")] + args.convert_current_metavar(); + #[cfg(feature = "autocomplete")] if let Some(comp) = args.check_complete() { return Err(ParseFailure::Completion(comp)); diff --git a/src/meta_help.rs b/src/meta_help.rs index d222afd6..7b63c161 100644 --- a/src/meta_help.rs +++ b/src/meta_help.rs @@ -353,6 +353,24 @@ impl Doc { } } +impl std::fmt::Display for Metavar { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self + .0 + .chars() + .all(|c| c.is_uppercase() || c.is_ascii_digit() || c == '-' || c == '_') + { + f.write_str(self.0)?; + } else { + use std::fmt::Write; + f.write_char('<')?; + f.write_str(self.0)?; + f.write_char('>')?; + } + Ok(()) + } +} + #[allow(clippy::too_many_lines)] // lines are _very_ boring fn write_help_item(buf: &mut Doc, item: &HelpItem, include_env: bool) { match item { diff --git a/src/params.rs b/src/params.rs index 8f870a31..6cafc4ee 100644 --- a/src/params.rs +++ b/src/params.rs @@ -435,15 +435,6 @@ impl Parser for ParseCommand { args.take_cmd(&tmp) }) { - #[cfg(feature = "autocomplete")] - if args.touching_last_remove() { - // in completion mode prefer to autocomplete the command name vs going inside the - // parser - args.clear_comps(); - args.push_command(self.longs[0], self.shorts.first().copied(), &self.help); - return Err(Error(Message::Missing(Vec::new()))); - } - if let Some(cur) = args.current { args.set_scope(cur..args.scope().end); } @@ -484,7 +475,7 @@ impl Parser for ParseCommand { } } else { #[cfg(feature = "autocomplete")] - args.push_command(self.longs[0], self.shorts.first().copied(), &self.help); + args.complete_command(&self.longs, &self.shorts, self.help.as_ref()); let missing = MissingItem { item: self.item(), @@ -798,23 +789,46 @@ fn parse_pos_word( Ok((ix, is_strict, word)) => { if strict && !is_strict { #[cfg(feature = "autocomplete")] - args.push_pos_sep(); + todo!(); + // args.push_pos_sep(); return Err(Error(Message::StrictPos(ix, metavar))); } - #[cfg(feature = "autocomplete")] - if args.touching_last_remove() && !args.check_no_pos_ahead() { - args.push_metavar(metavar.0, help, false); - args.set_no_pos_ahead(); - } + // #[cfg(feature = "autocomplete")] + // if args.touching_last_remove() && !args.check_no_pos_ahead() { + // args.push_metavar(metavar.0, help, false); + // args.set_no_pos_ahead(); + // } + Ok(word) } Err(err) => { #[cfg(feature = "autocomplete")] - if !args.check_no_pos_ahead() { - args.push_metavar(metavar.0, help, false); - args.set_no_pos_ahead(); + if let Some(comp) = args.comp_mut() { + if !comp.consumed_as_positional { + comp.consumed_as_positional = true; + let metavar = crate::complete_gen::CurrentMeta { + name: metavar, + help: help.as_ref().and_then(crate::Doc::to_completion), + is_argument: false, + }; + comp.meta = Some(metavar); + // comp.comps2.push(crate::complete_gen::Comp::Metavariable { + // extra: crate::complete_gen::CompExtra { + // depth: 0, + // group: None, + // help: help.as_ref().and_then(crate::Doc::to_completion), + // }, + // meta: metavar.0, + // is_argument: false, + // }); + return Ok(comp.current_arg.clone().into()); + } } + // if !args.check_no_pos_ahead() { + // args.push_metavar(metavar.0, help, false); + // args.set_no_pos_ahead(); + // } Err(err) } } diff --git a/src/structs.rs b/src/structs.rs index baf59fc4..cb6734fb 100644 --- a/src/structs.rs +++ b/src/structs.rs @@ -368,29 +368,57 @@ fn this_or_that_picks_first( }; #[cfg(feature = "autocomplete")] - let len_a = args_a.len(); + { + let mut keep_a = true; + let mut keep_b = true; + println!("\na: {args_a:?}"); + println!("b: {args_b:?}\n"); - #[cfg(feature = "autocomplete")] - let len_b = args_b.len(); + // what happens if we - #[cfg(feature = "autocomplete")] - if let (Some(a), Some(b)) = (args_a.comp_mut(), args_b.comp_mut()) { - // if both parsers managed to consume the same amount - including 0, keep - // results from both, otherwise keep results from one that consumed more - let (keep_a, keep_b) = match res { - Ok((true, _)) => (true, false), - Ok((false, _)) => (false, true), - Err(_) => match len_a.cmp(&len_b) { - std::cmp::Ordering::Less => (true, false), - std::cmp::Ordering::Equal => (true, true), - std::cmp::Ordering::Greater => (false, true), - }, - }; - if keep_a { - comp_stash.extend(a.drain_comps()); + if args_a.len() != args_b.len() { + // If neither parser consumed anything - both can produce valid completions, otherwise + // look for the first "significant" consume and keep that parser + // + // This is needed to preserve completion from a choice between a positional and a flag + // See https://github.com/pacak/bpaf/issues/303 for more details + if let (Some(_), Some(_)) = (args_a.comp_mut(), args_b.comp_mut()) { + 'check: for (ix, arg) in args_a.items.iter().enumerate() { + // During completion process named and unnamed arguments behave + // different - `-` and `--` are positional arguments, but we want to produce + // named items too. An empty string is also a special type of item that + // gets passed when user starts completion without passing any actual data. + // + // All other strings are either valid named items or valid positional items + // those are hopefully follow the right logic for being parsed/not parsed + if ix + 1 == args_a.items.len() { + let os = arg.os_str(); + if os.is_empty() || os == "-" || os == "--" { + break 'check; + } + } + match (args_a.present(ix), args_b.present(ix)) { + (false, true) => { + keep_b = false; + break 'check; + } + (true, false) => { + keep_a = false; + break 'check; + } + _ => {} + } + } + } } - if keep_b { - comp_stash.extend(b.drain_comps()); + + if let (Some(a), Some(b)) = (args_a.comp_mut(), args_b.comp_mut()) { + if dbg!(keep_a) { + comp_stash.extend(a.drain_comps()); + } + if dbg!(keep_b) { + comp_stash.extend(b.drain_comps()); + } } } @@ -968,11 +996,56 @@ where { fn eval(&self, args: &mut State) -> Result { // stash old - let mut comp_items = Vec::new(); - args.swap_comps_with(&mut comp_items); + // let mut comp_items = Vec::new(); + // args.swap_comps_with(&mut comp_items); + + // autocompletion function should only run if inner parser is currently completing a value + // when it does that it sets meta + + let mut meta = None; + if let Some(comp) = args.comp_mut() { + meta = std::mem::take(&mut comp.meta); + } let res = self.inner.eval(args); + if let Some(comp) = args.comp_mut() { + std::mem::swap(&mut meta, &mut comp.meta); + } + let res = res?; + if let Some(comp) = args.comp_mut() { + if let Some(meta) = meta { + let pos = comp.comps2.len(); + for (rep, descr) in (self.op)(&res) { + let group = self.group.clone(); + comp.comps2.push(crate::complete_gen::Comp::Value { + extra: crate::complete_gen::CompExtra { + depth: 0, + group, + help: descr.map(Into::into), + }, + body: rep.into(), + is_argument: meta.is_argument, + }); + } + if comp.comps2.len() - pos > 1 { + comp.comps2.insert( + pos, + crate::complete_gen::Comp::Metavariable { + extra: crate::complete_gen::CompExtra { + depth: 0, + group: None, + help: None, + }, + meta: meta.name.0, + is_argument: meta.is_argument, + }, + ); + } + } + } + + /* // restore old, now metavars added by inner parser, if any, are in comp_items args.swap_comps_with(&mut comp_items); @@ -1011,7 +1084,7 @@ where comp.push_comp(ci); } } - } + }*/ Ok(res) } diff --git a/tests/chezmoi.rs b/tests/chezmoi.rs new file mode 100644 index 00000000..b4d31ba0 --- /dev/null +++ b/tests/chezmoi.rs @@ -0,0 +1,239 @@ +use bpaf::*; +use std::{path::PathBuf, str::FromStr}; + +#[derive(Copy, Clone, Debug)] +pub enum Style { + /// Program is in PATH + InPath, + /// Program is in .utils of chezmoi source state + InSrc, +} + +impl FromStr for Style { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + match s { + "path" => Ok(Style::InPath), + "src" => Ok(Style::InSrc), + _ => Err("Not valid"), + } + } +} + +/// Parser for `--style` +fn style() -> impl Parser