mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-03 07:04:53 +00:00
ruff server
: Support Jupyter Notebook (*.ipynb
) files (#11206)
## Summary Closes https://github.com/astral-sh/ruff/issues/10858. `ruff server` now supports `*.ipynb` (aka Jupyter Notebook) files. Extensive internal changes have been made to facilitate this, which I've done some work to contextualize with documentation and an pre-review that highlights notable sections of the code. `*.ipynb` cells should behave similarly to `*.py` documents, with one major exception. The format command `ruff.applyFormat` will only apply to the currently selected notebook cell - if you want to format an entire notebook document, use `Format Notebook` from the VS Code context menu. ## Test Plan The VS Code extension does not yet have Jupyter Notebook support enabled, so you'll first need to enable it manually. To do this, checkout the `pre-release` branch and modify `src/common/server.ts` as follows: Before:  After:  I recommend testing this PR with large, complicated notebook files. I used notebook files from [this popular repository](https://github.com/jakevdp/PythonDataScienceHandbook/tree/master/notebooks) in my preliminary testing. The main thing to test is ensuring that notebook cells behave the same as Python documents, besides the aforementioned issue with `ruff.applyFormat`. You should also test adding and deleting cells (in particular, deleting all the code cells and ensure that doesn't break anything), changing the kind of a cell (i.e. from markup -> code or vice versa), and creating a new notebook file from scratch. Finally, you should also test that source actions work as expected (and across the entire notebook). Note: `ruff.applyAutofix` and `ruff.applyOrganizeImports` are currently broken for notebook files, and I suspect it has something to do with https://github.com/astral-sh/ruff/issues/11248. Once this is fixed, I will update the test plan accordingly. --------- Co-authored-by: nolan <nolan.king90@gmail.com>
This commit is contained in:
parent
84531d1644
commit
b0731ef9cb
39 changed files with 1584 additions and 622 deletions
464
crates/ruff_server/src/session/index.rs
Normal file
464
crates/ruff_server/src/session/index.rs
Normal file
|
@ -0,0 +1,464 @@
|
|||
use anyhow::anyhow;
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
edit::{DocumentKey, DocumentVersion, NotebookDocument},
|
||||
PositionEncoding, TextDocument,
|
||||
};
|
||||
|
||||
use super::{
|
||||
settings::{self, ResolvedClientSettings},
|
||||
ClientSettings,
|
||||
};
|
||||
|
||||
mod ruff_settings;
|
||||
|
||||
pub(crate) use ruff_settings::RuffSettings;
|
||||
|
||||
type DocumentIndex = FxHashMap<PathBuf, DocumentController>;
|
||||
type NotebookCellIndex = FxHashMap<lsp_types::Url, PathBuf>;
|
||||
type SettingsIndex = BTreeMap<PathBuf, WorkspaceSettings>;
|
||||
|
||||
/// Stores and tracks all open documents in a session, along with their associated settings.
|
||||
#[derive(Default)]
|
||||
pub(crate) struct Index {
|
||||
/// Maps all document file paths to the associated document controller
|
||||
documents: DocumentIndex,
|
||||
/// Maps opaque cell URLs to a notebook path
|
||||
notebook_cells: NotebookCellIndex,
|
||||
/// Maps a workspace folder root to its settings.
|
||||
settings: SettingsIndex,
|
||||
}
|
||||
|
||||
/// Settings associated with a workspace.
|
||||
struct WorkspaceSettings {
|
||||
client_settings: ResolvedClientSettings,
|
||||
workspace_settings_index: ruff_settings::RuffSettingsIndex,
|
||||
}
|
||||
|
||||
/// A mutable handler to an underlying document.
|
||||
enum DocumentController {
|
||||
Text(Arc<TextDocument>),
|
||||
Notebook(Arc<NotebookDocument>),
|
||||
}
|
||||
|
||||
/// A read-only query to an open document.
|
||||
/// This query can 'select' a text document, full notebook, or a specific notebook cell.
|
||||
/// It also includes document settings.
|
||||
#[derive(Clone)]
|
||||
pub(crate) enum DocumentQuery {
|
||||
Text {
|
||||
file_path: PathBuf,
|
||||
document: Arc<TextDocument>,
|
||||
settings: Arc<RuffSettings>,
|
||||
},
|
||||
Notebook {
|
||||
/// The selected notebook cell, if it exists.
|
||||
cell_uri: Option<lsp_types::Url>,
|
||||
file_path: PathBuf,
|
||||
notebook: Arc<NotebookDocument>,
|
||||
settings: Arc<RuffSettings>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Index {
|
||||
pub(super) fn new(
|
||||
workspace_folders: Vec<(PathBuf, ClientSettings)>,
|
||||
global_settings: &ClientSettings,
|
||||
) -> Self {
|
||||
let mut settings_index = BTreeMap::new();
|
||||
for (path, workspace_settings) in workspace_folders {
|
||||
Self::register_workspace_settings(
|
||||
&mut settings_index,
|
||||
path,
|
||||
Some(workspace_settings),
|
||||
global_settings,
|
||||
);
|
||||
}
|
||||
|
||||
Self {
|
||||
documents: FxHashMap::default(),
|
||||
notebook_cells: FxHashMap::default(),
|
||||
settings: settings_index,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn update_text_document(
|
||||
&mut self,
|
||||
key: &DocumentKey,
|
||||
content_changes: Vec<lsp_types::TextDocumentContentChangeEvent>,
|
||||
new_version: DocumentVersion,
|
||||
encoding: PositionEncoding,
|
||||
) -> crate::Result<()> {
|
||||
let controller = self.document_controller_for_key(key)?;
|
||||
let Some(document) = controller.as_text_mut() else {
|
||||
anyhow::bail!("Text document URI does not point to a text document");
|
||||
};
|
||||
|
||||
if content_changes.is_empty() {
|
||||
document.update_version(new_version);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
document.apply_changes(content_changes, new_version, encoding);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn key_from_url(&self, url: &lsp_types::Url) -> Option<DocumentKey> {
|
||||
if self.notebook_cells.contains_key(url) {
|
||||
return Some(DocumentKey::NotebookCell(url.clone()));
|
||||
}
|
||||
let path = url.to_file_path().ok()?;
|
||||
Some(
|
||||
match path
|
||||
.extension()
|
||||
.unwrap_or_default()
|
||||
.to_str()
|
||||
.unwrap_or_default()
|
||||
{
|
||||
"ipynb" => DocumentKey::Notebook(path),
|
||||
_ => DocumentKey::Text(path),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn update_notebook_document(
|
||||
&mut self,
|
||||
key: &DocumentKey,
|
||||
cells: Option<lsp_types::NotebookDocumentCellChange>,
|
||||
metadata: Option<serde_json::Map<String, serde_json::Value>>,
|
||||
new_version: DocumentVersion,
|
||||
encoding: PositionEncoding,
|
||||
) -> crate::Result<()> {
|
||||
// update notebook cell index
|
||||
if let Some(lsp_types::NotebookDocumentCellChangeStructure {
|
||||
did_open,
|
||||
did_close,
|
||||
..
|
||||
}) = cells.as_ref().and_then(|cells| cells.structure.as_ref())
|
||||
{
|
||||
let Some(path) = self.path_for_key(key).cloned() else {
|
||||
anyhow::bail!("Tried to open unavailable document `{key}`");
|
||||
};
|
||||
|
||||
for opened_cell in did_open.iter().flatten() {
|
||||
self.notebook_cells
|
||||
.insert(opened_cell.uri.clone(), path.clone());
|
||||
}
|
||||
for closed_cell in did_close.iter().flatten() {
|
||||
if self.notebook_cells.remove(&closed_cell.uri).is_none() {
|
||||
tracing::warn!(
|
||||
"Tried to remove a notebook cell that does not exist: {}",
|
||||
closed_cell.uri
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let controller = self.document_controller_for_key(key)?;
|
||||
let Some(notebook) = controller.as_notebook_mut() else {
|
||||
anyhow::bail!("Notebook document URI does not point to a notebook document");
|
||||
};
|
||||
|
||||
notebook.update(cells, metadata, new_version, encoding)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn open_workspace_folder(
|
||||
&mut self,
|
||||
path: PathBuf,
|
||||
global_settings: &ClientSettings,
|
||||
) {
|
||||
// TODO(jane): Find a way for workspace client settings to be added or changed dynamically.
|
||||
Self::register_workspace_settings(&mut self.settings, path, None, global_settings);
|
||||
}
|
||||
|
||||
fn register_workspace_settings(
|
||||
settings_index: &mut SettingsIndex,
|
||||
workspace_path: PathBuf,
|
||||
workspace_settings: Option<ClientSettings>,
|
||||
global_settings: &ClientSettings,
|
||||
) {
|
||||
let client_settings = if let Some(workspace_settings) = workspace_settings {
|
||||
ResolvedClientSettings::with_workspace(&workspace_settings, global_settings)
|
||||
} else {
|
||||
ResolvedClientSettings::global(global_settings)
|
||||
};
|
||||
let workspace_settings_index = ruff_settings::RuffSettingsIndex::new(
|
||||
&workspace_path,
|
||||
client_settings.editor_settings(),
|
||||
);
|
||||
|
||||
settings_index.insert(
|
||||
workspace_path,
|
||||
WorkspaceSettings {
|
||||
client_settings,
|
||||
workspace_settings_index,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn close_workspace_folder(&mut self, workspace_path: &PathBuf) -> crate::Result<()> {
|
||||
self.settings.remove(workspace_path).ok_or_else(|| {
|
||||
anyhow!(
|
||||
"Tried to remove non-existent folder {}",
|
||||
workspace_path.display()
|
||||
)
|
||||
})?;
|
||||
// O(n) complexity, which isn't ideal... but this is an uncommon operation.
|
||||
self.documents
|
||||
.retain(|path, _| !path.starts_with(workspace_path));
|
||||
self.notebook_cells
|
||||
.retain(|_, path| !path.starts_with(workspace_path));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn make_document_ref(&self, key: DocumentKey) -> Option<DocumentQuery> {
|
||||
let path = self.path_for_key(&key)?.clone();
|
||||
let document_settings = self
|
||||
.settings_for_path(&path)?
|
||||
.workspace_settings_index
|
||||
.get(&path);
|
||||
|
||||
let controller = self.documents.get(&path)?;
|
||||
let cell_uri = match key {
|
||||
DocumentKey::NotebookCell(uri) => Some(uri),
|
||||
_ => None,
|
||||
};
|
||||
Some(controller.make_ref(cell_uri, path, document_settings))
|
||||
}
|
||||
|
||||
pub(super) fn reload_settings(&mut self, changed_path: &PathBuf) {
|
||||
for (root, settings) in self
|
||||
.settings
|
||||
.iter_mut()
|
||||
.filter(|(path, _)| path.starts_with(changed_path))
|
||||
{
|
||||
settings.workspace_settings_index = ruff_settings::RuffSettingsIndex::new(
|
||||
root,
|
||||
settings.client_settings.editor_settings(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn open_text_document(&mut self, path: PathBuf, document: TextDocument) {
|
||||
self.documents
|
||||
.insert(path, DocumentController::new_text(document));
|
||||
}
|
||||
|
||||
pub(super) fn open_notebook_document(&mut self, path: PathBuf, document: NotebookDocument) {
|
||||
for url in document.urls() {
|
||||
self.notebook_cells.insert(url.clone(), path.clone());
|
||||
}
|
||||
self.documents
|
||||
.insert(path, DocumentController::new_notebook(document));
|
||||
}
|
||||
|
||||
pub(super) fn close_document(&mut self, key: &DocumentKey) -> crate::Result<()> {
|
||||
let Some(path) = self.path_for_key(key).cloned() else {
|
||||
anyhow::bail!("Tried to open unavailable document `{key}`");
|
||||
};
|
||||
|
||||
let Some(controller) = self.documents.remove(&path) else {
|
||||
anyhow::bail!(
|
||||
"tried to close document that didn't exist at {}",
|
||||
path.display()
|
||||
)
|
||||
};
|
||||
if let Some(notebook) = controller.as_notebook() {
|
||||
for url in notebook.urls() {
|
||||
self.notebook_cells.remove(url).ok_or_else(|| {
|
||||
anyhow!("tried to de-register notebook cell with URL {url} that didn't exist")
|
||||
})?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn client_settings(
|
||||
&self,
|
||||
key: &DocumentKey,
|
||||
global_settings: &ClientSettings,
|
||||
) -> settings::ResolvedClientSettings {
|
||||
let Some(path) = self.path_for_key(key) else {
|
||||
return ResolvedClientSettings::global(global_settings);
|
||||
};
|
||||
let Some(WorkspaceSettings {
|
||||
client_settings, ..
|
||||
}) = self.settings_for_path(path)
|
||||
else {
|
||||
return ResolvedClientSettings::global(global_settings);
|
||||
};
|
||||
client_settings.clone()
|
||||
}
|
||||
|
||||
fn document_controller_for_key(
|
||||
&mut self,
|
||||
key: &DocumentKey,
|
||||
) -> crate::Result<&mut DocumentController> {
|
||||
let Some(path) = self.path_for_key(key).cloned() else {
|
||||
anyhow::bail!("Tried to open unavailable document `{key}`");
|
||||
};
|
||||
let Some(controller) = self.documents.get_mut(&path) else {
|
||||
anyhow::bail!("Document controller not available at `{}`", path.display());
|
||||
};
|
||||
Ok(controller)
|
||||
}
|
||||
|
||||
fn path_for_key<'a>(&'a self, key: &'a DocumentKey) -> Option<&'a PathBuf> {
|
||||
match key {
|
||||
DocumentKey::Notebook(path) | DocumentKey::Text(path) => Some(path),
|
||||
DocumentKey::NotebookCell(uri) => self.notebook_cells.get(uri),
|
||||
}
|
||||
}
|
||||
|
||||
fn settings_for_path(&self, path: &Path) -> Option<&WorkspaceSettings> {
|
||||
self.settings
|
||||
.range(..path.to_path_buf())
|
||||
.next_back()
|
||||
.map(|(_, settings)| settings)
|
||||
}
|
||||
}
|
||||
|
||||
impl DocumentController {
|
||||
fn new_text(document: TextDocument) -> Self {
|
||||
Self::Text(Arc::new(document))
|
||||
}
|
||||
|
||||
fn new_notebook(document: NotebookDocument) -> Self {
|
||||
Self::Notebook(Arc::new(document))
|
||||
}
|
||||
|
||||
fn make_ref(
|
||||
&self,
|
||||
cell_uri: Option<lsp_types::Url>,
|
||||
file_path: PathBuf,
|
||||
settings: Arc<RuffSettings>,
|
||||
) -> DocumentQuery {
|
||||
match &self {
|
||||
Self::Notebook(notebook) => DocumentQuery::Notebook {
|
||||
cell_uri,
|
||||
file_path,
|
||||
notebook: notebook.clone(),
|
||||
settings,
|
||||
},
|
||||
Self::Text(document) => DocumentQuery::Text {
|
||||
file_path,
|
||||
document: document.clone(),
|
||||
settings,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn as_notebook_mut(&mut self) -> Option<&mut NotebookDocument> {
|
||||
Some(match self {
|
||||
Self::Notebook(notebook) => Arc::make_mut(notebook),
|
||||
Self::Text(_) => return None,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn as_notebook(&self) -> Option<&NotebookDocument> {
|
||||
match self {
|
||||
Self::Notebook(notebook) => Some(notebook),
|
||||
Self::Text(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn as_text(&self) -> Option<&TextDocument> {
|
||||
match self {
|
||||
Self::Text(document) => Some(document),
|
||||
Self::Notebook(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn as_text_mut(&mut self) -> Option<&mut TextDocument> {
|
||||
Some(match self {
|
||||
Self::Text(document) => Arc::make_mut(document),
|
||||
Self::Notebook(_) => return None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl DocumentQuery {
|
||||
/// Retrieve the original key that describes this document query.
|
||||
pub(crate) fn make_key(&self) -> DocumentKey {
|
||||
match self {
|
||||
Self::Text { file_path, .. } => DocumentKey::Text(file_path.clone()),
|
||||
Self::Notebook {
|
||||
cell_uri: Some(cell_uri),
|
||||
..
|
||||
} => DocumentKey::NotebookCell(cell_uri.clone()),
|
||||
Self::Notebook { file_path, .. } => DocumentKey::Notebook(file_path.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the document settings associated with this query.
|
||||
pub(crate) fn settings(&self) -> &RuffSettings {
|
||||
match self {
|
||||
Self::Text { settings, .. } | Self::Notebook { settings, .. } => settings,
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a source kind used by the linter.
|
||||
pub(crate) fn make_source_kind(&self) -> ruff_linter::source_kind::SourceKind {
|
||||
match self {
|
||||
Self::Text { document, .. } => {
|
||||
ruff_linter::source_kind::SourceKind::Python(document.contents().to_string())
|
||||
}
|
||||
Self::Notebook { notebook, .. } => {
|
||||
ruff_linter::source_kind::SourceKind::IpyNotebook(notebook.make_ruff_notebook())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempts to access the underlying notebook document that this query is selecting.
|
||||
pub(crate) fn as_notebook(&self) -> Option<&NotebookDocument> {
|
||||
match self {
|
||||
Self::Notebook { notebook, .. } => Some(notebook),
|
||||
Self::Text { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the source type of the document associated with this query.
|
||||
pub(crate) fn source_type(&self) -> ruff_python_ast::PySourceType {
|
||||
match self {
|
||||
Self::Text { .. } => ruff_python_ast::PySourceType::Python,
|
||||
Self::Notebook { .. } => ruff_python_ast::PySourceType::Ipynb,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the version of document selected by this query.
|
||||
pub(crate) fn version(&self) -> DocumentVersion {
|
||||
match self {
|
||||
Self::Text { document, .. } => document.version(),
|
||||
Self::Notebook { notebook, .. } => notebook.version(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the underlying file path for the document selected by this query.
|
||||
pub(crate) fn file_path(&self) -> &PathBuf {
|
||||
match self {
|
||||
Self::Text { file_path, .. } | Self::Notebook { file_path, .. } => file_path,
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempt to access the single inner text document selected by the query.
|
||||
/// If this query is selecting an entire notebook document, this will return `None`.
|
||||
pub(crate) fn as_single_document(&self) -> Option<&TextDocument> {
|
||||
match self {
|
||||
Self::Text { document, .. } => Some(document),
|
||||
Self::Notebook {
|
||||
notebook, cell_uri, ..
|
||||
} => cell_uri
|
||||
.as_ref()
|
||||
.and_then(|cell_uri| notebook.cell_document_by_uri(cell_uri)),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,7 +7,7 @@ use serde::Deserialize;
|
|||
use ruff_linter::{line_width::LineLength, RuleSelector};
|
||||
|
||||
/// Maps a workspace URI to its associated client settings. Used during server initialization.
|
||||
pub(crate) type WorkspaceSettingsMap = FxHashMap<Url, ClientSettings>;
|
||||
pub(crate) type WorkspaceSettingsMap = FxHashMap<PathBuf, ClientSettings>;
|
||||
|
||||
/// 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
|
||||
|
@ -169,7 +169,12 @@ impl AllSettings {
|
|||
workspace_settings: workspace_settings.map(|workspace_settings| {
|
||||
workspace_settings
|
||||
.into_iter()
|
||||
.map(|settings| (settings.workspace, settings.settings))
|
||||
.map(|settings| {
|
||||
(
|
||||
settings.workspace.to_file_path().unwrap(),
|
||||
settings.settings,
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}),
|
||||
}
|
||||
|
@ -360,7 +365,7 @@ mod tests {
|
|||
serde_json::from_str(content).expect("test fixture JSON should deserialize")
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(not(windows), test)]
|
||||
fn test_vs_code_init_options_deserialize() {
|
||||
let options: InitializationOptions = deserialize_fixture(VS_CODE_INIT_OPTIONS_FIXTURE);
|
||||
|
||||
|
@ -545,19 +550,19 @@ mod tests {
|
|||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg_attr(not(windows), test)]
|
||||
fn test_vs_code_workspace_settings_resolve() {
|
||||
let options = deserialize_fixture(VS_CODE_INIT_OPTIONS_FIXTURE);
|
||||
let AllSettings {
|
||||
global_settings,
|
||||
workspace_settings,
|
||||
} = AllSettings::from_init_options(options);
|
||||
let url = Url::parse("file:///Users/test/projects/pandas").expect("url should parse");
|
||||
let path = PathBuf::from_str("/Users/test/projects/pandas").expect("path should be valid");
|
||||
let workspace_settings = workspace_settings.expect("workspace settings should exist");
|
||||
assert_eq!(
|
||||
ResolvedClientSettings::with_workspace(
|
||||
workspace_settings
|
||||
.get(&url)
|
||||
.get(&path)
|
||||
.expect("workspace setting should exist"),
|
||||
&global_settings
|
||||
),
|
||||
|
@ -583,11 +588,11 @@ mod tests {
|
|||
}
|
||||
}
|
||||
);
|
||||
let url = Url::parse("file:///Users/test/projects/scipy").expect("url should parse");
|
||||
let path = PathBuf::from_str("/Users/test/projects/scipy").expect("path should be valid");
|
||||
assert_eq!(
|
||||
ResolvedClientSettings::with_workspace(
|
||||
workspace_settings
|
||||
.get(&url)
|
||||
.get(&path)
|
||||
.expect("workspace setting should exist"),
|
||||
&global_settings
|
||||
),
|
||||
|
|
|
@ -1,269 +0,0 @@
|
|||
use anyhow::anyhow;
|
||||
use lsp_types::Url;
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
ops::Deref,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use crate::{edit::DocumentVersion, Document};
|
||||
|
||||
use self::ruff_settings::RuffSettingsIndex;
|
||||
|
||||
use super::{
|
||||
settings::{self, ResolvedClientSettings, ResolvedEditorSettings},
|
||||
ClientSettings,
|
||||
};
|
||||
|
||||
mod ruff_settings;
|
||||
|
||||
pub(crate) use ruff_settings::RuffSettings;
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct Workspaces(BTreeMap<PathBuf, Workspace>);
|
||||
|
||||
pub(crate) struct Workspace {
|
||||
open_documents: OpenDocuments,
|
||||
settings: ResolvedClientSettings,
|
||||
}
|
||||
|
||||
pub(crate) struct OpenDocuments {
|
||||
documents: FxHashMap<Url, DocumentController>,
|
||||
settings_index: ruff_settings::RuffSettingsIndex,
|
||||
}
|
||||
|
||||
/// 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>,
|
||||
settings: Arc<RuffSettings>,
|
||||
}
|
||||
|
||||
impl Workspaces {
|
||||
pub(super) fn new(
|
||||
workspaces: Vec<(Url, ClientSettings)>,
|
||||
global_settings: &ClientSettings,
|
||||
) -> crate::Result<Self> {
|
||||
Ok(Self(
|
||||
workspaces
|
||||
.into_iter()
|
||||
.map(|(url, workspace_settings)| {
|
||||
Workspace::new(&url, &workspace_settings, global_settings)
|
||||
})
|
||||
.collect::<crate::Result<_>>()?,
|
||||
))
|
||||
}
|
||||
|
||||
pub(super) fn open_workspace_folder(
|
||||
&mut self,
|
||||
folder_url: &Url,
|
||||
global_settings: &ClientSettings,
|
||||
) -> 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(), global_settings)?;
|
||||
self.0.insert(path, workspace);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) 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(())
|
||||
}
|
||||
|
||||
pub(super) fn snapshot(&self, document_url: &Url) -> Option<DocumentRef> {
|
||||
self.workspace_for_url(document_url)?
|
||||
.open_documents
|
||||
.snapshot(document_url)
|
||||
}
|
||||
|
||||
pub(super) fn controller(&mut self, document_url: &Url) -> Option<&mut DocumentController> {
|
||||
self.workspace_for_url_mut(document_url)?
|
||||
.open_documents
|
||||
.controller(document_url)
|
||||
}
|
||||
|
||||
pub(super) fn reload_settings(&mut self, changed_url: &Url) -> crate::Result<()> {
|
||||
let (root, workspace) = self
|
||||
.entry_for_url_mut(changed_url)
|
||||
.ok_or_else(|| anyhow!("Workspace not found for {changed_url}"))?;
|
||||
workspace.reload_settings(root);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) 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);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) 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)
|
||||
}
|
||||
|
||||
pub(super) fn client_settings(
|
||||
&self,
|
||||
url: &Url,
|
||||
global_settings: &ClientSettings,
|
||||
) -> settings::ResolvedClientSettings {
|
||||
self.workspace_for_url(url).map_or_else(
|
||||
|| {
|
||||
tracing::warn!(
|
||||
"Workspace not found for {url}. Global settings will be used for this document"
|
||||
);
|
||||
settings::ResolvedClientSettings::global(global_settings)
|
||||
},
|
||||
|workspace| workspace.settings.clone(),
|
||||
)
|
||||
}
|
||||
|
||||
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,
|
||||
workspace_settings: &ClientSettings,
|
||||
global_settings: &ClientSettings,
|
||||
) -> crate::Result<(PathBuf, Self)> {
|
||||
let path = root
|
||||
.to_file_path()
|
||||
.map_err(|()| anyhow!("workspace URL was not a file path!"))?;
|
||||
|
||||
let settings = ResolvedClientSettings::with_workspace(workspace_settings, global_settings);
|
||||
|
||||
let workspace = Self {
|
||||
open_documents: OpenDocuments::new(&path, settings.editor_settings()),
|
||||
settings,
|
||||
};
|
||||
|
||||
Ok((path, workspace))
|
||||
}
|
||||
|
||||
fn reload_settings(&mut self, root: &Path) {
|
||||
self.open_documents
|
||||
.reload_settings(root, self.settings.editor_settings());
|
||||
}
|
||||
}
|
||||
|
||||
impl OpenDocuments {
|
||||
fn new(path: &Path, editor_settings: &ResolvedEditorSettings) -> Self {
|
||||
Self {
|
||||
documents: FxHashMap::default(),
|
||||
settings_index: RuffSettingsIndex::new(path, editor_settings),
|
||||
}
|
||||
}
|
||||
|
||||
fn snapshot(&self, url: &Url) -> Option<DocumentRef> {
|
||||
let path = url
|
||||
.to_file_path()
|
||||
.expect("document URL should convert to file path: {url}");
|
||||
let document_settings = self.settings_index.get(&path);
|
||||
Some(self.documents.get(url)?.make_ref(document_settings))
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
fn reload_settings(&mut self, root: &Path, editor_settings: &ResolvedEditorSettings) {
|
||||
self.settings_index = RuffSettingsIndex::new(root, editor_settings);
|
||||
}
|
||||
}
|
||||
|
||||
impl DocumentController {
|
||||
fn new(contents: String, version: DocumentVersion) -> Self {
|
||||
Self {
|
||||
document: Arc::new(Document::new(contents, version)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn make_ref(&self, document_settings: Arc<RuffSettings>) -> DocumentRef {
|
||||
DocumentRef {
|
||||
document: self.document.clone(),
|
||||
settings: document_settings,
|
||||
}
|
||||
}
|
||||
|
||||
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 DocumentRef {
|
||||
pub(crate) fn settings(&self) -> &RuffSettings {
|
||||
&self.settings
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue