diff --git a/crates/ruff/src/args.rs b/crates/ruff/src/args.rs index 3b274c2365..19c4f766d1 100644 --- a/crates/ruff/src/args.rs +++ b/crates/ruff/src/args.rs @@ -30,6 +30,8 @@ use ruff_workspace::resolver::ConfigurationTransformer; use rustc_hash::FxHashMap; use toml; +use crate::commands::completions::config::{OptionString, OptionStringParser}; + /// All configuration options that can be passed "globally", /// i.e., can be passed to all subcommands #[derive(Debug, Default, Clone, clap::Args)] @@ -114,7 +116,11 @@ pub enum Command { /// List or describe the available configuration options. Config { /// Config key to show - option: Option, + #[arg( + value_parser = OptionStringParser, + hide_possible_values = true + )] + option: Option, /// Output format #[arg(long, value_enum, default_value = "text")] output_format: HelpFormat, diff --git a/crates/ruff/src/commands/completions/config.rs b/crates/ruff/src/commands/completions/config.rs new file mode 100644 index 0000000000..3d6ddeadc8 --- /dev/null +++ b/crates/ruff/src/commands/completions/config.rs @@ -0,0 +1,158 @@ +use clap::builder::{PossibleValue, TypedValueParser, ValueParserFactory}; +use itertools::Itertools; +use std::str::FromStr; + +use ruff_workspace::{ + options::Options, + options_base::{OptionField, OptionSet, OptionsMetadata, Visit}, +}; + +#[derive(Default)] +struct CollectOptionsVisitor { + values: Vec<(String, String)>, + parents: Vec, +} + +impl IntoIterator for CollectOptionsVisitor { + type Item = (String, String); + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.values.into_iter() + } +} + +impl Visit for CollectOptionsVisitor { + fn record_set(&mut self, name: &str, group: OptionSet) { + let fully_qualified_name = self + .parents + .iter() + .map(String::as_str) + .chain(std::iter::once(name)) + .collect::>() + .join("."); + + // Only add the set to completion list if it has it's own documentation. + self.values.push(( + fully_qualified_name, + group.documentation().unwrap_or("").to_owned(), + )); + + self.parents.push(name.to_owned()); + group.record(self); + self.parents.pop(); + } + + fn record_field(&mut self, name: &str, field: OptionField) { + let fqn = self + .parents + .iter() + .map(String::as_str) + .chain(std::iter::once(name)) + .collect::>() + .join("."); + + self.values.push((fqn, field.doc.to_owned())); + } +} + +/// Opaque type used solely to enable tab completions +/// for `ruff option [OPTION]` command. +#[derive(Clone, Debug)] +pub struct OptionString(String); + +impl From for OptionString { + fn from(s: String) -> Self { + OptionString(s) + } +} + +impl From for String { + fn from(value: OptionString) -> Self { + value.0 + } +} + +impl From<&str> for OptionString { + fn from(s: &str) -> Self { + OptionString(s.to_string()) + } +} + +impl std::ops::Deref for OptionString { + type Target = str; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl FromStr for OptionString { + type Err = (); + + fn from_str(s: &str) -> Result { + Options::metadata() + .has(s) + .then(|| OptionString(s.to_owned())) + .ok_or(()) + } +} + +#[derive(Clone)] +pub struct OptionStringParser; + +impl ValueParserFactory for OptionString { + type Parser = OptionStringParser; + + fn value_parser() -> Self::Parser { + OptionStringParser + } +} + +impl TypedValueParser for OptionStringParser { + type Value = OptionString; + + fn parse_ref( + &self, + cmd: &clap::Command, + arg: Option<&clap::Arg>, + value: &std::ffi::OsStr, + ) -> Result { + let value = value + .to_str() + .ok_or_else(|| clap::Error::new(clap::error::ErrorKind::InvalidUtf8))?; + + value.parse().map_err(|()| { + let mut error = clap::Error::new(clap::error::ErrorKind::ValueValidation).with_cmd(cmd); + if let Some(arg) = arg { + error.insert( + clap::error::ContextKind::InvalidArg, + clap::error::ContextValue::String(arg.to_string()), + ); + } + error.insert( + clap::error::ContextKind::InvalidValue, + clap::error::ContextValue::String(value.to_string()), + ); + error + }) + } + + fn possible_values(&self) -> Option + '_>> { + let mut visitor = CollectOptionsVisitor::default(); + Options::metadata().record(&mut visitor); + + Some(Box::new(visitor.into_iter().map(|(name, doc)| { + let first_paragraph = doc + .lines() + .take_while(|line| !line.trim_end().is_empty()) + // Replace double quotes with single quotes,to avoid clap's lack of escaping + // when creating zsh completions. This has no security implications, as it only + // affects the help string, which is never executed + .map(|s| s.replace('"', "'")) + .join(" "); + + PossibleValue::new(name).help(first_paragraph) + }))) + } +} diff --git a/crates/ruff/src/commands/completions/mod.rs b/crates/ruff/src/commands/completions/mod.rs new file mode 100644 index 0000000000..30e489dae6 --- /dev/null +++ b/crates/ruff/src/commands/completions/mod.rs @@ -0,0 +1 @@ +pub(crate) mod config; diff --git a/crates/ruff/src/commands/mod.rs b/crates/ruff/src/commands/mod.rs index 4d463a4ef5..9358c76877 100644 --- a/crates/ruff/src/commands/mod.rs +++ b/crates/ruff/src/commands/mod.rs @@ -1,3 +1,5 @@ +pub(crate) mod completions; + pub(crate) mod add_noqa; pub(crate) mod analyze_graph; pub(crate) mod check;