ruff/crates/ruff_server/src/session/settings.rs
Zanie Blue 9cdac2d6fb
Add support for using uv as an alternative formatter backend (#19665)
This adds a new `backend: internal | uv` option to the LSP
`FormatOptions` allowing users to perform document and range formatting
operations though uv. The idea here is to prototype a solution for users
to transition to a `uv format` command without encountering version
mismatches (and consequently, formatting differences) between the LSP's
version of `ruff` and uv's version of `ruff`.

The primarily alternative to this would be to use uv to discover the
`ruff` version used to start the LSP in the first place. However, this
would increase the scope of a minimal `uv format` command beyond "run a
formatter", and raise larger questions about how uv should be used to
coordinate toolchain discovery. I think those are good things to
explore, but I'm hesitant to let them block a `uv format`
implementation. Another downside of using uv to discover `ruff`, is that
it needs to be implemented _outside_ the LSP; e.g., we'd need to change
the instructions on how to run the LSP and implement it in each editor
integration, like the VS Code plugin.

---------

Co-authored-by: Dhruv Manilawala <dhruvmanila@gmail.com>
2025-09-09 20:39:53 +05:30

173 lines
5.6 KiB
Rust

use std::{path::PathBuf, sync::Arc};
use thiserror::Error;
use ruff_linter::RuleSelector;
use ruff_linter::line_width::LineLength;
use ruff_workspace::options::Options;
use crate::{
ClientOptions,
format::FormatBackend,
session::{
Client,
options::{ClientConfiguration, ConfigurationPreference},
},
};
pub struct GlobalClientSettings {
pub(super) options: ClientOptions,
/// Lazily initialized client settings to avoid showing error warnings
/// when a field of the global settings has any errors but the field is overridden
/// in the workspace settings. This can avoid showing unnecessary errors
/// when the workspace settings e.g. select some rules that aren't available in a specific workspace
/// and said workspace overrides the selected rules.
pub(super) settings: std::cell::OnceCell<Arc<ClientSettings>>,
pub(super) client: Client,
}
impl GlobalClientSettings {
pub(super) fn options(&self) -> &ClientOptions {
&self.options
}
fn settings_impl(&self) -> &Arc<ClientSettings> {
self.settings.get_or_init(|| {
let settings = self.options.clone().into_settings();
let settings = match settings {
Ok(settings) => settings,
Err(settings) => {
self.client.show_error_message(
"Ruff received invalid settings from the editor. Refer to the logs for more information."
);
settings
}
};
Arc::new(settings)
})
}
/// Lazily resolves the client options to the settings.
pub(super) fn to_settings(&self) -> &ClientSettings {
self.settings_impl()
}
/// Lazily resolves the client options to the settings.
pub(super) fn to_settings_arc(&self) -> Arc<ClientSettings> {
self.settings_impl().clone()
}
}
/// Resolved client settings for a specific document. These settings are meant to be
/// used directly by the server, and are *not* a 1:1 representation with how the client
/// sends them.
#[derive(Clone, Debug)]
#[cfg_attr(test, derive(PartialEq, Eq))]
#[expect(clippy::struct_excessive_bools)]
pub(crate) struct ClientSettings {
pub(super) fix_all: bool,
pub(super) organize_imports: bool,
pub(super) lint_enable: bool,
pub(super) disable_rule_comment_enable: bool,
pub(super) fix_violation_enable: bool,
pub(super) show_syntax_errors: bool,
pub(super) editor_settings: EditorSettings,
}
/// Contains the resolved values of 'editor settings' - Ruff configuration for the linter/formatter that was passed in via
/// LSP client settings. These fields are optional because we don't want to override file-based linter/formatting settings
/// if these were un-set.
#[derive(Clone, Debug)]
#[cfg_attr(test, derive(Default, PartialEq, Eq))]
pub(crate) struct EditorSettings {
pub(super) configuration: Option<ResolvedConfiguration>,
pub(super) lint_preview: Option<bool>,
pub(super) format_preview: Option<bool>,
pub(super) format_backend: Option<FormatBackend>,
pub(super) select: Option<Vec<RuleSelector>>,
pub(super) extend_select: Option<Vec<RuleSelector>>,
pub(super) ignore: Option<Vec<RuleSelector>>,
pub(super) exclude: Option<Vec<String>>,
pub(super) line_length: Option<LineLength>,
pub(super) configuration_preference: ConfigurationPreference,
}
/// The resolved configuration from the client settings.
#[derive(Clone, Debug)]
#[cfg_attr(test, derive(PartialEq, Eq))]
pub(crate) enum ResolvedConfiguration {
FilePath(PathBuf),
Inline(Box<Options>),
}
impl TryFrom<ClientConfiguration> for ResolvedConfiguration {
type Error = ResolvedConfigurationError;
fn try_from(value: ClientConfiguration) -> Result<Self, Self::Error> {
match value {
ClientConfiguration::String(path) => Ok(ResolvedConfiguration::FilePath(
PathBuf::from(shellexpand::full(&path)?.as_ref()),
)),
ClientConfiguration::Object(map) => {
let options = toml::Table::try_from(map)?.try_into::<Options>()?;
if options.extend.is_some() {
Err(ResolvedConfigurationError::ExtendNotSupported)
} else {
Ok(ResolvedConfiguration::Inline(Box::new(options)))
}
}
}
}
}
/// An error that can occur when trying to resolve the `configuration` value from the client
/// settings.
#[derive(Debug, Error)]
pub(crate) enum ResolvedConfigurationError {
#[error(transparent)]
EnvVarLookupError(#[from] shellexpand::LookupError<std::env::VarError>),
#[error("error serializing configuration to TOML: {0}")]
InvalidToml(#[from] toml::ser::Error),
#[error(transparent)]
InvalidRuffSchema(#[from] toml::de::Error),
#[error("using `extend` is unsupported for inline configuration")]
ExtendNotSupported,
}
impl ClientSettings {
pub(crate) fn fix_all(&self) -> bool {
self.fix_all
}
pub(crate) fn organize_imports(&self) -> bool {
self.organize_imports
}
pub(crate) fn lint(&self) -> bool {
self.lint_enable
}
pub(crate) fn noqa_comments(&self) -> bool {
self.disable_rule_comment_enable
}
pub(crate) fn fix_violation(&self) -> bool {
self.fix_violation_enable
}
pub(crate) fn show_syntax_errors(&self) -> bool {
self.show_syntax_errors
}
pub(crate) fn editor_settings(&self) -> &EditorSettings {
&self.editor_settings
}
}
impl EditorSettings {
pub(crate) fn format_backend(&self) -> FormatBackend {
self.format_backend.unwrap_or_default()
}
}