mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 21:34:57 +00:00
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>
This commit is contained in:
parent
79706a2e26
commit
9cdac2d6fb
12 changed files with 696 additions and 22 deletions
12
.github/workflows/ci.yaml
vendored
12
.github/workflows/ci.yaml
vendored
|
@ -259,6 +259,10 @@ jobs:
|
||||||
uses: taiki-e/install-action@6064345e6658255e90e9500fdf9a06ab77e6909c # v2.57.6
|
uses: taiki-e/install-action@6064345e6658255e90e9500fdf9a06ab77e6909c # v2.57.6
|
||||||
with:
|
with:
|
||||||
tool: cargo-insta
|
tool: cargo-insta
|
||||||
|
- name: "Install uv"
|
||||||
|
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
|
||||||
|
with:
|
||||||
|
enable-cache: "true"
|
||||||
- name: ty mdtests (GitHub annotations)
|
- name: ty mdtests (GitHub annotations)
|
||||||
if: ${{ needs.determine_changes.outputs.ty == 'true' }}
|
if: ${{ needs.determine_changes.outputs.ty == 'true' }}
|
||||||
env:
|
env:
|
||||||
|
@ -317,6 +321,10 @@ jobs:
|
||||||
uses: taiki-e/install-action@6064345e6658255e90e9500fdf9a06ab77e6909c # v2.57.6
|
uses: taiki-e/install-action@6064345e6658255e90e9500fdf9a06ab77e6909c # v2.57.6
|
||||||
with:
|
with:
|
||||||
tool: cargo-insta
|
tool: cargo-insta
|
||||||
|
- name: "Install uv"
|
||||||
|
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
|
||||||
|
with:
|
||||||
|
enable-cache: "true"
|
||||||
- name: "Run tests"
|
- name: "Run tests"
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
|
@ -340,6 +348,10 @@ jobs:
|
||||||
uses: taiki-e/install-action@6064345e6658255e90e9500fdf9a06ab77e6909c # v2.57.6
|
uses: taiki-e/install-action@6064345e6658255e90e9500fdf9a06ab77e6909c # v2.57.6
|
||||||
with:
|
with:
|
||||||
tool: cargo-nextest
|
tool: cargo-nextest
|
||||||
|
- name: "Install uv"
|
||||||
|
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
|
||||||
|
with:
|
||||||
|
enable-cache: "true"
|
||||||
- name: "Run tests"
|
- name: "Run tests"
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
|
|
|
@ -81,14 +81,19 @@ impl IndentStyle {
|
||||||
pub const fn is_space(&self) -> bool {
|
pub const fn is_space(&self) -> bool {
|
||||||
matches!(self, IndentStyle::Space)
|
matches!(self, IndentStyle::Space)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the string representation of the indent style.
|
||||||
|
pub const fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
IndentStyle::Tab => "tab",
|
||||||
|
IndentStyle::Space => "space",
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for IndentStyle {
|
impl std::fmt::Display for IndentStyle {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
f.write_str(self.as_str())
|
||||||
IndentStyle::Tab => std::write!(f, "tab"),
|
|
||||||
IndentStyle::Space => std::write!(f, "space"),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -139,4 +139,16 @@ impl LineEnding {
|
||||||
LineEnding::CarriageReturn => "\r",
|
LineEnding::CarriageReturn => "\r",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the string used to configure this line ending.
|
||||||
|
///
|
||||||
|
/// See [`LineEnding::as_str`] for the actual string representation of the line ending.
|
||||||
|
#[inline]
|
||||||
|
pub const fn as_setting_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
LineEnding::LineFeed => "lf",
|
||||||
|
LineEnding::CarriageReturnLineFeed => "crlf",
|
||||||
|
LineEnding::CarriageReturn => "cr",
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -252,15 +252,20 @@ impl QuoteStyle {
|
||||||
pub const fn is_preserve(self) -> bool {
|
pub const fn is_preserve(self) -> bool {
|
||||||
matches!(self, QuoteStyle::Preserve)
|
matches!(self, QuoteStyle::Preserve)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the string representation of the quote style.
|
||||||
|
pub const fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
QuoteStyle::Single => "single",
|
||||||
|
QuoteStyle::Double => "double",
|
||||||
|
QuoteStyle::Preserve => "preserve",
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for QuoteStyle {
|
impl fmt::Display for QuoteStyle {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
f.write_str(self.as_str())
|
||||||
Self::Single => write!(f, "single"),
|
|
||||||
Self::Double => write!(f, "double"),
|
|
||||||
Self::Preserve => write!(f, "preserve"),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -302,10 +307,10 @@ impl MagicTrailingComma {
|
||||||
|
|
||||||
impl fmt::Display for MagicTrailingComma {
|
impl fmt::Display for MagicTrailingComma {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
f.write_str(match self {
|
||||||
Self::Respect => write!(f, "respect"),
|
MagicTrailingComma::Respect => "respect",
|
||||||
Self::Ignore => write!(f, "ignore"),
|
MagicTrailingComma::Ignore => "ignore",
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -50,5 +50,8 @@ insta = { workspace = true }
|
||||||
[target.'cfg(target_vendor = "apple")'.dependencies]
|
[target.'cfg(target_vendor = "apple")'.dependencies]
|
||||||
libc = { workspace = true }
|
libc = { workspace = true }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
test-uv = []
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
|
@ -1,18 +1,52 @@
|
||||||
|
use std::io::Write;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
use std::process::{Command, Stdio};
|
||||||
|
|
||||||
use ruff_formatter::PrintedRange;
|
use anyhow::Context;
|
||||||
|
|
||||||
|
use ruff_formatter::{FormatOptions, PrintedRange};
|
||||||
use ruff_python_ast::PySourceType;
|
use ruff_python_ast::PySourceType;
|
||||||
use ruff_python_formatter::{FormatModuleError, format_module_source};
|
use ruff_python_formatter::{FormatModuleError, PyFormatOptions, format_module_source};
|
||||||
|
use ruff_source_file::LineIndex;
|
||||||
use ruff_text_size::TextRange;
|
use ruff_text_size::TextRange;
|
||||||
use ruff_workspace::FormatterSettings;
|
use ruff_workspace::FormatterSettings;
|
||||||
|
|
||||||
use crate::edit::TextDocument;
|
use crate::edit::TextDocument;
|
||||||
|
|
||||||
|
/// The backend to use for formatting.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Deserialize)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub(crate) enum FormatBackend {
|
||||||
|
/// Use the built-in Ruff formatter.
|
||||||
|
///
|
||||||
|
/// The formatter version will match the LSP version.
|
||||||
|
#[default]
|
||||||
|
Internal,
|
||||||
|
/// Use uv for formatting.
|
||||||
|
///
|
||||||
|
/// The formatter version may differ from the LSP version.
|
||||||
|
Uv,
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn format(
|
pub(crate) fn format(
|
||||||
document: &TextDocument,
|
document: &TextDocument,
|
||||||
source_type: PySourceType,
|
source_type: PySourceType,
|
||||||
formatter_settings: &FormatterSettings,
|
formatter_settings: &FormatterSettings,
|
||||||
path: &Path,
|
path: &Path,
|
||||||
|
backend: FormatBackend,
|
||||||
|
) -> crate::Result<Option<String>> {
|
||||||
|
match backend {
|
||||||
|
FormatBackend::Uv => format_external(document, source_type, formatter_settings, path),
|
||||||
|
FormatBackend::Internal => format_internal(document, source_type, formatter_settings, path),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Format using the built-in Ruff formatter.
|
||||||
|
fn format_internal(
|
||||||
|
document: &TextDocument,
|
||||||
|
source_type: PySourceType,
|
||||||
|
formatter_settings: &FormatterSettings,
|
||||||
|
path: &Path,
|
||||||
) -> crate::Result<Option<String>> {
|
) -> crate::Result<Option<String>> {
|
||||||
let format_options =
|
let format_options =
|
||||||
formatter_settings.to_format_options(source_type, document.contents(), Some(path));
|
formatter_settings.to_format_options(source_type, document.contents(), Some(path));
|
||||||
|
@ -35,12 +69,44 @@ pub(crate) fn format(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Format using an external uv command.
|
||||||
|
fn format_external(
|
||||||
|
document: &TextDocument,
|
||||||
|
source_type: PySourceType,
|
||||||
|
formatter_settings: &FormatterSettings,
|
||||||
|
path: &Path,
|
||||||
|
) -> crate::Result<Option<String>> {
|
||||||
|
let format_options =
|
||||||
|
formatter_settings.to_format_options(source_type, document.contents(), Some(path));
|
||||||
|
let uv_command = UvFormatCommand::from(format_options);
|
||||||
|
uv_command.format_document(document.contents(), path)
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn format_range(
|
pub(crate) fn format_range(
|
||||||
document: &TextDocument,
|
document: &TextDocument,
|
||||||
source_type: PySourceType,
|
source_type: PySourceType,
|
||||||
formatter_settings: &FormatterSettings,
|
formatter_settings: &FormatterSettings,
|
||||||
range: TextRange,
|
range: TextRange,
|
||||||
path: &Path,
|
path: &Path,
|
||||||
|
backend: FormatBackend,
|
||||||
|
) -> crate::Result<Option<PrintedRange>> {
|
||||||
|
match backend {
|
||||||
|
FormatBackend::Uv => {
|
||||||
|
format_range_external(document, source_type, formatter_settings, range, path)
|
||||||
|
}
|
||||||
|
FormatBackend::Internal => {
|
||||||
|
format_range_internal(document, source_type, formatter_settings, range, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Format range using the built-in Ruff formatter
|
||||||
|
fn format_range_internal(
|
||||||
|
document: &TextDocument,
|
||||||
|
source_type: PySourceType,
|
||||||
|
formatter_settings: &FormatterSettings,
|
||||||
|
range: TextRange,
|
||||||
|
path: &Path,
|
||||||
) -> crate::Result<Option<PrintedRange>> {
|
) -> crate::Result<Option<PrintedRange>> {
|
||||||
let format_options =
|
let format_options =
|
||||||
formatter_settings.to_format_options(source_type, document.contents(), Some(path));
|
formatter_settings.to_format_options(source_type, document.contents(), Some(path));
|
||||||
|
@ -63,6 +129,198 @@ pub(crate) fn format_range(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Format range using an external command, i.e., `uv`.
|
||||||
|
fn format_range_external(
|
||||||
|
document: &TextDocument,
|
||||||
|
source_type: PySourceType,
|
||||||
|
formatter_settings: &FormatterSettings,
|
||||||
|
range: TextRange,
|
||||||
|
path: &Path,
|
||||||
|
) -> crate::Result<Option<PrintedRange>> {
|
||||||
|
let format_options =
|
||||||
|
formatter_settings.to_format_options(source_type, document.contents(), Some(path));
|
||||||
|
let uv_command = UvFormatCommand::from(format_options);
|
||||||
|
|
||||||
|
// Format the range using uv and convert the result to `PrintedRange`
|
||||||
|
match uv_command.format_range(document.contents(), range, path, document.index())? {
|
||||||
|
Some(formatted) => Ok(Some(PrintedRange::new(formatted, range))),
|
||||||
|
None => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builder for uv format commands
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(crate) struct UvFormatCommand {
|
||||||
|
options: PyFormatOptions,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<PyFormatOptions> for UvFormatCommand {
|
||||||
|
fn from(options: PyFormatOptions) -> Self {
|
||||||
|
Self { options }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UvFormatCommand {
|
||||||
|
/// Build the command with all necessary arguments
|
||||||
|
fn build_command(
|
||||||
|
&self,
|
||||||
|
path: &Path,
|
||||||
|
range_with_index: Option<(TextRange, &LineIndex, &str)>,
|
||||||
|
) -> Command {
|
||||||
|
let mut command = Command::new("uv");
|
||||||
|
command.arg("format");
|
||||||
|
command.arg("--");
|
||||||
|
|
||||||
|
let target_version = format!(
|
||||||
|
"py{}{}",
|
||||||
|
self.options.target_version().major,
|
||||||
|
self.options.target_version().minor
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add only the formatting options that the CLI supports
|
||||||
|
command.arg("--target-version");
|
||||||
|
command.arg(&target_version);
|
||||||
|
|
||||||
|
command.arg("--line-length");
|
||||||
|
command.arg(self.options.line_width().to_string());
|
||||||
|
|
||||||
|
if self.options.preview().is_enabled() {
|
||||||
|
command.arg("--preview");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass other formatting options via --config
|
||||||
|
command.arg("--config");
|
||||||
|
command.arg(format!(
|
||||||
|
"format.indent-style = '{}'",
|
||||||
|
self.options.indent_style()
|
||||||
|
));
|
||||||
|
|
||||||
|
command.arg("--config");
|
||||||
|
command.arg(format!("indent-width = {}", self.options.indent_width()));
|
||||||
|
|
||||||
|
command.arg("--config");
|
||||||
|
command.arg(format!(
|
||||||
|
"format.quote-style = '{}'",
|
||||||
|
self.options.quote_style()
|
||||||
|
));
|
||||||
|
|
||||||
|
command.arg("--config");
|
||||||
|
command.arg(format!(
|
||||||
|
"format.line-ending = '{}'",
|
||||||
|
self.options.line_ending().as_setting_str()
|
||||||
|
));
|
||||||
|
|
||||||
|
command.arg("--config");
|
||||||
|
command.arg(format!(
|
||||||
|
"format.skip-magic-trailing-comma = {}",
|
||||||
|
match self.options.magic_trailing_comma() {
|
||||||
|
ruff_python_formatter::MagicTrailingComma::Respect => "false",
|
||||||
|
ruff_python_formatter::MagicTrailingComma::Ignore => "true",
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
if let Some((range, line_index, source)) = range_with_index {
|
||||||
|
// The CLI expects line:column format
|
||||||
|
let start_pos = line_index.line_column(range.start(), source);
|
||||||
|
let end_pos = line_index.line_column(range.end(), source);
|
||||||
|
let range_str = format!(
|
||||||
|
"{}:{}-{}:{}",
|
||||||
|
start_pos.line.get(),
|
||||||
|
start_pos.column.get(),
|
||||||
|
end_pos.line.get(),
|
||||||
|
end_pos.column.get()
|
||||||
|
);
|
||||||
|
command.arg("--range");
|
||||||
|
command.arg(&range_str);
|
||||||
|
}
|
||||||
|
|
||||||
|
command.arg("--stdin-filename");
|
||||||
|
command.arg(path.to_string_lossy().as_ref());
|
||||||
|
|
||||||
|
command.stdin(Stdio::piped());
|
||||||
|
command.stdout(Stdio::piped());
|
||||||
|
command.stderr(Stdio::piped());
|
||||||
|
|
||||||
|
command
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Execute the format command on the given source.
|
||||||
|
pub(crate) fn format(
|
||||||
|
&self,
|
||||||
|
source: &str,
|
||||||
|
path: &Path,
|
||||||
|
range_with_index: Option<(TextRange, &LineIndex)>,
|
||||||
|
) -> crate::Result<Option<String>> {
|
||||||
|
let mut command =
|
||||||
|
self.build_command(path, range_with_index.map(|(r, idx)| (r, idx, source)));
|
||||||
|
let mut child = match command.spawn() {
|
||||||
|
Ok(child) => child,
|
||||||
|
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
|
||||||
|
anyhow::bail!("uv was not found; is it installed and on the PATH?")
|
||||||
|
}
|
||||||
|
Err(err) => return Err(err).context("Failed to spawn uv"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut stdin = child
|
||||||
|
.stdin
|
||||||
|
.take()
|
||||||
|
.context("Failed to get stdin from format subprocess")?;
|
||||||
|
stdin
|
||||||
|
.write_all(source.as_bytes())
|
||||||
|
.context("Failed to write to stdin")?;
|
||||||
|
drop(stdin);
|
||||||
|
|
||||||
|
let result = child
|
||||||
|
.wait_with_output()
|
||||||
|
.context("Failed to get output from format subprocess")?;
|
||||||
|
|
||||||
|
if !result.status.success() {
|
||||||
|
let stderr = String::from_utf8_lossy(&result.stderr);
|
||||||
|
// We don't propagate format errors due to invalid syntax
|
||||||
|
if stderr.contains("Failed to parse") {
|
||||||
|
tracing::warn!("Unable to format document: {}", stderr);
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
// Special-case for when `uv format` is not available
|
||||||
|
if stderr.contains("unrecognized subcommand 'format'") {
|
||||||
|
anyhow::bail!(
|
||||||
|
"The installed version of uv does not support `uv format`; upgrade to a newer version"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
anyhow::bail!("Failed to format document: {}", stderr);
|
||||||
|
}
|
||||||
|
|
||||||
|
let formatted = String::from_utf8(result.stdout)
|
||||||
|
.context("Failed to parse stdout from format subprocess as utf-8")?;
|
||||||
|
|
||||||
|
if formatted == source {
|
||||||
|
Ok(None)
|
||||||
|
} else {
|
||||||
|
Ok(Some(formatted))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Format the entire document.
|
||||||
|
pub(crate) fn format_document(
|
||||||
|
&self,
|
||||||
|
source: &str,
|
||||||
|
path: &Path,
|
||||||
|
) -> crate::Result<Option<String>> {
|
||||||
|
self.format(source, path, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Format a specific range.
|
||||||
|
pub(crate) fn format_range(
|
||||||
|
&self,
|
||||||
|
source: &str,
|
||||||
|
range: TextRange,
|
||||||
|
path: &Path,
|
||||||
|
line_index: &LineIndex,
|
||||||
|
) -> crate::Result<Option<String>> {
|
||||||
|
self.format(source, path, Some((range, line_index)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
@ -74,7 +332,7 @@ mod tests {
|
||||||
use ruff_workspace::FormatterSettings;
|
use ruff_workspace::FormatterSettings;
|
||||||
|
|
||||||
use crate::TextDocument;
|
use crate::TextDocument;
|
||||||
use crate::format::{format, format_range};
|
use crate::format::{FormatBackend, format, format_range};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn format_per_file_version() {
|
fn format_per_file_version() {
|
||||||
|
@ -98,6 +356,7 @@ with open("a_really_long_foo") as foo, open("a_really_long_bar") as bar, open("a
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
Path::new("test.py"),
|
Path::new("test.py"),
|
||||||
|
FormatBackend::Internal,
|
||||||
)
|
)
|
||||||
.expect("Expected no errors when formatting")
|
.expect("Expected no errors when formatting")
|
||||||
.expect("Expected formatting changes");
|
.expect("Expected formatting changes");
|
||||||
|
@ -120,6 +379,7 @@ with open("a_really_long_foo") as foo, open("a_really_long_bar") as bar, open("a
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
Path::new("test.py"),
|
Path::new("test.py"),
|
||||||
|
FormatBackend::Internal,
|
||||||
)
|
)
|
||||||
.expect("Expected no errors when formatting")
|
.expect("Expected no errors when formatting")
|
||||||
.expect("Expected formatting changes");
|
.expect("Expected formatting changes");
|
||||||
|
@ -168,6 +428,7 @@ sys.exit(
|
||||||
},
|
},
|
||||||
range,
|
range,
|
||||||
Path::new("test.py"),
|
Path::new("test.py"),
|
||||||
|
FormatBackend::Internal,
|
||||||
)
|
)
|
||||||
.expect("Expected no errors when formatting")
|
.expect("Expected no errors when formatting")
|
||||||
.expect("Expected formatting changes");
|
.expect("Expected formatting changes");
|
||||||
|
@ -191,6 +452,7 @@ sys.exit(
|
||||||
},
|
},
|
||||||
range,
|
range,
|
||||||
Path::new("test.py"),
|
Path::new("test.py"),
|
||||||
|
FormatBackend::Internal,
|
||||||
)
|
)
|
||||||
.expect("Expected no errors when formatting")
|
.expect("Expected no errors when formatting")
|
||||||
.expect("Expected formatting changes");
|
.expect("Expected formatting changes");
|
||||||
|
@ -204,4 +466,279 @@ sys.exit(
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "test-uv")]
|
||||||
|
mod uv_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_uv_format_document() {
|
||||||
|
let document = TextDocument::new(
|
||||||
|
r#"
|
||||||
|
def hello( x,y ,z ):
|
||||||
|
return x+y +z
|
||||||
|
|
||||||
|
|
||||||
|
def world( ):
|
||||||
|
pass
|
||||||
|
"#
|
||||||
|
.to_string(),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
|
let result = format(
|
||||||
|
&document,
|
||||||
|
PySourceType::Python,
|
||||||
|
&FormatterSettings::default(),
|
||||||
|
Path::new("test.py"),
|
||||||
|
FormatBackend::Uv,
|
||||||
|
)
|
||||||
|
.expect("Expected no errors when formatting with uv")
|
||||||
|
.expect("Expected formatting changes");
|
||||||
|
|
||||||
|
// uv should format this to a consistent style
|
||||||
|
assert_snapshot!(result, @r#"
|
||||||
|
def hello(x, y, z):
|
||||||
|
return x + y + z
|
||||||
|
|
||||||
|
|
||||||
|
def world():
|
||||||
|
pass
|
||||||
|
"#);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_uv_format_range() -> anyhow::Result<()> {
|
||||||
|
let document = TextDocument::new(
|
||||||
|
r#"
|
||||||
|
def messy_function( a, b,c ):
|
||||||
|
return a+b+c
|
||||||
|
|
||||||
|
def another_function(x,y,z):
|
||||||
|
result=x+y+z
|
||||||
|
return result
|
||||||
|
"#
|
||||||
|
.to_string(),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Find the range of the second function
|
||||||
|
let start = document.contents().find("def another_function").unwrap();
|
||||||
|
let end = document.contents().find("return result").unwrap() + "return result".len();
|
||||||
|
let range = TextRange::new(TextSize::try_from(start)?, TextSize::try_from(end)?);
|
||||||
|
|
||||||
|
let result = format_range(
|
||||||
|
&document,
|
||||||
|
PySourceType::Python,
|
||||||
|
&FormatterSettings::default(),
|
||||||
|
range,
|
||||||
|
Path::new("test.py"),
|
||||||
|
FormatBackend::Uv,
|
||||||
|
)
|
||||||
|
.expect("Expected no errors when formatting range with uv")
|
||||||
|
.expect("Expected formatting changes");
|
||||||
|
|
||||||
|
assert_snapshot!(result.as_code(), @r#"
|
||||||
|
def messy_function( a, b,c ):
|
||||||
|
return a+b+c
|
||||||
|
|
||||||
|
def another_function(x, y, z):
|
||||||
|
result = x + y + z
|
||||||
|
return result
|
||||||
|
"#);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_uv_format_with_line_length() {
|
||||||
|
use ruff_formatter::LineWidth;
|
||||||
|
|
||||||
|
let document = TextDocument::new(
|
||||||
|
r#"
|
||||||
|
def hello(very_long_parameter_name_1, very_long_parameter_name_2, very_long_parameter_name_3):
|
||||||
|
return very_long_parameter_name_1 + very_long_parameter_name_2 + very_long_parameter_name_3
|
||||||
|
"#
|
||||||
|
.to_string(),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test with shorter line length
|
||||||
|
let formatter_settings = FormatterSettings {
|
||||||
|
line_width: LineWidth::try_from(60).unwrap(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = format(
|
||||||
|
&document,
|
||||||
|
PySourceType::Python,
|
||||||
|
&formatter_settings,
|
||||||
|
Path::new("test.py"),
|
||||||
|
FormatBackend::Uv,
|
||||||
|
)
|
||||||
|
.expect("Expected no errors when formatting with uv")
|
||||||
|
.expect("Expected formatting changes");
|
||||||
|
|
||||||
|
// With line length 60, the function should be wrapped
|
||||||
|
assert_snapshot!(result, @r#"
|
||||||
|
def hello(
|
||||||
|
very_long_parameter_name_1,
|
||||||
|
very_long_parameter_name_2,
|
||||||
|
very_long_parameter_name_3,
|
||||||
|
):
|
||||||
|
return (
|
||||||
|
very_long_parameter_name_1
|
||||||
|
+ very_long_parameter_name_2
|
||||||
|
+ very_long_parameter_name_3
|
||||||
|
)
|
||||||
|
"#);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_uv_format_with_indent_style() {
|
||||||
|
use ruff_formatter::IndentStyle;
|
||||||
|
|
||||||
|
let document = TextDocument::new(
|
||||||
|
r#"
|
||||||
|
def hello():
|
||||||
|
if True:
|
||||||
|
print("Hello")
|
||||||
|
if False:
|
||||||
|
print("World")
|
||||||
|
"#
|
||||||
|
.to_string(),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test with tabs instead of spaces
|
||||||
|
let formatter_settings = FormatterSettings {
|
||||||
|
indent_style: IndentStyle::Tab,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = format(
|
||||||
|
&document,
|
||||||
|
PySourceType::Python,
|
||||||
|
&formatter_settings,
|
||||||
|
Path::new("test.py"),
|
||||||
|
FormatBackend::Uv,
|
||||||
|
)
|
||||||
|
.expect("Expected no errors when formatting with uv")
|
||||||
|
.expect("Expected formatting changes");
|
||||||
|
|
||||||
|
// Should have formatting changes (spaces to tabs)
|
||||||
|
assert_snapshot!(result, @r#"
|
||||||
|
def hello():
|
||||||
|
if True:
|
||||||
|
print("Hello")
|
||||||
|
if False:
|
||||||
|
print("World")
|
||||||
|
"#);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_uv_format_syntax_error() {
|
||||||
|
let document = TextDocument::new(
|
||||||
|
r#"
|
||||||
|
def broken(:
|
||||||
|
pass
|
||||||
|
"#
|
||||||
|
.to_string(),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
|
// uv should return None for syntax errors (as indicated by the TODO comment)
|
||||||
|
let result = format(
|
||||||
|
&document,
|
||||||
|
PySourceType::Python,
|
||||||
|
&FormatterSettings::default(),
|
||||||
|
Path::new("test.py"),
|
||||||
|
FormatBackend::Uv,
|
||||||
|
)
|
||||||
|
.expect("Expected no errors from format function");
|
||||||
|
|
||||||
|
// Should return None since the syntax is invalid
|
||||||
|
assert_eq!(result, None, "Expected None for syntax error");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_uv_format_with_quote_style() {
|
||||||
|
use ruff_python_formatter::QuoteStyle;
|
||||||
|
|
||||||
|
let document = TextDocument::new(
|
||||||
|
r#"
|
||||||
|
x = "hello"
|
||||||
|
y = 'world'
|
||||||
|
z = '''multi
|
||||||
|
line'''
|
||||||
|
"#
|
||||||
|
.to_string(),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test with single quotes
|
||||||
|
let formatter_settings = FormatterSettings {
|
||||||
|
quote_style: QuoteStyle::Single,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = format(
|
||||||
|
&document,
|
||||||
|
PySourceType::Python,
|
||||||
|
&formatter_settings,
|
||||||
|
Path::new("test.py"),
|
||||||
|
FormatBackend::Uv,
|
||||||
|
)
|
||||||
|
.expect("Expected no errors when formatting with uv")
|
||||||
|
.expect("Expected formatting changes");
|
||||||
|
|
||||||
|
assert_snapshot!(result, @r#"
|
||||||
|
x = 'hello'
|
||||||
|
y = 'world'
|
||||||
|
z = """multi
|
||||||
|
line"""
|
||||||
|
"#);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_uv_format_with_magic_trailing_comma() {
|
||||||
|
use ruff_python_formatter::MagicTrailingComma;
|
||||||
|
|
||||||
|
let document = TextDocument::new(
|
||||||
|
r#"
|
||||||
|
foo = [
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
3,
|
||||||
|
]
|
||||||
|
|
||||||
|
bar = [1, 2, 3,]
|
||||||
|
"#
|
||||||
|
.to_string(),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test with ignore magic trailing comma
|
||||||
|
let formatter_settings = FormatterSettings {
|
||||||
|
magic_trailing_comma: MagicTrailingComma::Ignore,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = format(
|
||||||
|
&document,
|
||||||
|
PySourceType::Python,
|
||||||
|
&formatter_settings,
|
||||||
|
Path::new("test.py"),
|
||||||
|
FormatBackend::Uv,
|
||||||
|
)
|
||||||
|
.expect("Expected no errors when formatting with uv")
|
||||||
|
.expect("Expected formatting changes");
|
||||||
|
|
||||||
|
assert_snapshot!(result, @r#"
|
||||||
|
foo = [1, 2, 3]
|
||||||
|
|
||||||
|
bar = [1, 2, 3]
|
||||||
|
"#);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,10 @@ impl super::BackgroundDocumentRequestHandler for Format {
|
||||||
pub(super) fn format_full_document(snapshot: &DocumentSnapshot) -> Result<Fixes> {
|
pub(super) fn format_full_document(snapshot: &DocumentSnapshot) -> Result<Fixes> {
|
||||||
let mut fixes = Fixes::default();
|
let mut fixes = Fixes::default();
|
||||||
let query = snapshot.query();
|
let query = snapshot.query();
|
||||||
|
let backend = snapshot
|
||||||
|
.client_settings()
|
||||||
|
.editor_settings()
|
||||||
|
.format_backend();
|
||||||
|
|
||||||
match snapshot.query() {
|
match snapshot.query() {
|
||||||
DocumentQuery::Notebook { notebook, .. } => {
|
DocumentQuery::Notebook { notebook, .. } => {
|
||||||
|
@ -41,7 +45,7 @@ pub(super) fn format_full_document(snapshot: &DocumentSnapshot) -> Result<Fixes>
|
||||||
.map(|url| (url.clone(), notebook.cell_document_by_uri(url).unwrap()))
|
.map(|url| (url.clone(), notebook.cell_document_by_uri(url).unwrap()))
|
||||||
{
|
{
|
||||||
if let Some(changes) =
|
if let Some(changes) =
|
||||||
format_text_document(text_document, query, snapshot.encoding(), true)?
|
format_text_document(text_document, query, snapshot.encoding(), true, backend)?
|
||||||
{
|
{
|
||||||
fixes.insert(url, changes);
|
fixes.insert(url, changes);
|
||||||
}
|
}
|
||||||
|
@ -49,7 +53,7 @@ pub(super) fn format_full_document(snapshot: &DocumentSnapshot) -> Result<Fixes>
|
||||||
}
|
}
|
||||||
DocumentQuery::Text { document, .. } => {
|
DocumentQuery::Text { document, .. } => {
|
||||||
if let Some(changes) =
|
if let Some(changes) =
|
||||||
format_text_document(document, query, snapshot.encoding(), false)?
|
format_text_document(document, query, snapshot.encoding(), false, backend)?
|
||||||
{
|
{
|
||||||
fixes.insert(snapshot.query().make_key().into_url(), changes);
|
fixes.insert(snapshot.query().make_key().into_url(), changes);
|
||||||
}
|
}
|
||||||
|
@ -68,11 +72,16 @@ pub(super) fn format_document(snapshot: &DocumentSnapshot) -> Result<super::Form
|
||||||
.context("Failed to get text document for the format request")
|
.context("Failed to get text document for the format request")
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let query = snapshot.query();
|
let query = snapshot.query();
|
||||||
|
let backend = snapshot
|
||||||
|
.client_settings()
|
||||||
|
.editor_settings()
|
||||||
|
.format_backend();
|
||||||
format_text_document(
|
format_text_document(
|
||||||
text_document,
|
text_document,
|
||||||
query,
|
query,
|
||||||
snapshot.encoding(),
|
snapshot.encoding(),
|
||||||
query.as_notebook().is_some(),
|
query.as_notebook().is_some(),
|
||||||
|
backend,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,6 +90,7 @@ fn format_text_document(
|
||||||
query: &DocumentQuery,
|
query: &DocumentQuery,
|
||||||
encoding: PositionEncoding,
|
encoding: PositionEncoding,
|
||||||
is_notebook: bool,
|
is_notebook: bool,
|
||||||
|
backend: crate::format::FormatBackend,
|
||||||
) -> Result<super::FormatResponse> {
|
) -> Result<super::FormatResponse> {
|
||||||
let settings = query.settings();
|
let settings = query.settings();
|
||||||
let file_path = query.virtual_file_path();
|
let file_path = query.virtual_file_path();
|
||||||
|
@ -101,6 +111,7 @@ fn format_text_document(
|
||||||
query.source_type(),
|
query.source_type(),
|
||||||
&settings.formatter,
|
&settings.formatter,
|
||||||
&file_path,
|
&file_path,
|
||||||
|
backend,
|
||||||
)
|
)
|
||||||
.with_failure_code(lsp_server::ErrorCode::InternalError)?;
|
.with_failure_code(lsp_server::ErrorCode::InternalError)?;
|
||||||
let Some(mut formatted) = formatted else {
|
let Some(mut formatted) = formatted else {
|
||||||
|
|
|
@ -36,7 +36,11 @@ fn format_document_range(
|
||||||
.context("Failed to get text document for the format range request")
|
.context("Failed to get text document for the format range request")
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let query = snapshot.query();
|
let query = snapshot.query();
|
||||||
format_text_document_range(text_document, range, query, snapshot.encoding())
|
let backend = snapshot
|
||||||
|
.client_settings()
|
||||||
|
.editor_settings()
|
||||||
|
.format_backend();
|
||||||
|
format_text_document_range(text_document, range, query, snapshot.encoding(), backend)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Formats the specified [`Range`] in the [`TextDocument`].
|
/// Formats the specified [`Range`] in the [`TextDocument`].
|
||||||
|
@ -45,6 +49,7 @@ fn format_text_document_range(
|
||||||
range: Range,
|
range: Range,
|
||||||
query: &DocumentQuery,
|
query: &DocumentQuery,
|
||||||
encoding: PositionEncoding,
|
encoding: PositionEncoding,
|
||||||
|
backend: crate::format::FormatBackend,
|
||||||
) -> Result<super::FormatResponse> {
|
) -> Result<super::FormatResponse> {
|
||||||
let settings = query.settings();
|
let settings = query.settings();
|
||||||
let file_path = query.virtual_file_path();
|
let file_path = query.virtual_file_path();
|
||||||
|
@ -68,6 +73,7 @@ fn format_text_document_range(
|
||||||
&settings.formatter,
|
&settings.formatter,
|
||||||
range,
|
range,
|
||||||
&file_path,
|
&file_path,
|
||||||
|
backend,
|
||||||
)
|
)
|
||||||
.with_failure_code(lsp_server::ErrorCode::InternalError)?;
|
.with_failure_code(lsp_server::ErrorCode::InternalError)?;
|
||||||
|
|
||||||
|
|
|
@ -401,6 +401,7 @@ impl ConfigurationTransformer for EditorConfigurationTransformer<'_> {
|
||||||
configuration,
|
configuration,
|
||||||
format_preview,
|
format_preview,
|
||||||
lint_preview,
|
lint_preview,
|
||||||
|
format_backend: _,
|
||||||
select,
|
select,
|
||||||
extend_select,
|
extend_select,
|
||||||
ignore,
|
ignore,
|
||||||
|
|
|
@ -7,9 +7,12 @@ use serde_json::{Map, Value};
|
||||||
|
|
||||||
use ruff_linter::{RuleSelector, line_width::LineLength, rule_selector::ParseError};
|
use ruff_linter::{RuleSelector, line_width::LineLength, rule_selector::ParseError};
|
||||||
|
|
||||||
use crate::session::{
|
use crate::{
|
||||||
|
format::FormatBackend,
|
||||||
|
session::{
|
||||||
Client,
|
Client,
|
||||||
settings::{ClientSettings, EditorSettings, GlobalClientSettings, ResolvedConfiguration},
|
settings::{ClientSettings, EditorSettings, GlobalClientSettings, ResolvedConfiguration},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(crate) type WorkspaceOptionsMap = FxHashMap<Url, ClientOptions>;
|
pub(crate) type WorkspaceOptionsMap = FxHashMap<Url, ClientOptions>;
|
||||||
|
@ -124,6 +127,7 @@ impl ClientOptions {
|
||||||
configuration,
|
configuration,
|
||||||
lint_preview: lint.preview,
|
lint_preview: lint.preview,
|
||||||
format_preview: format.preview,
|
format_preview: format.preview,
|
||||||
|
format_backend: format.backend,
|
||||||
select: lint.select.and_then(|select| {
|
select: lint.select.and_then(|select| {
|
||||||
Self::resolve_rules(
|
Self::resolve_rules(
|
||||||
&select,
|
&select,
|
||||||
|
@ -283,11 +287,13 @@ impl Combine for LintOptions {
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct FormatOptions {
|
struct FormatOptions {
|
||||||
preview: Option<bool>,
|
preview: Option<bool>,
|
||||||
|
backend: Option<FormatBackend>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Combine for FormatOptions {
|
impl Combine for FormatOptions {
|
||||||
fn combine_with(&mut self, other: Self) {
|
fn combine_with(&mut self, other: Self) {
|
||||||
self.preview.combine_with(other.preview);
|
self.preview.combine_with(other.preview);
|
||||||
|
self.backend.combine_with(other.backend);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -443,6 +449,12 @@ pub(crate) trait Combine {
|
||||||
fn combine_with(&mut self, other: Self);
|
fn combine_with(&mut self, other: Self);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Combine for FormatBackend {
|
||||||
|
fn combine_with(&mut self, other: Self) {
|
||||||
|
*self = other;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<T> Combine for Option<T>
|
impl<T> Combine for Option<T>
|
||||||
where
|
where
|
||||||
T: Combine,
|
T: Combine,
|
||||||
|
@ -584,6 +596,7 @@ mod tests {
|
||||||
format: Some(
|
format: Some(
|
||||||
FormatOptions {
|
FormatOptions {
|
||||||
preview: None,
|
preview: None,
|
||||||
|
backend: None,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
code_action: Some(
|
code_action: Some(
|
||||||
|
@ -640,6 +653,7 @@ mod tests {
|
||||||
format: Some(
|
format: Some(
|
||||||
FormatOptions {
|
FormatOptions {
|
||||||
preview: None,
|
preview: None,
|
||||||
|
backend: None,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
code_action: Some(
|
code_action: Some(
|
||||||
|
@ -704,6 +718,7 @@ mod tests {
|
||||||
format: Some(
|
format: Some(
|
||||||
FormatOptions {
|
FormatOptions {
|
||||||
preview: None,
|
preview: None,
|
||||||
|
backend: None,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
code_action: Some(
|
code_action: Some(
|
||||||
|
@ -782,6 +797,7 @@ mod tests {
|
||||||
configuration: None,
|
configuration: None,
|
||||||
lint_preview: Some(true),
|
lint_preview: Some(true),
|
||||||
format_preview: None,
|
format_preview: None,
|
||||||
|
format_backend: None,
|
||||||
select: Some(vec![
|
select: Some(vec![
|
||||||
RuleSelector::Linter(Linter::Pyflakes),
|
RuleSelector::Linter(Linter::Pyflakes),
|
||||||
RuleSelector::Linter(Linter::Isort)
|
RuleSelector::Linter(Linter::Isort)
|
||||||
|
@ -819,6 +835,7 @@ mod tests {
|
||||||
configuration: None,
|
configuration: None,
|
||||||
lint_preview: Some(false),
|
lint_preview: Some(false),
|
||||||
format_preview: None,
|
format_preview: None,
|
||||||
|
format_backend: None,
|
||||||
select: Some(vec![
|
select: Some(vec![
|
||||||
RuleSelector::Linter(Linter::Pyflakes),
|
RuleSelector::Linter(Linter::Pyflakes),
|
||||||
RuleSelector::Linter(Linter::Isort)
|
RuleSelector::Linter(Linter::Isort)
|
||||||
|
@ -919,6 +936,7 @@ mod tests {
|
||||||
configuration: None,
|
configuration: None,
|
||||||
lint_preview: None,
|
lint_preview: None,
|
||||||
format_preview: None,
|
format_preview: None,
|
||||||
|
format_backend: None,
|
||||||
select: None,
|
select: None,
|
||||||
extend_select: None,
|
extend_select: None,
|
||||||
ignore: Some(vec![RuleSelector::from_str("RUF001").unwrap()]),
|
ignore: Some(vec![RuleSelector::from_str("RUF001").unwrap()]),
|
||||||
|
|
|
@ -8,6 +8,7 @@ use ruff_workspace::options::Options;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
ClientOptions,
|
ClientOptions,
|
||||||
|
format::FormatBackend,
|
||||||
session::{
|
session::{
|
||||||
Client,
|
Client,
|
||||||
options::{ClientConfiguration, ConfigurationPreference},
|
options::{ClientConfiguration, ConfigurationPreference},
|
||||||
|
@ -84,6 +85,7 @@ pub(crate) struct EditorSettings {
|
||||||
pub(super) configuration: Option<ResolvedConfiguration>,
|
pub(super) configuration: Option<ResolvedConfiguration>,
|
||||||
pub(super) lint_preview: Option<bool>,
|
pub(super) lint_preview: Option<bool>,
|
||||||
pub(super) format_preview: Option<bool>,
|
pub(super) format_preview: Option<bool>,
|
||||||
|
pub(super) format_backend: Option<FormatBackend>,
|
||||||
pub(super) select: Option<Vec<RuleSelector>>,
|
pub(super) select: Option<Vec<RuleSelector>>,
|
||||||
pub(super) extend_select: Option<Vec<RuleSelector>>,
|
pub(super) extend_select: Option<Vec<RuleSelector>>,
|
||||||
pub(super) ignore: Option<Vec<RuleSelector>>,
|
pub(super) ignore: Option<Vec<RuleSelector>>,
|
||||||
|
@ -163,3 +165,9 @@ impl ClientSettings {
|
||||||
&self.editor_settings
|
&self.editor_settings
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl EditorSettings {
|
||||||
|
pub(crate) fn format_backend(&self) -> FormatBackend {
|
||||||
|
self.format_backend.unwrap_or_default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -971,6 +971,62 @@ Whether to enable Ruff's preview mode when formatting.
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### `backend` {: #format_backend }
|
||||||
|
|
||||||
|
The backend to use for formatting files. Following options are available:
|
||||||
|
|
||||||
|
- `"internal"`: Use the built-in Ruff formatter
|
||||||
|
- `"uv"`: Use uv for formatting (requires uv >= 0.8.13)
|
||||||
|
|
||||||
|
For `internal`, the formatter version will match the selected Ruff version while for `uv`, the
|
||||||
|
formatter version may differ.
|
||||||
|
|
||||||
|
**Default value**: `"internal"`
|
||||||
|
|
||||||
|
**Type**: `"internal" | "uv"`
|
||||||
|
|
||||||
|
**Example usage**:
|
||||||
|
|
||||||
|
=== "VS Code"
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ruff.format.backend": "uv"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "Neovim"
|
||||||
|
|
||||||
|
```lua
|
||||||
|
require('lspconfig').ruff.setup {
|
||||||
|
init_options = {
|
||||||
|
settings = {
|
||||||
|
format = {
|
||||||
|
backend = "uv"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
=== "Zed"
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"lsp": {
|
||||||
|
"ruff": {
|
||||||
|
"initialization_options": {
|
||||||
|
"settings": {
|
||||||
|
"format": {
|
||||||
|
"backend": "uv"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## VS Code specific
|
## VS Code specific
|
||||||
|
|
||||||
Additionally, the Ruff extension provides the following settings specific to VS Code. These settings
|
Additionally, the Ruff extension provides the following settings specific to VS Code. These settings
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue