Skip to content

Conversation

@zanieb
Copy link
Member

@zanieb zanieb commented Jul 31, 2025

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.

@github-actions
Copy link
Contributor

github-actions bot commented Jul 31, 2025

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

✅ ecosystem check detected no linter changes.

Formatter (stable)

✅ ecosystem check detected no format changes.

Formatter (preview)

✅ ecosystem check detected no format changes.

// TODO(zanieb): We should consider returning this case to the caller somehow
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
tracing::warn!("uvx ruff format failed: {}", stderr);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probalby show a message to the user if that happens see Client

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we differentiate between failures due to syntax errors in an easy way?

Will look at the Client for sending the message. Thank you!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See d23af97

My understanding is this will propagate upstream as the format_internal errors do. We could show a message directly to the user with more context, but I think we can defer that for now?

zanieb added a commit to astral-sh/uv that referenced this pull request Aug 21, 2025
As a frontend to Ruff's formatter.

There are some interesting choices here, some of which may just be
temporary:

1. We pin a default version of Ruff, so `uv format` is stable for a
given uv version
2. We install Ruff from GitHub instead of PyPI, which means we don't
need a Python interpreter or environment
3. We do not read the Ruff version from the dependency tree

See astral-sh/ruff#19665 for a prototype of the
LSP integration.
@zanieb
Copy link
Member Author

zanieb commented Aug 22, 2025

@dhruvmanila would you mind helping me ship this? I'm not sure what all needs to happen :)

@zanieb zanieb marked this pull request as ready for review August 22, 2025 19:26
@dhruvmanila
Copy link
Member

would you mind helping me ship this? I'm not sure what all needs to happen :)

Sure! I'll look into this in a bit

@zanieb
Copy link
Member Author

zanieb commented Aug 25, 2025

I'll look into the test failures.

Copy link
Member

@dhruvmanila dhruvmanila left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

I read the corresponding Notion document for this in which you've mentioned:

Micha and I talked quite a bit offline about the IDE experience, and there are a couple possible approaches that we can take to alleviate those problems.

Is there any mention of other approaches apart from the one that's implemented in this PR? Nevermind, I see it in the PR description.

There are couple of follow-ups that would need to be done:

  1. Updating the documentation to include the new option, this needs to be done manually for editor settings
  2. Updating the VS Code extension to expose this option in VS Code settings and pass it to the server

Can you confirm whether this was tested in the editor? If so, which editor did you test this with, I can try it out in some other editor.

Comment on lines 181 to 182
let line_width = FormatOptions::line_width(&self.options).value().to_string();
let indent_width = FormatOptions::indent_width(&self.options)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this use self.options.line_width() and self.options.indent_width() instead?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, thanks!

Comment on lines 207 to 212
command.arg("--config");
command.arg(format!(
"format.quote-style = '{}'",
self.options.quote_style().as_str()
));

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a bit surprised that this works, is this equivalent to --config format.quote-style = 'single' (notice there are no quotes to the --config value):

$ ruff format --config format.quote-style = 'single' --diff isolated.py               
error: invalid value 'format.quote-style' for '--config <CONFIG_OPTION>'

  tip: A `--config` flag must either be a path to a `.toml` configuration file
       or a TOML `<KEY> = <VALUE>` pair overriding a specific configuration
       option

For more information, try '--help'.

I think the entire value would need to be quoted, so the following should work:

$ ruff format --config "format.quote-style = 'single'" --diff isolated.py

Same goes for other --config flags.

Copy link
Member Author

@zanieb zanieb Aug 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When using Command::arg the outer quotes (") are implicit. Those outer quotes are being parsed by your shell into a single argument. I think providing them here would actually break things, as they'd be included in the value.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I see, thanks for clarifying

) -> Command {
// TODO(zanieb): We should probably check the uv version to make sure it supports
// `uv format` or add a special error message when the subcommand doesn't exist
let mut command = Command::new("uv");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will always use the uv command that comes the earliest on PATH as seen from within the editor. Do you see any issues that could occur when doing so?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. It could be the wrong version.

I don't think I expect multiple uv versions to be available on people's machines — and when it happens it's usually an accident. We can provide a way to set the uv path in the future though. We'll want it for other things.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that makes sense

Comment on lines 170 to 171
// TODO(zanieb): We should probably check the uv version to make sure it supports
// `uv format` or add a special error message when the subcommand doesn't exist
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, we'd also need to provide a better error message when the uv command itself isn't available.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What were you thinking for that case?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like, what the error message would be?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, but I just made a small improvement here for now.

Comment on lines +133 to +139
fn format_range_external(
document: &TextDocument,
source_type: PySourceType,
formatter_settings: &FormatterSettings,
range: TextRange,
path: &Path,
) -> crate::Result<Option<PrintedRange>> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returning Result type here would mean that any errors resulted would be counted as internal server error and will be shown to the user. I think this is fine for now, but we might want to categorize a subset of them with better error message for example the TODO comment to check whether the subcommand is available or not.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense, thanks for clarifying!

Comment on lines 288 to 291
struct FormatOptions {
preview: Option<bool>,
backend: Option<FormatBackend>,
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Including it directly in the format table might indicate that this is a stable option, I think there are two ways to communicate the experimental nature of this feature:

  1. Make it explicit in the documentation
  2. Include it under experimental.format.backend similar to ty's experimental namespace (https://docs.astral.sh/ty/reference/editor-settings/#experimental)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have strong feelings. (2) seems nice but is weird to transition off of?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking about this more, I think the current structure is fine (including it in the existing format namespace).

@dhruvmanila
Copy link
Member

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.

Does this mean something like uv server ?

Yeah, I think I'd be a bit hesitant to make the change as described here mainly because we only control the client side implementation for VS Code while for other editors we'd need to explicitly specify on how to start the server and what does it mean to start it from uv vs ruff.

@zanieb
Copy link
Member Author

zanieb commented Aug 26, 2025

Does this mean something like uv server ?

Yeah, basically.

... while for other editors we'd need to explicitly specify on how to start the server and what does it mean to start it from uv vs ruff.

Yeah, this was the same concern Micha and I had.

@zanieb
Copy link
Member Author

zanieb commented Aug 26, 2025

The test failures on Windows are due to concurrent-installs being unsafe in uv. I'll fix that upstream.

@dhruvmanila dhruvmanila added configuration Related to settings and configuration server Related to the LSP server labels Sep 8, 2025
@dhruvmanila dhruvmanila merged commit 9cdac2d into main Sep 9, 2025
37 checks passed
@dhruvmanila dhruvmanila deleted the zb/format-uv branch September 9, 2025 15:09
dhruvmanila added a commit to astral-sh/ruff-vscode that referenced this pull request Sep 9, 2025
This PR adds the `ruff.format.backend` setting corresponding to
astral-sh/ruff#19665 in Ruff.
second-ed pushed a commit to second-ed/ruff that referenced this pull request Sep 9, 2025
…sh#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 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

configuration Related to settings and configuration server Related to the LSP server

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants