Skip to content

Commit ef8727f

Browse files
committed
Implement support for Jupyter Notebooks in ruff server
1 parent 0213eb8 commit ef8727f

37 files changed

+969
-414
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/ruff_notebook/src/cell.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ impl fmt::Display for SourceValue {
2323

2424
impl Cell {
2525
/// Return the [`SourceValue`] of the cell.
26-
pub(crate) fn source(&self) -> &SourceValue {
26+
pub fn source(&self) -> &SourceValue {
2727
match self {
2828
Cell::Code(cell) => &cell.source,
2929
Cell::Markdown(cell) => &cell.source,

crates/ruff_notebook/src/notebook.rs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use std::cmp::Ordering;
2-
use std::collections::HashSet;
2+
use std::collections::{BTreeMap, HashSet};
33
use std::fs::File;
44
use std::io::{BufReader, Cursor, Read, Seek, SeekFrom, Write};
55
use std::path::Path;
@@ -51,6 +51,15 @@ pub enum NotebookError {
5151
InvalidFormat(i64),
5252
}
5353

54+
pub enum NotebookSource {
55+
File(Notebook),
56+
Server {
57+
source_code: String,
58+
index: OnceCell<NotebookIndex>,
59+
cell_offsets: CellOffsets,
60+
},
61+
}
62+
5463
#[derive(Clone, Debug, PartialEq)]
5564
pub struct Notebook {
5665
/// Python source code of the notebook.
@@ -85,6 +94,24 @@ impl Notebook {
8594
Self::from_reader(Cursor::new(source_code))
8695
}
8796

97+
pub fn from_cells(cells: Vec<Cell>) -> Result<Self, NotebookError> {
98+
let raw_notebook = RawNotebook {
99+
cells,
100+
metadata: crate::RawNotebookMetadata {
101+
authors: None,
102+
kernelspec: None,
103+
language_info: None,
104+
orig_nbformat: None,
105+
title: None,
106+
extra: BTreeMap::default(),
107+
},
108+
nbformat: 4,
109+
nbformat_minor: 5,
110+
};
111+
112+
Self::from_raw(raw_notebook, false)
113+
}
114+
88115
/// Read a Jupyter Notebook from a [`Read`] implementer.
89116
///
90117
/// See also the black implementation
@@ -113,7 +140,13 @@ impl Notebook {
113140
});
114141
}
115142
};
143+
Self::from_raw(raw_notebook, trailing_newline)
144+
}
116145

146+
fn from_raw(
147+
mut raw_notebook: RawNotebook,
148+
trailing_newline: bool,
149+
) -> Result<Self, NotebookError> {
117150
// v4 is what everybody uses
118151
if raw_notebook.nbformat != 4 {
119152
// bail because we should have already failed at the json schema stage

crates/ruff_server/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ ruff_python_formatter = { path = "../ruff_python_formatter" }
2222
ruff_python_index = { path = "../ruff_python_index" }
2323
ruff_python_parser = { path = "../ruff_python_parser" }
2424
ruff_source_file = { path = "../ruff_source_file" }
25+
ruff_notebook = { path = "../ruff_notebook" }
2526
ruff_text_size = { path = "../ruff_text_size" }
2627
ruff_workspace = { path = "../ruff_workspace" }
2728

crates/ruff_server/src/edit.rs

Lines changed: 136 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,29 @@
11
//! Types and utilities for working with text, modifying source files, and `Ruff <-> LSP` type conversion.
22
33
mod document;
4+
mod notebook;
45
mod range;
56
mod replacement;
67

7-
use std::collections::HashMap;
8+
use std::{
9+
collections::HashMap,
10+
path::{Display, PathBuf},
11+
};
812

9-
pub use document::Document;
13+
use anyhow::anyhow;
1014
pub(crate) use document::DocumentVersion;
15+
pub use document::TextDocument;
1116
use lsp_types::PositionEncodingKind;
17+
pub(crate) use notebook::NotebookDocument;
1218
pub(crate) use range::{RangeExt, ToRangeExt};
1319
pub(crate) use replacement::Replacement;
20+
use ruff_linter::source_kind::SourceKind;
21+
use ruff_source_file::LineIndex;
1422

15-
use crate::session::ResolvedClientCapabilities;
23+
use crate::{
24+
fix::Fixes,
25+
session::{DocumentQuery, ResolvedClientCapabilities},
26+
};
1627

1728
/// A convenient enumeration for supported text encodings. Can be converted to [`lsp_types::PositionEncodingKind`].
1829
// Please maintain the order from least to greatest priority for the derived `Ord` impl.
@@ -29,6 +40,108 @@ pub enum PositionEncoding {
2940
UTF8,
3041
}
3142

43+
/// A wrapper for a document. Can be either a text document (`.py`)
44+
/// or a notebook document (`.ipynb`).
45+
#[derive(Clone)]
46+
pub(crate) enum Document {
47+
Text(TextDocument),
48+
Notebook(NotebookDocument),
49+
}
50+
51+
#[derive(Clone, Debug)]
52+
pub(crate) enum DocumentKey {
53+
File(PathBuf),
54+
Cell(lsp_types::Url),
55+
}
56+
57+
impl Document {
58+
pub(crate) fn version(&self) -> DocumentVersion {
59+
match self {
60+
Self::Notebook(notebook) => notebook.version(),
61+
Self::Text(py) => py.version(),
62+
}
63+
}
64+
65+
pub(crate) fn make_source_kind(&self) -> SourceKind {
66+
match self {
67+
Self::Notebook(notebook) => {
68+
let notebook = SourceKind::IpyNotebook(notebook.make_ruff_notebook());
69+
tracing::info!("{notebook:?}");
70+
notebook
71+
}
72+
Self::Text(text) => SourceKind::Python(text.contents().to_string()),
73+
}
74+
}
75+
76+
pub(crate) fn as_notebook(&self) -> Option<&NotebookDocument> {
77+
match self {
78+
Self::Notebook(notebook) => Some(&notebook),
79+
Self::Text(_) => None,
80+
}
81+
}
82+
83+
pub(crate) fn as_text(&self) -> Option<&TextDocument> {
84+
match self {
85+
Self::Text(py) => Some(&py),
86+
Self::Notebook(_) => None,
87+
}
88+
}
89+
90+
pub(crate) fn as_notebook_mut(&mut self) -> Option<&mut NotebookDocument> {
91+
match self {
92+
Self::Notebook(ref mut notebook) => Some(notebook),
93+
Self::Text(_) => None,
94+
}
95+
}
96+
97+
pub(crate) fn as_text_mut(&mut self) -> Option<&mut TextDocument> {
98+
match self {
99+
Self::Notebook(_) => None,
100+
Self::Text(ref mut py) => Some(py),
101+
}
102+
}
103+
104+
pub(crate) fn as_notebook_cell(&self, uri: &lsp_types::Url) -> Option<&TextDocument> {
105+
self.as_notebook()?.cell_document_by_uri(uri)
106+
}
107+
108+
pub(crate) fn as_notebook_cell_mut(
109+
&mut self,
110+
uri: &lsp_types::Url,
111+
) -> Option<&mut TextDocument> {
112+
self.as_notebook_mut()?.cell_document_by_uri_mut(uri)
113+
}
114+
}
115+
116+
impl DocumentKey {
117+
pub(crate) fn from_url(url: &lsp_types::Url) -> Self {
118+
if url.scheme() != "file" {
119+
return Self::Cell(url.clone());
120+
}
121+
url.to_file_path()
122+
.map(Self::File)
123+
.unwrap_or_else(|_| Self::Cell(url.clone()))
124+
}
125+
126+
pub(crate) fn to_url(self) -> lsp_types::Url {
127+
match self {
128+
Self::Cell(url) => url,
129+
Self::File(path) => {
130+
lsp_types::Url::from_file_path(path).expect("path should convert to file URL")
131+
}
132+
}
133+
}
134+
}
135+
136+
impl std::fmt::Display for DocumentKey {
137+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
138+
match self {
139+
Self::Cell(url) => url.fmt(f),
140+
Self::File(path) => path.display().fmt(f),
141+
}
142+
}
143+
}
144+
32145
/// Tracks multi-document edits to eventually merge into a `WorkspaceEdit`.
33146
/// Compatible with clients that don't support `workspace.workspaceEdit.documentChanges`.
34147
#[derive(Debug)]
@@ -72,6 +185,26 @@ impl WorkspaceEditTracker {
72185
}
73186
}
74187

188+
pub(crate) fn set_fixes_for_document(
189+
&mut self,
190+
document: &DocumentQuery,
191+
fixes: Fixes,
192+
) -> crate::Result<()> {
193+
for (cell, edits) in fixes.into_iter() {
194+
let uri = if let Some(notebook) = document.as_notebook() {
195+
notebook
196+
.cell_uri_by_index(cell)
197+
.cloned()
198+
.ok_or_else(|| anyhow!("cell index {cell} does not exist"))?
199+
} else {
200+
document.key().clone().to_url()
201+
};
202+
203+
self.set_edits_for_document(uri, document.version(), edits)?;
204+
}
205+
Ok(())
206+
}
207+
75208
/// Sets the edits made to a specific document. This should only be called
76209
/// once for each document `uri`, and will fail if this is called for the same `uri`
77210
/// multiple times.

crates/ruff_server/src/edit/document.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ pub(crate) type DocumentVersion = i32;
1010
/// The state for an individual document in the server. Stays up-to-date
1111
/// with changes made by the user, including unsaved changes.
1212
#[derive(Debug, Clone)]
13-
pub struct Document {
13+
pub struct TextDocument {
1414
/// The string contents of the document.
1515
contents: String,
1616
/// A computed line index for the document. This should always reflect
@@ -22,7 +22,7 @@ pub struct Document {
2222
version: DocumentVersion,
2323
}
2424

25-
impl Document {
25+
impl TextDocument {
2626
pub fn new(contents: String, version: DocumentVersion) -> Self {
2727
let index = LineIndex::from_source_text(&contents);
2828
Self {

0 commit comments

Comments
 (0)