Skip to content

Commit 6b02c39

Browse files
authored
[red-knot] Incorporate recent ruff server improvements into red knot's LSP (#17044)
1 parent 78b0b5a commit 6b02c39

File tree

6 files changed

+285
-49
lines changed

6 files changed

+285
-49
lines changed

crates/red_knot_server/src/edit/notebook.rs

Lines changed: 126 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -136,17 +136,15 @@ impl NotebookDocument {
136136
// provide the actual contents of the cells, so we'll initialize them with empty
137137
// contents.
138138
for cell in structure.array.cells.into_iter().flatten().rev() {
139-
if let Some(text_document) = deleted_cells.remove(&cell.document) {
140-
let version = text_document.version();
141-
self.cells.push(NotebookCell::new(
142-
cell,
143-
text_document.into_contents(),
144-
version,
145-
));
146-
} else {
147-
self.cells
148-
.insert(start, NotebookCell::new(cell, String::new(), 0));
149-
}
139+
let (content, version) =
140+
if let Some(text_document) = deleted_cells.remove(&cell.document) {
141+
let version = text_document.version();
142+
(text_document.into_contents(), version)
143+
} else {
144+
(String::new(), 0)
145+
};
146+
self.cells
147+
.insert(start, NotebookCell::new(cell, content, version));
150148
}
151149

152150
// Third, register the new cells in the index and update existing ones that came
@@ -200,6 +198,11 @@ impl NotebookDocument {
200198
self.version
201199
}
202200

201+
/// Get the URI for a cell by its index within the cell array.
202+
pub(crate) fn cell_uri_by_index(&self, index: CellId) -> Option<&lsp_types::Url> {
203+
self.cells.get(index).map(|cell| &cell.url)
204+
}
205+
203206
/// Get the text document representing the contents of a cell by the cell URI.
204207
pub(crate) fn cell_document_by_uri(&self, uri: &lsp_types::Url) -> Option<&TextDocument> {
205208
self.cells
@@ -238,3 +241,115 @@ impl NotebookCell {
238241
}
239242
}
240243
}
244+
245+
#[cfg(test)]
246+
mod tests {
247+
use super::NotebookDocument;
248+
249+
enum TestCellContent {
250+
#[allow(dead_code)]
251+
Markup(String),
252+
Code(String),
253+
}
254+
255+
fn create_test_url(index: usize) -> lsp_types::Url {
256+
lsp_types::Url::parse(&format!("cell:/test.ipynb#{index}")).unwrap()
257+
}
258+
259+
fn create_test_notebook(test_cells: Vec<TestCellContent>) -> NotebookDocument {
260+
let mut cells = Vec::with_capacity(test_cells.len());
261+
let mut cell_documents = Vec::with_capacity(test_cells.len());
262+
263+
for (index, test_cell) in test_cells.into_iter().enumerate() {
264+
let url = create_test_url(index);
265+
match test_cell {
266+
TestCellContent::Markup(content) => {
267+
cells.push(lsp_types::NotebookCell {
268+
kind: lsp_types::NotebookCellKind::Markup,
269+
document: url.clone(),
270+
metadata: None,
271+
execution_summary: None,
272+
});
273+
cell_documents.push(lsp_types::TextDocumentItem {
274+
uri: url,
275+
language_id: "markdown".to_owned(),
276+
version: 0,
277+
text: content,
278+
});
279+
}
280+
TestCellContent::Code(content) => {
281+
cells.push(lsp_types::NotebookCell {
282+
kind: lsp_types::NotebookCellKind::Code,
283+
document: url.clone(),
284+
metadata: None,
285+
execution_summary: None,
286+
});
287+
cell_documents.push(lsp_types::TextDocumentItem {
288+
uri: url,
289+
language_id: "python".to_owned(),
290+
version: 0,
291+
text: content,
292+
});
293+
}
294+
}
295+
}
296+
297+
NotebookDocument::new(0, cells, serde_json::Map::default(), cell_documents).unwrap()
298+
}
299+
300+
/// This test case checks that for a notebook with three code cells, when the client sends a
301+
/// change request to swap the first two cells, the notebook document is updated correctly.
302+
///
303+
/// The swap operation as a change request is represented as deleting the first two cells and
304+
/// adding them back in the reverse order.
305+
#[test]
306+
fn swap_cells() {
307+
let mut notebook = create_test_notebook(vec![
308+
TestCellContent::Code("cell = 0".to_owned()),
309+
TestCellContent::Code("cell = 1".to_owned()),
310+
TestCellContent::Code("cell = 2".to_owned()),
311+
]);
312+
313+
notebook
314+
.update(
315+
Some(lsp_types::NotebookDocumentCellChange {
316+
structure: Some(lsp_types::NotebookDocumentCellChangeStructure {
317+
array: lsp_types::NotebookCellArrayChange {
318+
start: 0,
319+
delete_count: 2,
320+
cells: Some(vec![
321+
lsp_types::NotebookCell {
322+
kind: lsp_types::NotebookCellKind::Code,
323+
document: create_test_url(1),
324+
metadata: None,
325+
execution_summary: None,
326+
},
327+
lsp_types::NotebookCell {
328+
kind: lsp_types::NotebookCellKind::Code,
329+
document: create_test_url(0),
330+
metadata: None,
331+
execution_summary: None,
332+
},
333+
]),
334+
},
335+
did_open: None,
336+
did_close: None,
337+
}),
338+
data: None,
339+
text_content: None,
340+
}),
341+
None,
342+
1,
343+
crate::PositionEncoding::default(),
344+
)
345+
.unwrap();
346+
347+
assert_eq!(
348+
notebook.make_ruff_notebook().source_code(),
349+
"cell = 1
350+
cell = 0
351+
cell = 2
352+
"
353+
);
354+
}
355+
}

crates/red_knot_server/src/edit/text_document.rs

Lines changed: 102 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,23 @@ pub struct TextDocument {
2020
/// The latest version of the document, set by the LSP client. The server will panic in
2121
/// debug mode if we attempt to update the document with an 'older' version.
2222
version: DocumentVersion,
23+
/// The language ID of the document as provided by the client.
24+
language_id: Option<LanguageId>,
25+
}
26+
27+
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
28+
pub enum LanguageId {
29+
Python,
30+
Other,
31+
}
32+
33+
impl From<&str> for LanguageId {
34+
fn from(language_id: &str) -> Self {
35+
match language_id {
36+
"python" => Self::Python,
37+
_ => Self::Other,
38+
}
39+
}
2340
}
2441

2542
impl TextDocument {
@@ -29,9 +46,16 @@ impl TextDocument {
2946
contents,
3047
index,
3148
version,
49+
language_id: None,
3250
}
3351
}
3452

53+
#[must_use]
54+
pub fn with_language_id(mut self, language_id: &str) -> Self {
55+
self.language_id = Some(LanguageId::from(language_id));
56+
self
57+
}
58+
3559
pub fn into_contents(self) -> String {
3660
self.contents
3761
}
@@ -48,6 +72,10 @@ impl TextDocument {
4872
self.version
4973
}
5074

75+
pub fn language_id(&self) -> Option<LanguageId> {
76+
self.language_id
77+
}
78+
5179
pub fn apply_changes(
5280
&mut self,
5381
changes: Vec<lsp_types::TextDocumentContentChangeEvent>,
@@ -66,7 +94,6 @@ impl TextDocument {
6694
return;
6795
}
6896

69-
let old_contents = self.contents().to_string();
7097
let mut new_contents = self.contents().to_string();
7198
let mut active_index = self.index().clone();
7299

@@ -87,15 +114,11 @@ impl TextDocument {
87114
new_contents = change;
88115
}
89116

90-
if new_contents != old_contents {
91-
active_index = LineIndex::from_source_text(&new_contents);
92-
}
117+
active_index = LineIndex::from_source_text(&new_contents);
93118
}
94119

95120
self.modify_with_manual_index(|contents, version, index| {
96-
if contents != &new_contents {
97-
*index = active_index;
98-
}
121+
*index = active_index;
99122
*contents = new_contents;
100123
*version = new_version;
101124
});
@@ -125,3 +148,75 @@ impl TextDocument {
125148
debug_assert!(self.version >= old_version);
126149
}
127150
}
151+
152+
#[cfg(test)]
153+
mod tests {
154+
use crate::{PositionEncoding, TextDocument};
155+
use lsp_types::{Position, TextDocumentContentChangeEvent};
156+
157+
#[test]
158+
fn redo_edit() {
159+
let mut document = TextDocument::new(
160+
r#""""
161+
测试comment
162+
一些测试内容
163+
"""
164+
import click
165+
166+
167+
@click.group()
168+
def interface():
169+
pas
170+
"#
171+
.to_string(),
172+
0,
173+
);
174+
175+
// Add an `s`, remove it again (back to the original code), and then re-add the `s`
176+
document.apply_changes(
177+
vec![
178+
TextDocumentContentChangeEvent {
179+
range: Some(lsp_types::Range::new(
180+
Position::new(9, 7),
181+
Position::new(9, 7),
182+
)),
183+
range_length: Some(0),
184+
text: "s".to_string(),
185+
},
186+
TextDocumentContentChangeEvent {
187+
range: Some(lsp_types::Range::new(
188+
Position::new(9, 7),
189+
Position::new(9, 8),
190+
)),
191+
range_length: Some(1),
192+
text: String::new(),
193+
},
194+
TextDocumentContentChangeEvent {
195+
range: Some(lsp_types::Range::new(
196+
Position::new(9, 7),
197+
Position::new(9, 7),
198+
)),
199+
range_length: Some(0),
200+
text: "s".to_string(),
201+
},
202+
],
203+
1,
204+
PositionEncoding::UTF16,
205+
);
206+
207+
assert_eq!(
208+
&document.contents,
209+
r#""""
210+
测试comment
211+
一些测试内容
212+
"""
213+
import click
214+
215+
216+
@click.group()
217+
def interface():
218+
pass
219+
"#
220+
);
221+
}
222+
}

crates/red_knot_server/src/server/api.rs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
use lsp_server as server;
2-
31
use crate::server::schedule::Task;
42
use crate::session::Session;
53
use crate::system::{url_to_any_system_path, AnySystemPath};
4+
use lsp_server as server;
5+
use lsp_types::notification::Notification;
66

77
mod diagnostics;
88
mod notifications;
@@ -52,6 +52,11 @@ pub(super) fn notification<'a>(notif: server::Notification) -> Task<'a> {
5252
notification::DidCloseNotebookHandler::METHOD => {
5353
local_notification_task::<notification::DidCloseNotebookHandler>(notif)
5454
}
55+
lsp_types::notification::SetTrace::METHOD => {
56+
tracing::trace!("Ignoring `setTrace` notification");
57+
return Task::nothing();
58+
},
59+
5560
method => {
5661
tracing::warn!("Received notification {method} which does not have a handler.");
5762
return Task::nothing();
@@ -69,6 +74,7 @@ fn _local_request_task<'a, R: traits::SyncRequestHandler>(
6974
) -> super::Result<Task<'a>> {
7075
let (id, params) = cast_request::<R>(req)?;
7176
Ok(Task::local(|session, notifier, requester, responder| {
77+
let _span = tracing::trace_span!("request", %id, method = R::METHOD).entered();
7278
let result = R::run(session, notifier, requester, params);
7379
respond::<R>(id, result, &responder);
7480
}))
@@ -98,6 +104,7 @@ fn background_request_task<'a, R: traits::BackgroundDocumentRequestHandler>(
98104
};
99105

100106
Box::new(move |notifier, responder| {
107+
let _span = tracing::trace_span!("request", %id, method = R::METHOD).entered();
101108
let result = R::run_with_snapshot(snapshot, db, notifier, params);
102109
respond::<R>(id, result, &responder);
103110
})
@@ -109,6 +116,7 @@ fn local_notification_task<'a, N: traits::SyncNotificationHandler>(
109116
) -> super::Result<Task<'a>> {
110117
let (id, params) = cast_notification::<N>(notif)?;
111118
Ok(Task::local(move |session, notifier, requester, _| {
119+
let _span = tracing::trace_span!("notification", method = N::METHOD).entered();
112120
if let Err(err) = N::run(session, notifier, requester, params) {
113121
tracing::error!("An error occurred while running {id}: {err}");
114122
show_err_msg!("Ruff encountered a problem. Check the logs for more details.");
@@ -128,6 +136,7 @@ fn background_notification_thread<'a, N: traits::BackgroundDocumentNotificationH
128136
return Box::new(|_, _| {});
129137
};
130138
Box::new(move |notifier, _| {
139+
let _span = tracing::trace_span!("notification", method = N::METHOD).entered();
131140
if let Err(err) = N::run_with_snapshot(snapshot, notifier, params) {
132141
tracing::error!("An error occurred while running {id}: {err}");
133142
show_err_msg!("Ruff encountered a problem. Check the logs for more details.");
@@ -174,7 +183,7 @@ fn respond<Req>(
174183
Req: traits::RequestHandler,
175184
{
176185
if let Err(err) = &result {
177-
tracing::error!("An error occurred with result ID {id}: {err}");
186+
tracing::error!("An error occurred with request ID {id}: {err}");
178187
show_err_msg!("Ruff encountered a problem. Check the logs for more details.");
179188
}
180189
if let Err(err) = responder.respond(id, result) {

0 commit comments

Comments
 (0)