From 646f1942aa6543f141741025c853d7454ccca346 Mon Sep 17 00:00:00 2001 From: Mike Perlov Date: Mon, 27 Jan 2025 10:09:04 -0500 Subject: [PATCH] Implement tab autocomplete for `ruff config` (#15603) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Not the most important feature, but hey... was marked as the good first issue ;-) fixes #4551 Unfortunately, looks like clap only generates proper completions for zsh, so this would not make any difference for bash/fish. ## Test Plan - cargo nextest run - manual test by sourcing completions and then triggering autocomplete: ```shell misha@PandaBook ruff % source <(target/debug/ruff generate-shell-completion zsh) misha@PandaBook ruff % target/debug/ruff config lin line-length -- The line length to use when enforcing long-lines violations lint -- Configures how Ruff checks your code. lint.allowed-confusables -- A list of allowed 'confusable' Unicode characters to ignore lint.dummy-variable-rgx -- A regular expression used to identify 'dummy' variables, or lint.exclude -- A list of file patterns to exclude from linting in addition lint.explicit-preview-rules -- Whether to require exact codes to select preview rules. Whe lint.extend-fixable -- A list of rule codes or prefixes to consider fixable, in ad lint.extend-ignore -- A list of rule codes or prefixes to ignore, in addition to lint.extend-per-file-ignores -- A list of mappings from file pattern to rule codes or prefi lint.extend-safe-fixes -- A list of rule codes or prefixes for which unsafe fixes sho lint.extend-select -- A list of rule codes or prefixes to enable, in addition to lint.extend-unsafe-fixes -- A list of rule codes or prefixes for which safe fixes shoul lint.external -- A list of rule codes or prefixes that are unsupported by Ru lint.fixable -- A list of rule codes or prefixes to consider fixable. By de lint.flake8-annotations -- Print a list of available options lint.flake8-annotations.allow-star-arg-any -- Whether to suppress `ANN401` for dynamically typed `*args` ... ``` - check command help ```shell ❯ target/debug/ruff config -h List or describe the available configuration options Usage: ruff config [OPTIONS] [OPTION] Arguments: [OPTION] Config key to show Options: --output-format Output format [default: text] [possible values: text, json] -h, --help Print help Log levels: -v, --verbose Enable verbose logging -q, --quiet Print diagnostics, but nothing else -s, --silent Disable all logging (but still exit with status code "1" upon detecting diagnostics) Global options: --config Either a path to a TOML configuration file (`pyproject.toml` or `ruff.toml`), or a TOML ` = ` pair (such as you might find in a `ruff.toml` configuration file) overriding a specific configuration option. Overrides of individual settings using this option always take precedence over all configuration files, including configuration files that were also specified using `--config` --isolated Ignore all configuration files ``` - running original command ```shell ❯ target/debug/ruff config cache-dir extend output-format fix unsafe-fixes fix-only show-fixes required-version preview exclude extend-exclude extend-include force-exclude include respect-gitignore builtins namespace-packages target-version src line-length indent-width lint format analyze ``` --- crates/ruff/src/args.rs | 8 +- .../ruff/src/commands/completions/config.rs | 158 ++++++++++++++++++ crates/ruff/src/commands/completions/mod.rs | 1 + crates/ruff/src/commands/mod.rs | 2 + 4 files changed, 168 insertions(+), 1 deletion(-) create mode 100644 crates/ruff/src/commands/completions/config.rs create mode 100644 crates/ruff/src/commands/completions/mod.rs 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;