ruff server: Resolve configuration for each document individually (#10950)

## Summary

Configuration is no longer the property of a workspace but rather of
individual documents. Just like the Ruff CLI, each document is
configured based on the 'nearest' project configuration. See [the Ruff
documentation](https://docs.astral.sh/ruff/configuration/#config-file-discovery)
for more details.

To reduce the amount of times we resolve configuration for a file, we
have an index for each workspace that stores a reference-counted pointer
to a configuration for a given folder. If another file in the same
folder is opened, the configuration is simply re-used rather than us
re-resolving it.

## Guide for reviewing

The first commit is just the restructuring work, which adds some noise
to the diff. If you want to quickly understand what's actually changed,
I recommend looking at the two commits that come after it.
f7c073d441 makes configuration a property
of `DocumentController`/`DocumentRef`, moving it out of `Workspace`, and
it also sets up the `ConfigurationIndex`, though it doesn't implement
its key function, `get_or_insert`. In the commit after it,
fc35618f17, we implement `get_or_insert`.

## Test Plan

The best way to test this would be to ensure that the behavior matches
the Ruff CLI. Open a project with multiple configuration files (or add
them yourself), and then introduce problems in certain files that won't
show due to their configuration. Add those same problems to a section of
the project where those rules are run. Confirm that the lint rules are
run as expected with `ruff check`. Then, open your editor and confirm
that the diagnostics shown match the CLI output.

As an example - I have a workspace with two separate folders, `pandas`
and `scipy`. I created a `pyproject.toml` file in `pandas/pandas/io` and
a `ruff.toml` file in `pandas/pandas/api`. I changed the `select` and
`preview` settings in the sub-folder configuration files and confirmed
that these were reflected in the diagnostics. I also confirmed that this
did not change the diagnostics for the `scipy` folder whatsoever.
This commit is contained in:
Jane Lewis 2024-04-16 11:15:02 -07:00 committed by GitHub
parent 4284e079b5
commit cffc55576f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 377 additions and 288 deletions

View file

@ -2,27 +2,23 @@
mod capabilities;
mod settings;
mod workspace;
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::{ops::Deref, sync::Arc};
use std::sync::Arc;
use anyhow::anyhow;
use lsp_types::{ClientCapabilities, Url};
use ruff_workspace::resolver::{ConfigurationTransformer, Relativity};
use rustc_hash::FxHashMap;
use crate::edit::{Document, DocumentVersion};
use crate::edit::DocumentVersion;
use crate::PositionEncoding;
pub(crate) use self::capabilities::ResolvedClientCapabilities;
use self::settings::ResolvedClientSettings;
pub(crate) use self::settings::{AllSettings, ClientSettings};
/// The global state for the LSP
pub(crate) struct Session {
/// Workspace folders in the current session, which contain the state of all open files.
workspaces: Workspaces,
workspaces: workspace::Workspaces,
/// The global position encoding, negotiated during LSP initialization.
position_encoding: PositionEncoding,
/// Global settings provided by the client.
@ -34,49 +30,13 @@ pub(crate) struct Session {
/// An immutable snapshot of `Session` that references
/// a specific document.
pub(crate) struct DocumentSnapshot {
configuration: Arc<RuffConfiguration>,
resolved_client_capabilities: Arc<ResolvedClientCapabilities>,
client_settings: settings::ResolvedClientSettings,
document_ref: DocumentRef,
document_ref: workspace::DocumentRef,
position_encoding: PositionEncoding,
url: Url,
}
#[derive(Default)]
pub(crate) struct RuffConfiguration {
// settings to pass into the ruff linter
pub(crate) linter: ruff_linter::settings::LinterSettings,
// settings to pass into the ruff formatter
pub(crate) formatter: ruff_workspace::FormatterSettings,
}
#[derive(Default)]
pub(crate) struct Workspaces(BTreeMap<PathBuf, Workspace>);
pub(crate) struct Workspace {
open_documents: OpenDocuments,
configuration: Arc<RuffConfiguration>,
settings: ClientSettings,
}
#[derive(Default)]
pub(crate) struct OpenDocuments {
documents: FxHashMap<Url, DocumentController>,
}
/// A mutable handler to an underlying document.
/// Handles copy-on-write mutation automatically when
/// calling `deref_mut`.
pub(crate) struct DocumentController {
document: Arc<Document>,
}
/// A read-only reference to a document.
#[derive(Clone)]
pub(crate) struct DocumentRef {
document: Arc<Document>,
}
impl Session {
pub(crate) fn new(
client_capabilities: &ClientCapabilities,
@ -90,13 +50,12 @@ impl Session {
resolved_client_capabilities: Arc::new(ResolvedClientCapabilities::new(
client_capabilities,
)),
workspaces: Workspaces::new(workspaces)?,
workspaces: workspace::Workspaces::new(workspaces)?,
})
}
pub(crate) fn take_snapshot(&self, url: &Url) -> Option<DocumentSnapshot> {
Some(DocumentSnapshot {
configuration: self.workspaces.configuration(url)?.clone(),
resolved_client_capabilities: self.resolved_client_capabilities.clone(),
client_settings: self.workspaces.client_settings(url, &self.global_settings),
document_ref: self.workspaces.snapshot(url)?,
@ -117,14 +76,14 @@ impl Session {
pub(crate) fn document_controller(
&mut self,
url: &Url,
) -> crate::Result<&mut DocumentController> {
) -> crate::Result<&mut workspace::DocumentController> {
self.workspaces
.controller(url)
.ok_or_else(|| anyhow!("Tried to open unavailable document `{url}`"))
}
pub(crate) fn reload_configuration(&mut self, url: &Url) -> crate::Result<()> {
self.workspaces.reload_configuration(url)
pub(crate) fn reload_settings(&mut self, url: &Url) -> crate::Result<()> {
self.workspaces.reload_settings(url)
}
pub(crate) fn open_workspace_folder(&mut self, url: &Url) -> crate::Result<()> {
@ -146,81 +105,20 @@ impl Session {
}
}
impl OpenDocuments {
fn snapshot(&self, url: &Url) -> Option<DocumentRef> {
Some(self.documents.get(url)?.make_ref())
}
fn controller(&mut self, url: &Url) -> Option<&mut DocumentController> {
self.documents.get_mut(url)
}
fn open(&mut self, url: &Url, contents: String, version: DocumentVersion) {
if self
.documents
.insert(url.clone(), DocumentController::new(contents, version))
.is_some()
{
tracing::warn!("Opening document `{url}` that is already open!");
}
}
fn close(&mut self, url: &Url) -> crate::Result<()> {
let Some(_) = self.documents.remove(url) else {
return Err(anyhow!(
"Tried to close document `{url}`, which was not open"
));
};
Ok(())
}
}
impl DocumentController {
fn new(contents: String, version: DocumentVersion) -> Self {
Self {
document: Arc::new(Document::new(contents, version)),
}
}
pub(crate) fn make_ref(&self) -> DocumentRef {
DocumentRef {
document: self.document.clone(),
}
}
pub(crate) fn make_mut(&mut self) -> &mut Document {
Arc::make_mut(&mut self.document)
}
}
impl Deref for DocumentController {
type Target = Document;
fn deref(&self) -> &Self::Target {
&self.document
}
}
impl Deref for DocumentRef {
type Target = Document;
fn deref(&self) -> &Self::Target {
&self.document
}
}
impl DocumentSnapshot {
pub(crate) fn configuration(&self) -> &RuffConfiguration {
&self.configuration
pub(crate) fn settings(&self) -> &workspace::RuffSettings {
self.document().settings()
}
pub(crate) fn resolved_client_capabilities(&self) -> &ResolvedClientCapabilities {
&self.resolved_client_capabilities
}
pub(crate) fn client_settings(&self) -> &ResolvedClientSettings {
pub(crate) fn client_settings(&self) -> &settings::ResolvedClientSettings {
&self.client_settings
}
pub(crate) fn document(&self) -> &DocumentRef {
pub(crate) fn document(&self) -> &workspace::DocumentRef {
&self.document_ref
}
@ -232,166 +130,3 @@ impl DocumentSnapshot {
&self.url
}
}
impl Workspaces {
fn new(workspaces: Vec<(Url, ClientSettings)>) -> crate::Result<Self> {
Ok(Self(
workspaces
.into_iter()
.map(|(url, settings)| Workspace::new(&url, settings))
.collect::<crate::Result<_>>()?,
))
}
fn open_workspace_folder(&mut self, folder_url: &Url) -> crate::Result<()> {
// TODO(jane): find a way to allow for workspace settings to be updated dynamically
let (path, workspace) = Workspace::new(folder_url, ClientSettings::default())?;
self.0.insert(path, workspace);
Ok(())
}
fn close_workspace_folder(&mut self, folder_url: &Url) -> crate::Result<()> {
let path = folder_url
.to_file_path()
.map_err(|()| anyhow!("Folder URI was not a proper file path"))?;
self.0
.remove(&path)
.ok_or_else(|| anyhow!("Tried to remove non-existent folder {}", path.display()))?;
Ok(())
}
fn snapshot(&self, document_url: &Url) -> Option<DocumentRef> {
self.workspace_for_url(document_url)?
.open_documents
.snapshot(document_url)
}
fn controller(&mut self, document_url: &Url) -> Option<&mut DocumentController> {
self.workspace_for_url_mut(document_url)?
.open_documents
.controller(document_url)
}
fn configuration(&self, document_url: &Url) -> Option<&Arc<RuffConfiguration>> {
Some(&self.workspace_for_url(document_url)?.configuration)
}
fn reload_configuration(&mut self, changed_url: &Url) -> crate::Result<()> {
let (path, workspace) = self
.entry_for_url_mut(changed_url)
.ok_or_else(|| anyhow!("Workspace not found for {changed_url}"))?;
workspace.reload_configuration(path);
Ok(())
}
fn open(&mut self, url: &Url, contents: String, version: DocumentVersion) {
if let Some(workspace) = self.workspace_for_url_mut(url) {
workspace.open_documents.open(url, contents, version);
}
}
fn close(&mut self, url: &Url) -> crate::Result<()> {
self.workspace_for_url_mut(url)
.ok_or_else(|| anyhow!("Workspace not found for {url}"))?
.open_documents
.close(url)
}
fn client_settings(
&self,
url: &Url,
global_settings: &ClientSettings,
) -> ResolvedClientSettings {
self.workspace_for_url(url).map_or_else(
|| {
tracing::warn!(
"Workspace not found for {url}. Global settings will be used for this document"
);
ResolvedClientSettings::global(global_settings)
},
|workspace| {
ResolvedClientSettings::with_workspace(&workspace.settings, global_settings)
},
)
}
fn workspace_for_url(&self, url: &Url) -> Option<&Workspace> {
Some(self.entry_for_url(url)?.1)
}
fn workspace_for_url_mut(&mut self, url: &Url) -> Option<&mut Workspace> {
Some(self.entry_for_url_mut(url)?.1)
}
fn entry_for_url(&self, url: &Url) -> Option<(&Path, &Workspace)> {
let path = url.to_file_path().ok()?;
self.0
.range(..path)
.next_back()
.map(|(path, workspace)| (path.as_path(), workspace))
}
fn entry_for_url_mut(&mut self, url: &Url) -> Option<(&Path, &mut Workspace)> {
let path = url.to_file_path().ok()?;
self.0
.range_mut(..path)
.next_back()
.map(|(path, workspace)| (path.as_path(), workspace))
}
}
impl Workspace {
pub(crate) fn new(root: &Url, settings: ClientSettings) -> crate::Result<(PathBuf, Self)> {
let path = root
.to_file_path()
.map_err(|()| anyhow!("workspace URL was not a file path!"))?;
// Fall-back to default configuration
let configuration = Self::find_configuration_or_fallback(&path);
Ok((
path,
Self {
open_documents: OpenDocuments::default(),
configuration: Arc::new(configuration),
settings,
},
))
}
fn reload_configuration(&mut self, path: &Path) {
self.configuration = Arc::new(Self::find_configuration_or_fallback(path));
}
fn find_configuration_or_fallback(root: &Path) -> RuffConfiguration {
find_configuration_from_root(root).unwrap_or_else(|err| {
tracing::error!("The following error occurred when trying to find a configuration file at `{}`:\n{err}", root.display());
tracing::error!("Falling back to default configuration for `{}`", root.display());
RuffConfiguration::default()
})
}
}
pub(crate) fn find_configuration_from_root(root: &Path) -> crate::Result<RuffConfiguration> {
let pyproject = ruff_workspace::pyproject::find_settings_toml(root)?
.ok_or_else(|| anyhow!("No pyproject.toml/ruff.toml/.ruff.toml file was found"))?;
let settings = ruff_workspace::resolver::resolve_root_settings(
&pyproject,
Relativity::Parent,
&LSPConfigTransformer,
)?;
Ok(RuffConfiguration {
linter: settings.linter,
formatter: settings.formatter,
})
}
struct LSPConfigTransformer;
impl ConfigurationTransformer for LSPConfigTransformer {
fn transform(
&self,
config: ruff_workspace::configuration::Configuration,
) -> ruff_workspace::configuration::Configuration {
config
}
}