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;