Skip to content

Commit 249643e

Browse files
committed
Add notebook support to LSP server
Implement comprehensive notebook document support: - Refactor NotebookDocument to simplify cell management - Remove embedded TextDocuments, defer to index - Add to_ruff_notebook() for converting to linter format - Implement LSP notification handlers for notebook lifecycle: - did_open_notebook: Open notebook and cell documents - did_close_notebook: Close notebook and cell documents - did_change_notebook: Handle cell structure and content changes - Update all LSP request handlers to use new conversion APIs: - Navigation (goto definition, references, declaration, type def) - Information (hover, completion, inlay hints, signature help) - Symbols (document and workspace symbols) - All handlers now use to_local_range()/to_location() appropriately - Add notebook diagnostics support: - Per-cell diagnostic publishing - Workspace diagnostics for notebooks - Update session and index management: - Track notebook documents and their cells - Map cell URIs to parent notebooks - Handle notebook file changes - Extend system path handling for notebook URIs - Enable notebook capabilities in server initialization - Add comprehensive E2E tests for notebooks This enables the LSP server to work with Jupyter notebooks by treating each cell as a separate text document while maintaining the notebook context for type checking and cross-cell references. Discard changes to crates/ty_ide/src/importer.rs Discard changes to crates/ty_server/src/server/api/requests/diagnostic.rs Discard changes to crates/ty_server/src/server/api/semantic_tokens.rs
1 parent fcd67f2 commit 249643e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+1869
-725
lines changed

crates/ruff_db/src/source.rs

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use ruff_source_file::LineIndex;
77

88
use crate::Db;
99
use crate::files::{File, FilePath};
10+
use crate::system::System;
1011

1112
/// Reads the source text of a python text file (must be valid UTF8) or notebook.
1213
#[salsa::tracked(heap_size=ruff_memory_usage::heap_size)]
@@ -15,7 +16,7 @@ pub fn source_text(db: &dyn Db, file: File) -> SourceText {
1516
let _span = tracing::trace_span!("source_text", file = %path).entered();
1617
let mut read_error = None;
1718

18-
let kind = if is_notebook(file.path(db)) {
19+
let kind = if is_notebook(db.system(), path) {
1920
file.read_to_notebook(db)
2021
.unwrap_or_else(|error| {
2122
tracing::debug!("Failed to read notebook '{path}': {error}");
@@ -40,17 +41,19 @@ pub fn source_text(db: &dyn Db, file: File) -> SourceText {
4041
}
4142
}
4243

43-
fn is_notebook(path: &FilePath) -> bool {
44-
match path {
45-
FilePath::System(system) => system.extension().is_some_and(|extension| {
44+
fn is_notebook(system: &dyn System, path: &FilePath) -> bool {
45+
let source_type = match path {
46+
FilePath::System(path) => system.source_type(path),
47+
FilePath::SystemVirtual(system_virtual) => system.virtual_path_source_type(system_virtual),
48+
FilePath::Vendored(_) => return false,
49+
};
50+
51+
if let Some(source_type) = source_type {
52+
source_type.is_ipynb()
53+
} else {
54+
path.extension().is_some_and(|extension| {
4655
PySourceType::try_from_extension(extension) == Some(PySourceType::Ipynb)
47-
}),
48-
FilePath::SystemVirtual(system_virtual) => {
49-
system_virtual.extension().is_some_and(|extension| {
50-
PySourceType::try_from_extension(extension) == Some(PySourceType::Ipynb)
51-
})
52-
}
53-
FilePath::Vendored(_) => false,
56+
})
5457
}
5558
}
5659

crates/ruff_db/src/system.rs

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,19 @@ pub use os::OsSystem;
99

1010
use filetime::FileTime;
1111
use ruff_notebook::{Notebook, NotebookError};
12+
use ruff_python_ast::PySourceType;
1213
use std::error::Error;
1314
use std::fmt::{Debug, Formatter};
1415
use std::path::{Path, PathBuf};
1516
use std::{fmt, io};
1617
pub use test::{DbWithTestSystem, DbWithWritableSystem, InMemorySystem, TestSystem};
1718
use walk_directory::WalkDirectoryBuilder;
1819

19-
use crate::file_revision::FileRevision;
20-
2120
pub use self::path::{
2221
DeduplicatedNestedPathsIter, SystemPath, SystemPathBuf, SystemVirtualPath,
2322
SystemVirtualPathBuf, deduplicate_nested_paths,
2423
};
24+
use crate::file_revision::FileRevision;
2525

2626
mod memory_fs;
2727
#[cfg(feature = "os")]
@@ -66,6 +66,27 @@ pub trait System: Debug + Sync + Send {
6666
/// See [dunce::canonicalize] for more information.
6767
fn canonicalize_path(&self, path: &SystemPath) -> Result<SystemPathBuf>;
6868

69+
/// Returns the source type for `path` if known or `None`.
70+
///
71+
/// This is primarily used for the LSP integration to respect
72+
/// the chosen language (or the fact that it is a notebook) in
73+
/// the editor.
74+
fn source_type(&self, path: &SystemPath) -> Option<PySourceType> {
75+
let _ = path;
76+
None
77+
}
78+
79+
/// Returns the source type for `path` if known or `None`.
80+
///
81+
/// This is primarily used for the LSP integration to respect
82+
/// the chosen language (or the fact that it is a notebook) in
83+
/// the editor.
84+
fn virtual_path_source_type(&self, path: &SystemVirtualPath) -> Option<PySourceType> {
85+
let _ = path;
86+
87+
None
88+
}
89+
6990
/// Reads the content of the file at `path` into a [`String`].
7091
fn read_to_string(&self, path: &SystemPath) -> Result<String>;
7192

crates/ruff_notebook/src/index.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ impl NotebookIndex {
4747
/// Translates the given [`LineColumn`] based on the indexing table.
4848
///
4949
/// This will translate the row/column in the concatenated source code
50-
/// to the row/column in the Jupyter Notebook.
50+
/// to the row/column in the Jupyter Notebook cell.
5151
pub fn translate_line_column(&self, source_location: &LineColumn) -> LineColumn {
5252
LineColumn {
5353
line: self
@@ -60,7 +60,7 @@ impl NotebookIndex {
6060
/// Translates the given [`SourceLocation`] based on the indexing table.
6161
///
6262
/// This will translate the line/character in the concatenated source code
63-
/// to the line/character in the Jupyter Notebook.
63+
/// to the line/character in the Jupyter Notebook cell.
6464
pub fn translate_source_location(&self, source_location: &SourceLocation) -> SourceLocation {
6565
SourceLocation {
6666
line: self

crates/ruff_notebook/src/notebook.rs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use thiserror::Error;
1313

1414
use ruff_diagnostics::{SourceMap, SourceMarker};
1515
use ruff_source_file::{NewlineWithTrailingNewline, OneIndexed, UniversalNewlineIterator};
16-
use ruff_text_size::TextSize;
16+
use ruff_text_size::{TextRange, TextSize};
1717

1818
use crate::cell::CellOffsets;
1919
use crate::index::NotebookIndex;
@@ -294,7 +294,7 @@ impl Notebook {
294294
}
295295
}
296296

297-
/// Build and return the [`JupyterIndex`].
297+
/// Build and return the [`NotebookIndex`].
298298
///
299299
/// ## Notes
300300
///
@@ -388,6 +388,21 @@ impl Notebook {
388388
&self.cell_offsets
389389
}
390390

391+
/// Returns the start offset of the cell at index `cell` in the concatenated
392+
/// text document.
393+
pub fn cell_offset(&self, cell: OneIndexed) -> Option<TextSize> {
394+
self.cell_offsets.get(cell.to_zero_indexed()).copied()
395+
}
396+
397+
/// Returns the text range in the concatenated document of the cell
398+
/// with index `cell`.
399+
pub fn cell_range(&self, cell: OneIndexed) -> Option<TextRange> {
400+
let start = self.cell_offsets.get(cell.to_zero_indexed()).copied()?;
401+
let end = self.cell_offsets.get(cell.to_zero_indexed() + 1).copied()?;
402+
403+
Some(TextRange::new(start, end))
404+
}
405+
391406
/// Return `true` if the notebook has a trailing newline, `false` otherwise.
392407
pub fn trailing_newline(&self) -> bool {
393408
self.trailing_newline

crates/ty_server/src/capabilities.rs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
use lsp_types::{
22
ClientCapabilities, CompletionOptions, DeclarationCapability, DiagnosticOptions,
33
DiagnosticServerCapabilities, HoverProviderCapability, InlayHintOptions,
4-
InlayHintServerCapabilities, MarkupKind, OneOf, RenameOptions,
5-
SelectionRangeProviderCapability, SemanticTokensFullOptions, SemanticTokensLegend,
6-
SemanticTokensOptions, SemanticTokensServerCapabilities, ServerCapabilities,
7-
SignatureHelpOptions, TextDocumentSyncCapability, TextDocumentSyncKind,
4+
InlayHintServerCapabilities, MarkupKind, NotebookCellSelector, NotebookSelector, OneOf,
5+
RenameOptions, SelectionRangeProviderCapability, SemanticTokensFullOptions,
6+
SemanticTokensLegend, SemanticTokensOptions, SemanticTokensServerCapabilities,
7+
ServerCapabilities, SignatureHelpOptions, TextDocumentSyncCapability, TextDocumentSyncKind,
88
TextDocumentSyncOptions, TypeDefinitionProviderCapability, WorkDoneProgressOptions,
99
};
1010

@@ -422,6 +422,16 @@ pub(crate) fn server_capabilities(
422422
selection_range_provider: Some(SelectionRangeProviderCapability::Simple(true)),
423423
document_symbol_provider: Some(OneOf::Left(true)),
424424
workspace_symbol_provider: Some(OneOf::Left(true)),
425+
notebook_document_sync: Some(OneOf::Left(lsp_types::NotebookDocumentSyncOptions {
426+
save: Some(false),
427+
notebook_selector: [NotebookSelector::ByCells {
428+
notebook: None,
429+
cells: vec![NotebookCellSelector {
430+
language: "python".to_string(),
431+
}],
432+
}]
433+
.to_vec(),
434+
})),
425435
..Default::default()
426436
}
427437
}

crates/ty_server/src/document.rs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,15 @@ mod notebook;
55
mod range;
66
mod text_document;
77

8-
pub(crate) use location::ToLink;
98
use lsp_types::{PositionEncodingKind, Url};
9+
use ruff_db::system::{SystemPathBuf, SystemVirtualPath, SystemVirtualPathBuf};
1010

1111
use crate::system::AnySystemPath;
12+
pub(crate) use location::ToLink;
1213
pub use notebook::NotebookDocument;
1314
pub(crate) use range::{FileRangeExt, PositionExt, RangeExt, TextSizeExt, ToRangeExt};
14-
use ruff_db::system::{SystemPathBuf, SystemVirtualPath};
15-
pub(crate) use text_document::DocumentVersion;
1615
pub use text_document::TextDocument;
16+
pub(crate) use text_document::{DocumentVersion, LanguageId};
1717

1818
/// A convenient enumeration for supported text encodings. Can be converted to [`lsp_types::PositionEncodingKind`].
1919
// Please maintain the order from least to greatest priority for the derived `Ord` impl.
@@ -84,13 +84,6 @@ impl DocumentKey {
8484
}
8585
}
8686

87-
pub(crate) fn as_opaque(&self) -> Option<&str> {
88-
match self {
89-
Self::Opaque(uri) => Some(uri),
90-
Self::File(_) => None,
91-
}
92-
}
93-
9487
/// Returns the corresponding [`AnySystemPath`] for this document key.
9588
///
9689
/// Note, calling this method on a `DocumentKey::Opaque` representing a cell document
@@ -104,6 +97,13 @@ impl DocumentKey {
10497
}
10598
}
10699
}
100+
101+
pub(super) fn into_file_path(self) -> AnySystemPath {
102+
match self {
103+
Self::File(path) => AnySystemPath::System(path),
104+
Self::Opaque(uri) => AnySystemPath::SystemVirtual(SystemVirtualPathBuf::from(uri)),
105+
}
106+
}
107107
}
108108

109109
impl From<AnySystemPath> for DocumentKey {

0 commit comments

Comments
 (0)