[ty] Fix stale documents on Windows (#18544)
Some checks are pending
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run

This commit is contained in:
Micha Reiser 2025-06-09 16:39:11 +02:00 committed by GitHub
parent ae2150bfa3
commit b44062b9ae
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 249 additions and 193 deletions

View file

@ -7,6 +7,8 @@ mod text_document;
pub(crate) use location::ToLink; pub(crate) use location::ToLink;
use lsp_types::{PositionEncodingKind, Url}; use lsp_types::{PositionEncodingKind, Url};
use crate::system::AnySystemPath;
pub use notebook::NotebookDocument; pub use notebook::NotebookDocument;
pub(crate) use range::{FileRangeExt, PositionExt, RangeExt, TextSizeExt, ToRangeExt}; pub(crate) use range::{FileRangeExt, PositionExt, RangeExt, TextSizeExt, ToRangeExt};
pub(crate) use text_document::DocumentVersion; pub(crate) use text_document::DocumentVersion;
@ -40,19 +42,38 @@ impl From<PositionEncoding> for ruff_source_file::PositionEncoding {
/// A unique document ID, derived from a URL passed as part of an LSP request. /// A unique document ID, derived from a URL passed as part of an LSP request.
/// This document ID can point to either be a standalone Python file, a full notebook, or a cell within a notebook. /// This document ID can point to either be a standalone Python file, a full notebook, or a cell within a notebook.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum DocumentKey { pub(crate) enum DocumentKey {
Notebook(Url), Notebook(AnySystemPath),
NotebookCell(Url), NotebookCell {
Text(Url), cell_url: Url,
notebook_path: AnySystemPath,
},
Text(AnySystemPath),
} }
impl DocumentKey { impl DocumentKey {
/// Returns the URL associated with the key. /// Returns the file path associated with the key.
pub(crate) fn url(&self) -> &Url { pub(crate) fn path(&self) -> &AnySystemPath {
match self { match self {
DocumentKey::NotebookCell(url) DocumentKey::Notebook(path) | DocumentKey::Text(path) => path,
| DocumentKey::Notebook(url) DocumentKey::NotebookCell { notebook_path, .. } => notebook_path,
| DocumentKey::Text(url) => url, }
}
pub(crate) fn from_path(path: AnySystemPath) -> Self {
// For text documents, we assume it's a text document unless it's a notebook file.
match path.extension() {
Some("ipynb") => Self::Notebook(path),
_ => Self::Text(path),
}
}
/// Returns the URL for this document key. For notebook cells, returns the cell URL.
/// For other document types, converts the path to a URL.
pub(crate) fn to_url(&self) -> Option<Url> {
match self {
DocumentKey::NotebookCell { cell_url, .. } => Some(cell_url.clone()),
DocumentKey::Notebook(path) | DocumentKey::Text(path) => path.to_url(),
} }
} }
} }
@ -60,7 +81,11 @@ impl DocumentKey {
impl std::fmt::Display for DocumentKey { impl std::fmt::Display for DocumentKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
Self::NotebookCell(url) | Self::Notebook(url) | Self::Text(url) => url.fmt(f), Self::NotebookCell { cell_url, .. } => cell_url.fmt(f),
Self::Notebook(path) | Self::Text(path) => match path {
AnySystemPath::System(system_path) => system_path.fmt(f),
AnySystemPath::SystemVirtual(virtual_path) => virtual_path.fmt(f),
},
} }
} }
} }

View file

@ -1,6 +1,6 @@
use crate::server::{ConnectionInitializer, Server}; use crate::server::{ConnectionInitializer, Server};
use anyhow::Context; use anyhow::Context;
pub use document::{DocumentKey, NotebookDocument, PositionEncoding, TextDocument}; pub use document::{NotebookDocument, PositionEncoding, TextDocument};
pub use session::{ClientSettings, DocumentQuery, DocumentSnapshot, Session}; pub use session::{ClientSettings, DocumentQuery, DocumentSnapshot, Session};
use std::num::NonZeroUsize; use std::num::NonZeroUsize;

View file

@ -1,6 +1,6 @@
use crate::server::schedule::Task; use crate::server::schedule::Task;
use crate::session::Session; use crate::session::Session;
use crate::system::{AnySystemPath, url_to_any_system_path}; use crate::system::AnySystemPath;
use anyhow::anyhow; use anyhow::anyhow;
use lsp_server as server; use lsp_server as server;
use lsp_server::RequestId; use lsp_server::RequestId;
@ -154,7 +154,7 @@ where
let url = R::document_url(&params).into_owned(); let url = R::document_url(&params).into_owned();
let Ok(path) = url_to_any_system_path(&url) else { let Ok(path) = AnySystemPath::try_from_url(&url) else {
tracing::warn!("Ignoring request for invalid `{url}`"); tracing::warn!("Ignoring request for invalid `{url}`");
return Box::new(|_| {}); return Box::new(|_| {});
}; };

View file

@ -12,10 +12,9 @@ use ruff_db::source::{line_index, source_text};
use ty_project::{Db, ProjectDatabase}; use ty_project::{Db, ProjectDatabase};
use super::LSPResult; use super::LSPResult;
use crate::document::{FileRangeExt, ToRangeExt}; use crate::document::{DocumentKey, FileRangeExt, ToRangeExt};
use crate::server::Result; use crate::server::Result;
use crate::session::client::Client; use crate::session::client::Client;
use crate::system::url_to_any_system_path;
use crate::{DocumentSnapshot, PositionEncoding, Session}; use crate::{DocumentSnapshot, PositionEncoding, Session};
/// Represents the diagnostics for a text document or a notebook document. /// Represents the diagnostics for a text document or a notebook document.
@ -42,13 +41,20 @@ impl Diagnostics {
} }
} }
/// Clears the diagnostics for the document at `uri`. /// Clears the diagnostics for the document identified by `key`.
/// ///
/// This is done by notifying the client with an empty list of diagnostics for the document. /// This is done by notifying the client with an empty list of diagnostics for the document.
pub(super) fn clear_diagnostics(uri: &Url, client: &Client) -> Result<()> { /// For notebook cells, this clears diagnostics for the specific cell.
/// For other document types, this clears diagnostics for the main document.
pub(super) fn clear_diagnostics(key: &DocumentKey, client: &Client) -> Result<()> {
let Some(uri) = key.to_url() else {
// If we can't convert to URL, we can't clear diagnostics
return Ok(());
};
client client
.send_notification::<PublishDiagnostics>(PublishDiagnosticsParams { .send_notification::<PublishDiagnostics>(PublishDiagnosticsParams {
uri: uri.clone(), uri,
diagnostics: vec![], diagnostics: vec![],
version: None, version: None,
}) })
@ -62,21 +68,27 @@ pub(super) fn clear_diagnostics(uri: &Url, client: &Client) -> Result<()> {
/// This function is a no-op if the client supports pull diagnostics. /// This function is a no-op if the client supports pull diagnostics.
/// ///
/// [publish diagnostics notification]: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_publishDiagnostics /// [publish diagnostics notification]: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_publishDiagnostics
pub(super) fn publish_diagnostics(session: &Session, url: Url, client: &Client) -> Result<()> { pub(super) fn publish_diagnostics(
session: &Session,
key: &DocumentKey,
client: &Client,
) -> Result<()> {
if session.client_capabilities().pull_diagnostics { if session.client_capabilities().pull_diagnostics {
return Ok(()); return Ok(());
} }
let Ok(path) = url_to_any_system_path(&url) else { let Some(url) = key.to_url() else {
return Ok(()); return Ok(());
}; };
let path = key.path();
let snapshot = session let snapshot = session
.take_snapshot(url.clone()) .take_snapshot(url.clone())
.ok_or_else(|| anyhow::anyhow!("Unable to take snapshot for document with URL {url}")) .ok_or_else(|| anyhow::anyhow!("Unable to take snapshot for document with URL {url}"))
.with_failure_code(lsp_server::ErrorCode::InternalError)?; .with_failure_code(lsp_server::ErrorCode::InternalError)?;
let db = session.project_db_or_default(&path); let db = session.project_db_or_default(path);
let Some(diagnostics) = compute_diagnostics(db, &snapshot) else { let Some(diagnostics) = compute_diagnostics(db, &snapshot) else {
return Ok(()); return Ok(());

View file

@ -8,7 +8,7 @@ use crate::server::api::diagnostics::publish_diagnostics;
use crate::server::api::traits::{NotificationHandler, SyncNotificationHandler}; use crate::server::api::traits::{NotificationHandler, SyncNotificationHandler};
use crate::session::Session; use crate::session::Session;
use crate::session::client::Client; use crate::session::client::Client;
use crate::system::{AnySystemPath, url_to_any_system_path}; use crate::system::AnySystemPath;
use ty_project::watch::ChangeEvent; use ty_project::watch::ChangeEvent;
pub(crate) struct DidChangeTextDocumentHandler; pub(crate) struct DidChangeTextDocumentHandler;
@ -28,30 +28,32 @@ impl SyncNotificationHandler for DidChangeTextDocumentHandler {
content_changes, content_changes,
} = params; } = params;
let Ok(path) = url_to_any_system_path(&uri) else { let Ok(key) = session.key_from_url(uri.clone()) else {
tracing::debug!("Failed to create document key from URI: {}", uri);
return Ok(()); return Ok(());
}; };
let key = session.key_from_url(uri.clone());
session session
.update_text_document(&key, content_changes, version) .update_text_document(&key, content_changes, version)
.with_failure_code(ErrorCode::InternalError)?; .with_failure_code(ErrorCode::InternalError)?;
match path { match key.path() {
AnySystemPath::System(path) => { AnySystemPath::System(path) => {
let db = match session.project_db_for_path_mut(path.as_std_path()) { let db = match session.project_db_for_path_mut(path.as_std_path()) {
Some(db) => db, Some(db) => db,
None => session.default_project_db_mut(), None => session.default_project_db_mut(),
}; };
db.apply_changes(vec![ChangeEvent::file_content_changed(path)], None); db.apply_changes(vec![ChangeEvent::file_content_changed(path.clone())], None);
} }
AnySystemPath::SystemVirtual(virtual_path) => { AnySystemPath::SystemVirtual(virtual_path) => {
let db = session.default_project_db_mut(); let db = session.default_project_db_mut();
db.apply_changes(vec![ChangeEvent::ChangedVirtual(virtual_path)], None); db.apply_changes(
vec![ChangeEvent::ChangedVirtual(virtual_path.clone())],
None,
);
} }
} }
publish_diagnostics(session, uri, client) publish_diagnostics(session, &key, client)
} }
} }

View file

@ -4,7 +4,7 @@ use crate::server::api::diagnostics::publish_diagnostics;
use crate::server::api::traits::{NotificationHandler, SyncNotificationHandler}; use crate::server::api::traits::{NotificationHandler, SyncNotificationHandler};
use crate::session::Session; use crate::session::Session;
use crate::session::client::Client; use crate::session::client::Client;
use crate::system::{AnySystemPath, url_to_any_system_path}; use crate::system::AnySystemPath;
use lsp_types as types; use lsp_types as types;
use lsp_types::{FileChangeType, notification as notif}; use lsp_types::{FileChangeType, notification as notif};
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
@ -26,7 +26,7 @@ impl SyncNotificationHandler for DidChangeWatchedFiles {
let mut events_by_db: FxHashMap<_, Vec<ChangeEvent>> = FxHashMap::default(); let mut events_by_db: FxHashMap<_, Vec<ChangeEvent>> = FxHashMap::default();
for change in params.changes { for change in params.changes {
let path = match url_to_any_system_path(&change.uri) { let path = match AnySystemPath::try_from_url(&change.uri) {
Ok(path) => path, Ok(path) => path,
Err(err) => { Err(err) => {
tracing::warn!( tracing::warn!(
@ -111,8 +111,8 @@ impl SyncNotificationHandler for DidChangeWatchedFiles {
) )
.with_failure_code(lsp_server::ErrorCode::InternalError)?; .with_failure_code(lsp_server::ErrorCode::InternalError)?;
} else { } else {
for url in session.text_document_urls() { for key in session.text_document_keys() {
publish_diagnostics(session, url.clone(), client)?; publish_diagnostics(session, &key, client)?;
} }
} }

View file

@ -4,7 +4,7 @@ use crate::server::api::diagnostics::clear_diagnostics;
use crate::server::api::traits::{NotificationHandler, SyncNotificationHandler}; use crate::server::api::traits::{NotificationHandler, SyncNotificationHandler};
use crate::session::Session; use crate::session::Session;
use crate::session::client::Client; use crate::session::client::Client;
use crate::system::{AnySystemPath, url_to_any_system_path}; use crate::system::AnySystemPath;
use lsp_server::ErrorCode; use lsp_server::ErrorCode;
use lsp_types::DidCloseTextDocumentParams; use lsp_types::DidCloseTextDocumentParams;
use lsp_types::notification::DidCloseTextDocument; use lsp_types::notification::DidCloseTextDocument;
@ -22,22 +22,25 @@ impl SyncNotificationHandler for DidCloseTextDocumentHandler {
client: &Client, client: &Client,
params: DidCloseTextDocumentParams, params: DidCloseTextDocumentParams,
) -> Result<()> { ) -> Result<()> {
let Ok(path) = url_to_any_system_path(&params.text_document.uri) else { let Ok(key) = session.key_from_url(params.text_document.uri.clone()) else {
tracing::debug!(
"Failed to create document key from URI: {}",
params.text_document.uri
);
return Ok(()); return Ok(());
}; };
let key = session.key_from_url(params.text_document.uri);
session session
.close_document(&key) .close_document(&key)
.with_failure_code(ErrorCode::InternalError)?; .with_failure_code(ErrorCode::InternalError)?;
if let AnySystemPath::SystemVirtual(virtual_path) = path { if let AnySystemPath::SystemVirtual(virtual_path) = key.path() {
let db = session.default_project_db_mut(); let db = session.default_project_db_mut();
db.apply_changes(vec![ChangeEvent::DeletedVirtual(virtual_path)], None); db.apply_changes(
vec![ChangeEvent::DeletedVirtual(virtual_path.clone())],
None,
);
} }
clear_diagnostics(key.url(), client)?; clear_diagnostics(&key, client)
Ok(())
} }
} }

View file

@ -6,7 +6,7 @@ use crate::server::api::LSPResult;
use crate::server::api::traits::{NotificationHandler, SyncNotificationHandler}; use crate::server::api::traits::{NotificationHandler, SyncNotificationHandler};
use crate::session::Session; use crate::session::Session;
use crate::session::client::Client; use crate::session::client::Client;
use crate::system::{AnySystemPath, url_to_any_system_path}; use crate::system::AnySystemPath;
use ty_project::watch::ChangeEvent; use ty_project::watch::ChangeEvent;
pub(crate) struct DidCloseNotebookHandler; pub(crate) struct DidCloseNotebookHandler;
@ -21,18 +21,23 @@ impl SyncNotificationHandler for DidCloseNotebookHandler {
_client: &Client, _client: &Client,
params: DidCloseNotebookDocumentParams, params: DidCloseNotebookDocumentParams,
) -> Result<()> { ) -> Result<()> {
let Ok(path) = url_to_any_system_path(&params.notebook_document.uri) else { let Ok(key) = session.key_from_url(params.notebook_document.uri.clone()) else {
tracing::debug!(
"Failed to create document key from URI: {}",
params.notebook_document.uri
);
return Ok(()); return Ok(());
}; };
let key = session.key_from_url(params.notebook_document.uri);
session session
.close_document(&key) .close_document(&key)
.with_failure_code(lsp_server::ErrorCode::InternalError)?; .with_failure_code(lsp_server::ErrorCode::InternalError)?;
if let AnySystemPath::SystemVirtual(virtual_path) = path { if let AnySystemPath::SystemVirtual(virtual_path) = key.path() {
let db = session.default_project_db_mut(); let db = session.default_project_db_mut();
db.apply_changes(vec![ChangeEvent::DeletedVirtual(virtual_path)], None); db.apply_changes(
vec![ChangeEvent::DeletedVirtual(virtual_path.clone())],
None,
);
} }
Ok(()) Ok(())

View file

@ -7,7 +7,7 @@ use crate::server::api::diagnostics::publish_diagnostics;
use crate::server::api::traits::{NotificationHandler, SyncNotificationHandler}; use crate::server::api::traits::{NotificationHandler, SyncNotificationHandler};
use crate::session::Session; use crate::session::Session;
use crate::session::client::Client; use crate::session::client::Client;
use crate::system::{AnySystemPath, url_to_any_system_path}; use crate::system::AnySystemPath;
use ruff_db::Db; use ruff_db::Db;
use ty_project::watch::ChangeEvent; use ty_project::watch::ChangeEvent;
@ -31,20 +31,21 @@ impl SyncNotificationHandler for DidOpenTextDocumentHandler {
}, },
}: DidOpenTextDocumentParams, }: DidOpenTextDocumentParams,
) -> Result<()> { ) -> Result<()> {
let Ok(path) = url_to_any_system_path(&uri) else { let Ok(key) = session.key_from_url(uri.clone()) else {
tracing::debug!("Failed to create document key from URI: {}", uri);
return Ok(()); return Ok(());
}; };
let document = TextDocument::new(text, version).with_language_id(&language_id); let document = TextDocument::new(text, version).with_language_id(&language_id);
session.open_text_document(uri.clone(), document); session.open_text_document(key.path(), document);
match &path { match key.path() {
AnySystemPath::System(path) => { AnySystemPath::System(system_path) => {
let db = match session.project_db_for_path_mut(path.as_std_path()) { let db = match session.project_db_for_path_mut(system_path.as_std_path()) {
Some(db) => db, Some(db) => db,
None => session.default_project_db_mut(), None => session.default_project_db_mut(),
}; };
db.apply_changes(vec![ChangeEvent::Opened(path.clone())], None); db.apply_changes(vec![ChangeEvent::Opened(system_path.clone())], None);
} }
AnySystemPath::SystemVirtual(virtual_path) => { AnySystemPath::SystemVirtual(virtual_path) => {
let db = session.default_project_db_mut(); let db = session.default_project_db_mut();
@ -52,6 +53,6 @@ impl SyncNotificationHandler for DidOpenTextDocumentHandler {
} }
} }
publish_diagnostics(session, uri, client) publish_diagnostics(session, &key, client)
} }
} }

View file

@ -11,7 +11,7 @@ use crate::server::api::LSPResult;
use crate::server::api::traits::{NotificationHandler, SyncNotificationHandler}; use crate::server::api::traits::{NotificationHandler, SyncNotificationHandler};
use crate::session::Session; use crate::session::Session;
use crate::session::client::Client; use crate::session::client::Client;
use crate::system::{AnySystemPath, url_to_any_system_path}; use crate::system::AnySystemPath;
pub(crate) struct DidOpenNotebookHandler; pub(crate) struct DidOpenNotebookHandler;
@ -25,7 +25,7 @@ impl SyncNotificationHandler for DidOpenNotebookHandler {
_client: &Client, _client: &Client,
params: DidOpenNotebookDocumentParams, params: DidOpenNotebookDocumentParams,
) -> Result<()> { ) -> Result<()> {
let Ok(path) = url_to_any_system_path(&params.notebook_document.uri) else { let Ok(path) = AnySystemPath::try_from_url(&params.notebook_document.uri) else {
return Ok(()); return Ok(());
}; };
@ -36,19 +36,19 @@ impl SyncNotificationHandler for DidOpenNotebookHandler {
params.cell_text_documents, params.cell_text_documents,
) )
.with_failure_code(ErrorCode::InternalError)?; .with_failure_code(ErrorCode::InternalError)?;
session.open_notebook_document(params.notebook_document.uri, notebook); session.open_notebook_document(&path, notebook);
match path { match &path {
AnySystemPath::System(path) => { AnySystemPath::System(system_path) => {
let db = match session.project_db_for_path_mut(path.as_std_path()) { let db = match session.project_db_for_path_mut(system_path.as_std_path()) {
Some(db) => db, Some(db) => db,
None => session.default_project_db_mut(), None => session.default_project_db_mut(),
}; };
db.apply_changes(vec![ChangeEvent::Opened(path)], None); db.apply_changes(vec![ChangeEvent::Opened(system_path.clone())], None);
} }
AnySystemPath::SystemVirtual(virtual_path) => { AnySystemPath::SystemVirtual(virtual_path) => {
let db = session.default_project_db_mut(); let db = session.default_project_db_mut();
db.files().virtual_file(db, &virtual_path); db.files().virtual_file(db, virtual_path);
} }
} }

View file

@ -19,7 +19,7 @@ pub use self::settings::ClientSettings;
pub(crate) use self::settings::Experimental; pub(crate) use self::settings::Experimental;
use crate::document::{DocumentKey, DocumentVersion, NotebookDocument}; use crate::document::{DocumentKey, DocumentVersion, NotebookDocument};
use crate::session::request_queue::RequestQueue; use crate::session::request_queue::RequestQueue;
use crate::system::{AnySystemPath, LSPSystem, url_to_any_system_path}; use crate::system::{AnySystemPath, LSPSystem};
use crate::{PositionEncoding, TextDocument}; use crate::{PositionEncoding, TextDocument};
mod capabilities; mod capabilities;
@ -158,35 +158,43 @@ impl Session {
.unwrap() .unwrap()
} }
pub fn key_from_url(&self, url: Url) -> DocumentKey { pub(crate) fn key_from_url(&self, url: Url) -> crate::Result<DocumentKey> {
self.index().key_from_url(url) self.index().key_from_url(url)
} }
/// Creates a document snapshot with the URL referencing the document to snapshot. /// Creates a document snapshot with the URL referencing the document to snapshot.
///
/// Returns `None` if the url can't be converted to a document key or if the document isn't open.
pub fn take_snapshot(&self, url: Url) -> Option<DocumentSnapshot> { pub fn take_snapshot(&self, url: Url) -> Option<DocumentSnapshot> {
let key = self.key_from_url(url); let key = self.key_from_url(url).ok()?;
Some(DocumentSnapshot { Some(DocumentSnapshot {
resolved_client_capabilities: self.resolved_client_capabilities.clone(), resolved_client_capabilities: self.resolved_client_capabilities.clone(),
document_ref: self.index().make_document_ref(key)?, document_ref: self.index().make_document_ref(&key)?,
position_encoding: self.position_encoding, position_encoding: self.position_encoding,
}) })
} }
/// Iterates over the LSP URLs for all open text documents. These URLs are valid file paths. /// Iterates over the document keys for all open text documents.
pub(super) fn text_document_urls(&self) -> impl Iterator<Item = &Url> + '_ { pub(super) fn text_document_keys(&self) -> impl Iterator<Item = DocumentKey> + '_ {
self.index().text_document_urls() self.index()
.text_document_paths()
.map(|path| DocumentKey::Text(path.clone()))
} }
/// Registers a notebook document at the provided `url`. /// Registers a notebook document at the provided `path`.
/// If a document is already open here, it will be overwritten. /// If a document is already open here, it will be overwritten.
pub fn open_notebook_document(&mut self, url: Url, document: NotebookDocument) { pub(crate) fn open_notebook_document(
self.index_mut().open_notebook_document(url, document); &mut self,
path: &AnySystemPath,
document: NotebookDocument,
) {
self.index_mut().open_notebook_document(path, document);
} }
/// Registers a text document at the provided `url`. /// Registers a text document at the provided `path`.
/// If a document is already open here, it will be overwritten. /// If a document is already open here, it will be overwritten.
pub(crate) fn open_text_document(&mut self, url: Url, document: TextDocument) { pub(crate) fn open_text_document(&mut self, path: &AnySystemPath, document: TextDocument) {
self.index_mut().open_text_document(url, document); self.index_mut().open_text_document(path, document);
} }
/// Updates a text document at the associated `key`. /// Updates a text document at the associated `key`.
@ -314,7 +322,7 @@ impl DocumentSnapshot {
} }
pub(crate) fn file(&self, db: &dyn Db) -> Option<File> { pub(crate) fn file(&self, db: &dyn Db) -> Option<File> {
match url_to_any_system_path(self.document_ref.file_url()).ok()? { match AnySystemPath::try_from_url(self.document_ref.file_url()).ok()? {
AnySystemPath::System(path) => system_path_to_file(db, path).ok(), AnySystemPath::System(path) => system_path_to_file(db, path).ok(),
AnySystemPath::SystemVirtual(virtual_path) => db AnySystemPath::SystemVirtual(virtual_path) => db
.files() .files()

View file

@ -1,4 +1,3 @@
use std::path::Path;
use std::sync::Arc; use std::sync::Arc;
use lsp_types::Url; use lsp_types::Url;
@ -7,6 +6,7 @@ use rustc_hash::FxHashMap;
use crate::{ use crate::{
PositionEncoding, TextDocument, PositionEncoding, TextDocument,
document::{DocumentKey, DocumentVersion, NotebookDocument}, document::{DocumentKey, DocumentVersion, NotebookDocument},
system::AnySystemPath,
}; };
use super::ClientSettings; use super::ClientSettings;
@ -14,11 +14,11 @@ use super::ClientSettings;
/// Stores and tracks all open documents in a session, along with their associated settings. /// Stores and tracks all open documents in a session, along with their associated settings.
#[derive(Default, Debug)] #[derive(Default, Debug)]
pub(crate) struct Index { pub(crate) struct Index {
/// Maps all document file URLs to the associated document controller /// Maps all document file paths to the associated document controller
documents: FxHashMap<Url, DocumentController>, documents: FxHashMap<AnySystemPath, DocumentController>,
/// Maps opaque cell URLs to a notebook URL (document) /// Maps opaque cell URLs to a notebook path (document)
notebook_cells: FxHashMap<Url, Url>, notebook_cells: FxHashMap<Url, AnySystemPath>,
/// Global settings provided by the client. /// Global settings provided by the client.
#[expect(dead_code)] #[expect(dead_code)]
@ -34,18 +34,18 @@ impl Index {
} }
} }
pub(super) fn text_document_urls(&self) -> impl Iterator<Item = &Url> + '_ { pub(super) fn text_document_paths(&self) -> impl Iterator<Item = &AnySystemPath> + '_ {
self.documents self.documents
.iter() .iter()
.filter_map(|(url, doc)| doc.as_text().and(Some(url))) .filter_map(|(path, doc)| doc.as_text().and(Some(path)))
} }
#[expect(dead_code)] #[expect(dead_code)]
pub(super) fn notebook_document_urls(&self) -> impl Iterator<Item = &Url> + '_ { pub(super) fn notebook_document_paths(&self) -> impl Iterator<Item = &AnySystemPath> + '_ {
self.documents self.documents
.iter() .iter()
.filter(|(_, doc)| doc.as_notebook().is_some()) .filter(|(_, doc)| doc.as_notebook().is_some())
.map(|(url, _)| url) .map(|(path, _)| path)
} }
pub(super) fn update_text_document( pub(super) fn update_text_document(
@ -57,7 +57,7 @@ impl Index {
) -> crate::Result<()> { ) -> crate::Result<()> {
let controller = self.document_controller_for_key(key)?; let controller = self.document_controller_for_key(key)?;
let Some(document) = controller.as_text_mut() else { let Some(document) = controller.as_text_mut() else {
anyhow::bail!("Text document URI does not point to a text document"); anyhow::bail!("Text document path does not point to a text document");
}; };
if content_changes.is_empty() { if content_changes.is_empty() {
@ -70,16 +70,24 @@ impl Index {
Ok(()) Ok(())
} }
pub(crate) fn key_from_url(&self, url: Url) -> DocumentKey { pub(crate) fn key_from_url(&self, url: Url) -> crate::Result<DocumentKey> {
if self.notebook_cells.contains_key(&url) { if let Some(notebook_path) = self.notebook_cells.get(&url) {
DocumentKey::NotebookCell(url) Ok(DocumentKey::NotebookCell {
} else if Path::new(url.path()) cell_url: url,
notebook_path: notebook_path.clone(),
})
} else {
let path = AnySystemPath::try_from_url(&url)
.map_err(|()| anyhow::anyhow!("Failed to convert URL to system path: {}", url))?;
if path
.extension() .extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("ipynb")) .is_some_and(|ext| ext.eq_ignore_ascii_case("ipynb"))
{ {
DocumentKey::Notebook(url) Ok(DocumentKey::Notebook(path))
} else { } else {
DocumentKey::Text(url) Ok(DocumentKey::Text(path))
}
} }
} }
@ -98,48 +106,55 @@ impl Index {
.. ..
}) = cells.as_ref().and_then(|cells| cells.structure.as_ref()) }) = cells.as_ref().and_then(|cells| cells.structure.as_ref())
{ {
let Some(path) = self.url_for_key(key).cloned() else { let notebook_path = key.path().clone();
anyhow::bail!("Tried to open unavailable document `{key}`");
};
for opened_cell in did_open { for opened_cell in did_open {
self.notebook_cells self.notebook_cells
.insert(opened_cell.uri.clone(), path.clone()); .insert(opened_cell.uri.clone(), notebook_path.clone());
} }
// deleted notebook cells are closed via textDocument/didClose - we don't close them here. // deleted notebook cells are closed via textDocument/didClose - we don't close them here.
} }
let controller = self.document_controller_for_key(key)?; let controller = self.document_controller_for_key(key)?;
let Some(notebook) = controller.as_notebook_mut() else { let Some(notebook) = controller.as_notebook_mut() else {
anyhow::bail!("Notebook document URI does not point to a notebook document"); anyhow::bail!("Notebook document path does not point to a notebook document");
}; };
notebook.update(cells, metadata, new_version, encoding)?; notebook.update(cells, metadata, new_version, encoding)?;
Ok(()) Ok(())
} }
pub(crate) fn make_document_ref(&self, key: DocumentKey) -> Option<DocumentQuery> { pub(crate) fn make_document_ref(&self, key: &DocumentKey) -> Option<DocumentQuery> {
let url = self.url_for_key(&key)?.clone(); let path = key.path();
let controller = self.documents.get(&url)?; let controller = self.documents.get(path)?;
let cell_url = match key { let (cell_url, file_url) = match &key {
DocumentKey::NotebookCell(cell_url) => Some(cell_url), DocumentKey::NotebookCell {
_ => None, cell_url,
notebook_path,
} => (Some(cell_url.clone()), notebook_path.to_url()?),
DocumentKey::Notebook(path) | DocumentKey::Text(path) => (None, path.to_url()?),
}; };
Some(controller.make_ref(cell_url, url)) Some(controller.make_ref(cell_url, file_url))
} }
pub(super) fn open_text_document(&mut self, url: Url, document: TextDocument) { pub(super) fn open_text_document(&mut self, path: &AnySystemPath, document: TextDocument) {
self.documents self.documents
.insert(url, DocumentController::new_text(document)); .insert(path.clone(), DocumentController::new_text(document));
} }
pub(super) fn open_notebook_document(&mut self, notebook_url: Url, document: NotebookDocument) { pub(super) fn open_notebook_document(
&mut self,
notebook_path: &AnySystemPath,
document: NotebookDocument,
) {
for cell_url in document.cell_urls() { for cell_url in document.cell_urls() {
self.notebook_cells self.notebook_cells
.insert(cell_url.clone(), notebook_url.clone()); .insert(cell_url.clone(), notebook_path.clone());
} }
self.documents self.documents.insert(
.insert(notebook_url, DocumentController::new_notebook(document)); notebook_path.clone(),
DocumentController::new_notebook(document),
);
} }
pub(super) fn close_document(&mut self, key: &DocumentKey) -> crate::Result<()> { pub(super) fn close_document(&mut self, key: &DocumentKey) -> crate::Result<()> {
@ -148,18 +163,16 @@ impl Index {
// is requested to be `closed` by VS Code after the notebook gets updated. // is requested to be `closed` by VS Code after the notebook gets updated.
// This is not documented in the LSP specification explicitly, and this assumption // This is not documented in the LSP specification explicitly, and this assumption
// may need revisiting in the future as we support more editors with notebook support. // may need revisiting in the future as we support more editors with notebook support.
if let DocumentKey::NotebookCell(uri) = key { if let DocumentKey::NotebookCell { cell_url, .. } = key {
if self.notebook_cells.remove(uri).is_none() { if self.notebook_cells.remove(cell_url).is_none() {
tracing::warn!("Tried to remove a notebook cell that does not exist: {uri}",); tracing::warn!("Tried to remove a notebook cell that does not exist: {cell_url}",);
} }
return Ok(()); return Ok(());
} }
let Some(url) = self.url_for_key(key).cloned() else { let path = key.path();
anyhow::bail!("Tried to close unavailable document `{key}`");
};
let Some(_) = self.documents.remove(&url) else { let Some(_) = self.documents.remove(path) else {
anyhow::bail!("tried to close document that didn't exist at {}", url) anyhow::bail!("tried to close document that didn't exist at {}", key)
}; };
Ok(()) Ok(())
} }
@ -168,21 +181,12 @@ impl Index {
&mut self, &mut self,
key: &DocumentKey, key: &DocumentKey,
) -> crate::Result<&mut DocumentController> { ) -> crate::Result<&mut DocumentController> {
let Some(url) = self.url_for_key(key).cloned() else { let path = key.path();
anyhow::bail!("Tried to open unavailable document `{key}`"); let Some(controller) = self.documents.get_mut(path) else {
}; anyhow::bail!("Document controller not available at `{}`", key);
let Some(controller) = self.documents.get_mut(&url) else {
anyhow::bail!("Document controller not available at `{}`", url);
}; };
Ok(controller) Ok(controller)
} }
fn url_for_key<'a>(&'a self, key: &'a DocumentKey) -> Option<&'a Url> {
match key {
DocumentKey::Notebook(path) | DocumentKey::Text(path) => Some(path),
DocumentKey::NotebookCell(uri) => self.notebook_cells.get(uri),
}
}
} }
/// A mutable handler to an underlying document. /// A mutable handler to an underlying document.
@ -263,19 +267,6 @@ pub enum DocumentQuery {
} }
impl DocumentQuery { impl DocumentQuery {
/// Retrieve the original key that describes this document query.
#[expect(dead_code)]
pub(crate) fn make_key(&self) -> DocumentKey {
match self {
Self::Text { file_url, .. } => DocumentKey::Text(file_url.clone()),
Self::Notebook {
cell_url: Some(cell_uri),
..
} => DocumentKey::NotebookCell(cell_uri.clone()),
Self::Notebook { file_url, .. } => DocumentKey::Notebook(file_url.clone()),
}
}
/// Attempts to access the underlying notebook document that this query is selecting. /// Attempts to access the underlying notebook document that this query is selecting.
pub fn as_notebook(&self) -> Option<&NotebookDocument> { pub fn as_notebook(&self) -> Option<&NotebookDocument> {
match self { match self {

View file

@ -14,28 +14,9 @@ use ruff_notebook::{Notebook, NotebookError};
use ty_python_semantic::Db; use ty_python_semantic::Db;
use crate::DocumentQuery; use crate::DocumentQuery;
use crate::document::DocumentKey;
use crate::session::index::Index; use crate::session::index::Index;
/// Converts the given [`Url`] to an [`AnySystemPath`].
///
/// If the URL scheme is `file`, then the path is converted to a [`SystemPathBuf`]. Otherwise, the
/// URL is converted to a [`SystemVirtualPathBuf`].
///
/// This fails in the following cases:
/// * The URL cannot be converted to a file path (refer to [`Url::to_file_path`]).
/// * If the URL is not a valid UTF-8 string.
pub(crate) fn url_to_any_system_path(url: &Url) -> std::result::Result<AnySystemPath, ()> {
if url.scheme() == "file" {
Ok(AnySystemPath::System(
SystemPathBuf::from_path_buf(url.to_file_path()?).map_err(|_| ())?,
))
} else {
Ok(AnySystemPath::SystemVirtual(
SystemVirtualPath::new(url.as_str()).to_path_buf(),
))
}
}
pub(crate) fn file_to_url(db: &dyn Db, file: File) -> Option<Url> { pub(crate) fn file_to_url(db: &dyn Db, file: File) -> Option<Url> {
match file.path(db) { match file.path(db) {
FilePath::System(system) => Url::from_file_path(system.as_std_path()).ok(), FilePath::System(system) => Url::from_file_path(system.as_std_path()).ok(),
@ -47,19 +28,57 @@ pub(crate) fn file_to_url(db: &dyn Db, file: File) -> Option<Url> {
} }
/// Represents either a [`SystemPath`] or a [`SystemVirtualPath`]. /// Represents either a [`SystemPath`] or a [`SystemVirtualPath`].
#[derive(Clone, Debug)] #[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub(crate) enum AnySystemPath { pub(crate) enum AnySystemPath {
System(SystemPathBuf), System(SystemPathBuf),
SystemVirtual(SystemVirtualPathBuf), SystemVirtual(SystemVirtualPathBuf),
} }
impl AnySystemPath { impl AnySystemPath {
/// Converts the given [`Url`] to an [`AnySystemPath`].
///
/// If the URL scheme is `file`, then the path is converted to a [`SystemPathBuf`]. Otherwise, the
/// URL is converted to a [`SystemVirtualPathBuf`].
///
/// This fails in the following cases:
/// * The URL cannot be converted to a file path (refer to [`Url::to_file_path`]).
/// * If the URL is not a valid UTF-8 string.
pub(crate) fn try_from_url(url: &Url) -> std::result::Result<Self, ()> {
if url.scheme() == "file" {
Ok(AnySystemPath::System(
SystemPathBuf::from_path_buf(url.to_file_path()?).map_err(|_| ())?,
))
} else {
Ok(AnySystemPath::SystemVirtual(
SystemVirtualPath::new(url.as_str()).to_path_buf(),
))
}
}
pub(crate) const fn as_system(&self) -> Option<&SystemPathBuf> { pub(crate) const fn as_system(&self) -> Option<&SystemPathBuf> {
match self { match self {
AnySystemPath::System(system_path_buf) => Some(system_path_buf), AnySystemPath::System(system_path_buf) => Some(system_path_buf),
AnySystemPath::SystemVirtual(_) => None, AnySystemPath::SystemVirtual(_) => None,
} }
} }
/// Returns the extension of the path, if any.
pub(crate) fn extension(&self) -> Option<&str> {
match self {
AnySystemPath::System(system_path) => system_path.extension(),
AnySystemPath::SystemVirtual(virtual_path) => virtual_path.extension(),
}
}
/// Converts the path to a URL.
pub(crate) fn to_url(&self) -> Option<Url> {
match self {
AnySystemPath::System(system_path) => {
Url::from_file_path(system_path.as_std_path()).ok()
}
AnySystemPath::SystemVirtual(virtual_path) => Url::parse(virtual_path.as_str()).ok(),
}
}
} }
#[derive(Debug)] #[derive(Debug)]
@ -107,39 +126,29 @@ impl LSPSystem {
self.index.as_ref().unwrap() self.index.as_ref().unwrap()
} }
fn make_document_ref(&self, url: Url) -> Option<DocumentQuery> { fn make_document_ref(&self, path: AnySystemPath) -> Option<DocumentQuery> {
let index = self.index(); let index = self.index();
let key = index.key_from_url(url); let key = DocumentKey::from_path(path);
index.make_document_ref(key) index.make_document_ref(&key)
} }
fn system_path_to_document_ref(&self, path: &SystemPath) -> Result<Option<DocumentQuery>> { fn system_path_to_document_ref(&self, path: &SystemPath) -> Option<DocumentQuery> {
let url = Url::from_file_path(path.as_std_path()).map_err(|()| { let any_path = AnySystemPath::System(path.to_path_buf());
std::io::Error::new( self.make_document_ref(any_path)
std::io::ErrorKind::InvalidInput,
format!("Failed to convert system path to URL: {path:?}"),
)
})?;
Ok(self.make_document_ref(url))
} }
fn system_virtual_path_to_document_ref( fn system_virtual_path_to_document_ref(
&self, &self,
path: &SystemVirtualPath, path: &SystemVirtualPath,
) -> Result<Option<DocumentQuery>> { ) -> Option<DocumentQuery> {
let url = Url::parse(path.as_str()).map_err(|_| { let any_path = AnySystemPath::SystemVirtual(path.to_path_buf());
std::io::Error::new( self.make_document_ref(any_path)
std::io::ErrorKind::InvalidInput,
format!("Failed to convert virtual path to URL: {path:?}"),
)
})?;
Ok(self.make_document_ref(url))
} }
} }
impl System for LSPSystem { impl System for LSPSystem {
fn path_metadata(&self, path: &SystemPath) -> Result<Metadata> { fn path_metadata(&self, path: &SystemPath) -> Result<Metadata> {
let document = self.system_path_to_document_ref(path)?; let document = self.system_path_to_document_ref(path);
if let Some(document) = document { if let Some(document) = document {
Ok(Metadata::new( Ok(Metadata::new(
@ -161,7 +170,7 @@ impl System for LSPSystem {
} }
fn read_to_string(&self, path: &SystemPath) -> Result<String> { fn read_to_string(&self, path: &SystemPath) -> Result<String> {
let document = self.system_path_to_document_ref(path)?; let document = self.system_path_to_document_ref(path);
match document { match document {
Some(DocumentQuery::Text { document, .. }) => Ok(document.contents().to_string()), Some(DocumentQuery::Text { document, .. }) => Ok(document.contents().to_string()),
@ -170,7 +179,7 @@ impl System for LSPSystem {
} }
fn read_to_notebook(&self, path: &SystemPath) -> std::result::Result<Notebook, NotebookError> { fn read_to_notebook(&self, path: &SystemPath) -> std::result::Result<Notebook, NotebookError> {
let document = self.system_path_to_document_ref(path)?; let document = self.system_path_to_document_ref(path);
match document { match document {
Some(DocumentQuery::Text { document, .. }) => { Some(DocumentQuery::Text { document, .. }) => {
@ -183,7 +192,7 @@ impl System for LSPSystem {
fn read_virtual_path_to_string(&self, path: &SystemVirtualPath) -> Result<String> { fn read_virtual_path_to_string(&self, path: &SystemVirtualPath) -> Result<String> {
let document = self let document = self
.system_virtual_path_to_document_ref(path)? .system_virtual_path_to_document_ref(path)
.ok_or_else(|| virtual_path_not_found(path))?; .ok_or_else(|| virtual_path_not_found(path))?;
if let DocumentQuery::Text { document, .. } = &document { if let DocumentQuery::Text { document, .. } = &document {
@ -198,7 +207,7 @@ impl System for LSPSystem {
path: &SystemVirtualPath, path: &SystemVirtualPath,
) -> std::result::Result<Notebook, NotebookError> { ) -> std::result::Result<Notebook, NotebookError> {
let document = self let document = self
.system_virtual_path_to_document_ref(path)? .system_virtual_path_to_document_ref(path)
.ok_or_else(|| virtual_path_not_found(path))?; .ok_or_else(|| virtual_path_not_found(path))?;
match document { match document {