mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-30 13:51:37 +00:00
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:
parent
4284e079b5
commit
cffc55576f
12 changed files with 377 additions and 288 deletions
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue