diff --git a/Cargo.lock b/Cargo.lock index 3bc2c0d97d..5c4e4c1019 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2822,6 +2822,7 @@ dependencies = [ "ruff_python_formatter", "ruff_python_parser", "ruff_python_trivia", + "ruff_server", "ruff_workspace", "schemars", "serde", @@ -3172,6 +3173,7 @@ dependencies = [ "ruff_diagnostics", "ruff_formatter", "ruff_linter", + "ruff_macros", "ruff_notebook", "ruff_python_ast", "ruff_python_codegen", diff --git a/crates/ruff_dev/Cargo.toml b/crates/ruff_dev/Cargo.toml index 9432476a5a..33debdec93 100644 --- a/crates/ruff_dev/Cargo.toml +++ b/crates/ruff_dev/Cargo.toml @@ -22,6 +22,7 @@ ruff_python_codegen = { workspace = true } ruff_python_formatter = { workspace = true } ruff_python_parser = { workspace = true } ruff_python_trivia = { workspace = true } +ruff_server = { workspace = true } ruff_workspace = { workspace = true, features = ["schemars"] } anyhow = { workspace = true } diff --git a/crates/ruff_dev/src/generate_options.rs b/crates/ruff_dev/src/generate_options.rs index 652cc4d2ee..32c2b8766a 100644 --- a/crates/ruff_dev/src/generate_options.rs +++ b/crates/ruff_dev/src/generate_options.rs @@ -2,6 +2,7 @@ //! //! Used for . use itertools::Itertools; +use ruff_server::ClientSettings; use std::fmt::Write; use ruff_python_trivia::textwrap; @@ -15,12 +16,32 @@ pub(crate) fn generate() -> String { &mut output, Set::Toplevel(Options::metadata()), &mut Vec::new(), + SetKind::Ruff, ); output } -fn generate_set(output: &mut String, set: Set, parents: &mut Vec) { +pub(crate) fn generate_server_options() -> String { + let mut output = String::new(); + + generate_set( + &mut output, + Set::Toplevel(ClientSettings::metadata()), + &mut Vec::new(), + SetKind::RuffServer, + ); + + output +} + +#[derive(Copy, Clone)] +enum SetKind { + Ruff, + RuffServer, +} + +fn generate_set(output: &mut String, set: Set, parents: &mut Vec, set_kind: SetKind) { match &set { Set::Toplevel(_) => { output.push_str("### Top-level\n"); @@ -53,7 +74,7 @@ fn generate_set(output: &mut String, set: Set, parents: &mut Vec) { // Generate the fields. for (name, field) in &fields { - emit_field(output, name, field, parents.as_slice()); + emit_field(output, name, field, parents.as_slice(), set_kind); output.push_str("---\n\n"); } @@ -66,6 +87,7 @@ fn generate_set(output: &mut String, set: Set, parents: &mut Vec) { set: *sub_set, }, parents, + set_kind, ); } @@ -93,7 +115,13 @@ impl Set { } } -fn emit_field(output: &mut String, name: &str, field: &OptionField, parents: &[Set]) { +fn emit_field( + output: &mut String, + name: &str, + field: &OptionField, + parents: &[Set], + set_kind: SetKind, +) { let header_level = if parents.is_empty() { "####" } else { "#####" }; let parents_anchor = parents.iter().filter_map(|parent| parent.name()).join("_"); @@ -137,28 +165,46 @@ fn emit_field(output: &mut String, name: &str, field: &OptionField, parents: &[S output.push_str(&format!("**Type**: `{}`\n", field.value_type)); output.push('\n'); output.push_str("**Example usage**:\n\n"); - output.push_str(&format_tab( - "pyproject.toml", - &format_header(field.scope, parents, ConfigurationFile::PyprojectToml), - field.example, - )); - output.push_str(&format_tab( - "ruff.toml", - &format_header(field.scope, parents, ConfigurationFile::RuffToml), - field.example, - )); + + match set_kind { + SetKind::Ruff => { + output.push_str(&format_tab( + "pyproject.toml", + &format_content(field, parents, ConfigurationFile::PyprojectToml), + )); + output.push_str(&format_tab( + "ruff.toml", + &format_content(field, parents, ConfigurationFile::RuffToml), + )); + } + SetKind::RuffServer => {} + } + output.push('\n'); } -fn format_tab(tab_name: &str, header: &str, content: &str) -> String { +fn format_tab(tab_name: &str, content: &str) -> String { format!( - "=== \"{}\"\n\n ```toml\n {}\n{}\n ```\n", + "=== \"{}\"\n\n{}\n\n", tab_name, - header, textwrap::indent(content, " ") ) } +fn format_content( + field: &OptionField, + parents: &[Set], + configuration: ConfigurationFile, +) -> String { + let header = format_header(field.scope, parents, configuration); + + format!( + "```toml\n{}\n{}\n```", + header, + textwrap::indent(field.example, " ") + ) +} + /// Format the TOML header for the example usage for a given option. /// /// For example: `[tool.ruff.format]` or `[tool.ruff.lint.isort]`. @@ -187,6 +233,15 @@ enum ConfigurationFile { RuffToml, } +fn format_server_content(field: &OptionField, editor: Editor) -> String {} + +#[derive(Debug, Copy, Clone)] +enum Editor { + VSCode, + Neovim, + Zed, +} + #[derive(Default)] struct CollectOptionsVisitor { groups: Vec<(String, OptionSet)>, diff --git a/crates/ruff_dev/src/main.rs b/crates/ruff_dev/src/main.rs index 5fb10476da..2623786873 100644 --- a/crates/ruff_dev/src/main.rs +++ b/crates/ruff_dev/src/main.rs @@ -46,6 +46,8 @@ enum Command { GenerateRulesTable, /// Generate a Markdown-compatible listing of configuration options. GenerateOptions, + /// Generate a Markdown-compatible listing of server options. + GenerateServerOptions, /// Generate CLI help. GenerateCliHelp(generate_cli_help::Args), /// Generate Markdown docs. @@ -89,6 +91,9 @@ fn main() -> Result { Command::GenerateKnotSchema(args) => generate_knot_schema::main(&args)?, Command::GenerateRulesTable => println!("{}", generate_rules_table::generate()), Command::GenerateOptions => println!("{}", generate_options::generate()), + Command::GenerateServerOptions => { + println!("{}", generate_options::generate_server_options()) + } Command::GenerateCliHelp(args) => generate_cli_help::main(&args)?, Command::GenerateDocs(args) => generate_docs::main(&args)?, Command::PrintAST(args) => print_ast::main(&args)?, diff --git a/crates/ruff_macros/src/config.rs b/crates/ruff_macros/src/config.rs index 0df56a2256..30509f29c5 100644 --- a/crates/ruff_macros/src/config.rs +++ b/crates/ruff_macros/src/config.rs @@ -24,19 +24,22 @@ pub(crate) fn derive_impl(input: DeriveInput) -> syn::Result { }) => { let mut output = vec![]; + let rename_value = + RenameValue::from_attributes(struct_attributes.as_slice()).unwrap_or_default(); + for field in &fields.named { if let Some(attr) = field .attrs .iter() .find(|attr| attr.path().is_ident("option")) { - output.push(handle_option(field, attr)?); + output.push(handle_option(field, attr, rename_value)?); } else if field .attrs .iter() .any(|attr| attr.path().is_ident("option_group")) { - output.push(handle_option_group(field)?); + output.push(handle_option_group(field, rename_value)?); } else if let Some(serde) = field .attrs .iter() @@ -86,8 +89,8 @@ pub(crate) fn derive_impl(input: DeriveInput) -> syn::Result { Ok(quote! { #[automatically_derived] - impl crate::options_base::OptionsMetadata for #ident { - fn record(visit: &mut dyn crate::options_base::Visit) { + impl ruff_workspace::options_base::OptionsMetadata for #ident { + fn record(visit: &mut dyn ruff_workspace::options_base::Visit) { #(#output);* } @@ -105,7 +108,10 @@ pub(crate) fn derive_impl(input: DeriveInput) -> syn::Result { /// For a field with type `Option` where `Foobar` itself is a struct /// deriving `ConfigurationOptions`, create code that calls retrieves options /// from that group: `Foobar::get_available_options()` -fn handle_option_group(field: &Field) -> syn::Result { +fn handle_option_group( + field: &Field, + rename_value: RenameValue, +) -> syn::Result { let ident = field .ident .as_ref() @@ -122,10 +128,10 @@ fn handle_option_group(field: &Field) -> syn::Result { PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }), }) if type_ident == "Option" => { let path = &args[0]; - let kebab_name = LitStr::new(&ident.to_string().replace('_', "-"), ident.span()); + let renamed_field = rename_value.apply(ident); Ok(quote_spanned!( - ident.span() => (visit.record_set(#kebab_name, crate::options_base::OptionSet::of::<#path>())) + ident.span() => (visit.record_set(#renamed_field, ruff_workspace::options_base::OptionSet::of::<#path>())) )) } _ => Err(syn::Error::new( @@ -154,7 +160,11 @@ fn parse_doc(doc: &Attribute) -> syn::Result { /// Parse an `#[option(doc="...", default="...", value_type="...", /// example="...")]` attribute and return data in the form of an `OptionField`. -fn handle_option(field: &Field, attr: &Attribute) -> syn::Result { +fn handle_option( + field: &Field, + attr: &Attribute, + rename_value: RenameValue, +) -> syn::Result { let docs: Vec<&Attribute> = field .attrs .iter() @@ -190,8 +200,7 @@ fn handle_option(field: &Field, attr: &Attribute) -> syn::Result syn::Result { - visit.record_field(#kebab_name, crate::options_base::OptionField{ + visit.record_field(#renamed_field, ruff_workspace::options_base::OptionField{ doc: &#doc, default: &#default, value_type: &#value_type, @@ -351,3 +360,66 @@ struct DeprecatedAttribute { since: Option, note: Option, } + +#[derive(Copy, Clone, Default)] +enum RenameValue { + #[default] + KebabCase, + CamelCase, +} + +impl RenameValue { + fn from_attributes(attrs: &[Attribute]) -> Option { + let serde = attrs.iter().find(|attr| attr.path().is_ident("serde"))?; + + let Meta::List(list) = &serde.meta else { + return None; + }; + + let mut rename_value = None; + + let _ = list.parse_nested_meta(|meta| { + if meta.path.is_ident("rename_all") { + let value = meta.value()?; + let s: LitStr = value.parse()?; + match s.value().as_str() { + "kebab-case" => { + rename_value = Some(RenameValue::KebabCase); + Ok(()) + } + "camelCase" => { + rename_value = Some(RenameValue::CamelCase); + Ok(()) + } + _ => Err(meta.error("Expected `kebab-case` or `camelCase`")), + } + } else { + Err(meta.error("Expected `rename_all`")) + } + }); + + rename_value + } + + fn apply(self, ident: &syn::Ident) -> syn::LitStr { + let renamed = match self { + RenameValue::KebabCase => ident.to_string().replace('_', "-"), + RenameValue::CamelCase => { + let mut result = String::new(); + let mut capitalize = false; + for c in ident.to_string().chars() { + if c == '_' { + capitalize = true; + } else if capitalize { + result.push(c.to_ascii_uppercase()); + capitalize = false; + } else { + result.push(c); + } + } + result + } + }; + LitStr::new(&renamed, ident.span()) + } +} diff --git a/crates/ruff_server/Cargo.toml b/crates/ruff_server/Cargo.toml index c9678e1a12..1dc24a7ce0 100644 --- a/crates/ruff_server/Cargo.toml +++ b/crates/ruff_server/Cargo.toml @@ -16,6 +16,7 @@ license = { workspace = true } ruff_diagnostics = { workspace = true } ruff_formatter = { workspace = true } ruff_linter = { workspace = true } +ruff_macros = { workspace = true } ruff_notebook = { workspace = true } ruff_python_ast = { workspace = true } ruff_python_codegen = { workspace = true } diff --git a/crates/ruff_server/src/session/settings.rs b/crates/ruff_server/src/session/settings.rs index 50bbd41307..3192586091 100644 --- a/crates/ruff_server/src/session/settings.rs +++ b/crates/ruff_server/src/session/settings.rs @@ -5,6 +5,8 @@ use rustc_hash::FxHashMap; use serde::Deserialize; use ruff_linter::{line_width::LineLength, RuleSelector}; +use ruff_macros::OptionsMetadata; +use ruff_workspace::options_base::{OptionField, OptionsMetadata}; /// Maps a workspace URI to its associated client settings. Used during server initialization. pub(crate) type WorkspaceSettingsMap = FxHashMap; @@ -57,27 +59,76 @@ pub(crate) enum ConfigurationPreference { EditorOnly, } -/// This is a direct representation of the settings schema sent by the client. -#[derive(Debug, Deserialize, Default)] +#[derive(Debug, Deserialize, Default, OptionsMetadata)] #[cfg_attr(test, derive(PartialEq, Eq))] #[serde(rename_all = "camelCase")] pub struct ClientSettings { + /// Path to a `ruff.toml` or `pyproject.toml` file to use for configuration. + /// + /// By default, Ruff will discover configuration for each project from the filesystem, + /// mirroring the behavior of the Ruff CLI. + #[option( + default = r#"null"#, + value_type = "string", + example = r#""~/path/to/ruff.toml""# + )] configuration: Option, - fix_all: Option, - organize_imports: Option, - lint: Option, - format: Option, - code_action: Option, - exclude: Option>, - line_length: Option, + + /// The strategy to use when resolving settings across VS Code and the filesystem. By default, + /// editor configuration is prioritized over `ruff.toml` and `pyproject.toml` files. + /// + /// * `"editorFirst"`: Editor settings take priority over configuration files present in the + /// workspace. + /// * `"filesystemFirst"`: Configuration files present in the workspace takes priority over + /// editor settings. + /// * `"editorOnly"`: Ignore configuration files entirely i.e., only use editor settings. + #[option( + default = r#""editorFirst""#, + value_type = r#""editorFirst" | "filesystemFirst" | "editorOnly""#, + example = r#""filesystemFirst""# + )] configuration_preference: Option, - /// If `true` or [`None`], show syntax errors as diagnostics. + /// A list of file patterns to exclude from linting and formatting. See [the + /// documentation](https://docs.astral.sh/ruff/settings/#exclude) for more details. + #[option( + default = r#"null"#, + value_type = "string[]", + example = r#"["**/tests/**"]"# + )] + exclude: Option>, + + /// The line length to use for the linter and formatter. + #[option(default = "null", value_type = "int", example = "100")] + line_length: Option, + + /// Whether to register the server as capable of handling `source.fixAll` code actions. + #[option(default = "true", value_type = "bool", example = "false")] + fix_all: Option, + + /// Whether to register the server as capable of handling `source.organizeImports` code + /// actions. + #[option(default = "true", value_type = "bool", example = "false")] + organize_imports: Option, + + /// _New in Ruff [v0.5.0](https://astral.sh/blog/ruff-v0.5.0#changes-to-e999-and-reporting-of-syntax-errors)_ + /// + /// Whether to show syntax error diagnostics. /// /// This is useful when using Ruff with other language servers, allowing the user to refer /// to syntax errors from only one source. + #[option(default = "true", value_type = "bool", example = "false")] show_syntax_errors: Option, + #[option_group] + lint: Option, + + #[option_group] + format: Option, + + #[option_group] + code_action: Option, + // These settings are only needed for tracing, and are only read from the global configuration. // These will not be in the resolved settings. #[serde(flatten)] @@ -98,14 +149,26 @@ impl ClientSettings { } } -/// Settings needed to initialize tracing. These will only be -/// read from the global configuration. -#[derive(Debug, Deserialize, Default)] +#[derive(Debug, Deserialize, Default, OptionsMetadata)] #[cfg_attr(test, derive(PartialEq, Eq))] #[serde(rename_all = "camelCase")] pub(crate) struct TracingSettings { + /// The log level to use for the server. + #[option( + default = r#""info""#, + value_type = r#""error" | "warn" | "info" | "debug" | "trace""#, + example = r#""debug""# + )] pub(crate) log_level: Option, - /// Path to the log file - tildes and environment variables are supported. + + /// Path to the log file to use for the server. + /// + /// If not set, logs will be written to stderr. Tildes and environment variables are expanded. + #[option( + default = r#"null"#, + value_type = "string", + example = r#""~/path/to/ruff.log""# + )] pub(crate) log_file: Option, } @@ -121,14 +184,31 @@ struct WorkspaceSettings { workspace: Url, } -#[derive(Debug, Default, Deserialize)] +/// Settings specific to the Ruff linter. +#[derive(Debug, Default, Deserialize, OptionsMetadata)] #[cfg_attr(test, derive(PartialEq, Eq))] #[serde(rename_all = "camelCase")] struct LintOptions { + /// Whether to enable linting. Set to `false` to use Ruff exclusively as a formatter. + #[option(default = "true", value_type = "bool", example = "false")] enable: Option, + + /// Whether to enable Ruff's preview mode when linting. + #[option(default = "null", value_type = "bool", example = "true")] preview: Option, + + /// Rules to enable by default. See [the + /// documentation](https://docs.astral.sh/ruff/settings/#lint_select). + #[option(default = "null", value_type = "string[]", example = r#"["E", "F"]"#)] select: Option>, + + /// Rules to enable in addition to those in [`lint.select`](#select). + #[option(default = "null", value_type = "string[]", example = r#"["W"]"#)] extend_select: Option>, + + /// Rules to disable by default. See [the + /// documentation](https://docs.astral.sh/ruff/settings/#lint_ignore). + #[option(default = "null", value_type = "string[]", example = r#"["E4", "E7"]"#)] ignore: Option>, } @@ -143,10 +223,13 @@ impl LintOptions { } } -#[derive(Debug, Default, Deserialize)] +/// Settings specific to the Ruff formatter. +#[derive(Debug, Default, Deserialize, OptionsMetadata)] #[cfg_attr(test, derive(PartialEq, Eq))] #[serde(rename_all = "camelCase")] struct FormatOptions { + /// Whether to enable Ruff's preview mode when formatting. + #[option(default = "null", value_type = "bool", example = "true")] preview: Option, } @@ -176,6 +259,38 @@ struct CodeActionParameters { enable: Option, } +impl OptionsMetadata for CodeActionOptions { + fn record(visit: &mut dyn ruff_workspace::options_base::Visit) { + visit.record_field( + "disableRuleComment.enable", + OptionField { + doc: "Whether to display Quick Fix actions to disable rules via `noqa` suppression comments.", + default: "true", + value_type: "bool", + scope: None, + example: "false", + deprecated: None, + }, + ); + + visit.record_field( + "fixViolation.enable", + OptionField { + doc: "Whether to display Quick Fix actions to autofix violations.", + default: "true", + value_type: "bool", + scope: None, + example: "false", + deprecated: None, + }, + ); + } + + fn documentation() -> Option<&'static str> { + Some("Enable or disable code actions provided by the server.") + } +} + /// This is the exact schema for initialization options sent in by the client /// during initialization. #[derive(Debug, Deserialize)] diff --git a/crates/ruff_workspace/src/options.rs b/crates/ruff_workspace/src/options.rs index 88994f6ecc..7b21fa7bd7 100644 --- a/crates/ruff_workspace/src/options.rs +++ b/crates/ruff_workspace/src/options.rs @@ -6,6 +6,7 @@ use std::collections::{BTreeMap, BTreeSet}; use std::path::PathBuf; use strum::IntoEnumIterator; +use crate as ruff_workspace; use crate::options_base::{OptionsMetadata, Visit}; use crate::settings::LineEnding; use ruff_formatter::IndentStyle;