use insta::assert_json_snapshot; use lsp_types::{NotebookCellKind, Position, Range}; use crate::{TestServer, TestServerBuilder}; #[test] fn publish_diagnostics_open() -> anyhow::Result<()> { let mut server = TestServerBuilder::new()? .build()? .wait_until_workspaces_are_initialized()?; server.initialization_result().unwrap(); let mut builder = NotebookBuilder::virtual_file("test.ipynb"); builder.add_python_cell( r#"from typing import Literal type Style = Literal["italic", "bold", "underline"]"#, ); builder.add_python_cell( r#"def with_style(line: str, word, style: Style) -> str: if style == "italic": return line.replace(word, f"*{word}*") elif style == "bold": return line.replace(word, f"__{word}__") position = line.find(word) output = line + "\n" output += " " * position output += "-" * len(word) "#, ); builder.add_python_cell( r#"print(with_style("ty is a fast type checker for Python.", "fast", "underlined")) "#, ); builder.open(&mut server); let cell1_diagnostics = server.await_notification::()?; let cell2_diagnostics = server.await_notification::()?; let cell3_diagnostics = server.await_notification::()?; assert_json_snapshot!([cell1_diagnostics, cell2_diagnostics, cell3_diagnostics]); Ok(()) } #[test] fn diagnostic_end_of_file() -> anyhow::Result<()> { let mut server = TestServerBuilder::new()? .build()? .wait_until_workspaces_are_initialized()?; server.initialization_result().unwrap(); let mut builder = NotebookBuilder::virtual_file("test.ipynb"); builder.add_python_cell( r#"from typing import Literal type Style = Literal["italic", "bold", "underline"]"#, ); builder.add_python_cell( r#"def with_style(line: str, word, style: Style) -> str: if style == "italic": return line.replace(word, f"*{word}*") elif style == "bold": return line.replace(word, f"__{word}__") position = line.find(word) output = line + "\n" output += " " * position output += "-" * len(word) "#, ); let cell_3 = builder.add_python_cell( r#"with_style("test", "word", "underline") IOError"#, ); let notebook_url = builder.open(&mut server); server.collect_publish_diagnostic_notifications(3)?; server.send_notification::( lsp_types::DidChangeNotebookDocumentParams { notebook_document: lsp_types::VersionedNotebookDocumentIdentifier { version: 0, uri: notebook_url, }, change: lsp_types::NotebookDocumentChangeEvent { metadata: None, cells: Some(lsp_types::NotebookDocumentCellChange { structure: None, data: None, text_content: Some(vec![lsp_types::NotebookDocumentChangeTextContent { document: lsp_types::VersionedTextDocumentIdentifier { uri: cell_3, version: 0, }, changes: { vec![lsp_types::TextDocumentContentChangeEvent { range: Some(Range::new(Position::new(0, 16), Position::new(0, 17))), range_length: Some(1), text: String::new(), }] }, }]), }), }, }, ); let diagnostics = server.collect_publish_diagnostic_notifications(3)?; assert_json_snapshot!(diagnostics); Ok(()) } #[test] fn semantic_tokens() -> anyhow::Result<()> { let mut server = TestServerBuilder::new()? .build()? .wait_until_workspaces_are_initialized()?; server.initialization_result().unwrap(); let mut builder = NotebookBuilder::virtual_file("src/test.ipynb"); let first_cell = builder.add_python_cell( r#"from typing import Literal type Style = Literal["italic", "bold", "underline"]"#, ); let second_cell = builder.add_python_cell( r#"def with_style(line: str, word, style: Style) -> str: if style == "italic": return line.replace(word, f"*{word}*") elif style == "bold": return line.replace(word, f"__{word}__") position = line.find(word) output = line + "\n" output += " " * position output += "-" * len(word) "#, ); let third_cell = builder.add_python_cell( r#"print(with_style("ty is a fast type checker for Python.", "fast", "underlined")) "#, ); builder.open(&mut server); let cell1_tokens = semantic_tokens_full_for_cell(&mut server, &first_cell)?; let cell2_tokens = semantic_tokens_full_for_cell(&mut server, &second_cell)?; let cell3_tokens = semantic_tokens_full_for_cell(&mut server, &third_cell)?; assert_json_snapshot!([cell1_tokens, cell2_tokens, cell3_tokens]); server.collect_publish_diagnostic_notifications(3)?; Ok(()) } #[test] fn swap_cells() -> anyhow::Result<()> { let mut server = TestServerBuilder::new()? .build()? .wait_until_workspaces_are_initialized()?; server.initialization_result().unwrap(); let mut builder = NotebookBuilder::virtual_file("src/test.ipynb"); let first_cell = builder.add_python_cell( r#"b = a "#, ); let second_cell = builder.add_python_cell(r#"a = 10"#); builder.add_python_cell(r#"c = b"#); let notebook = builder.open(&mut server); let diagnostics = server.collect_publish_diagnostic_notifications(3)?; assert_json_snapshot!(diagnostics, @r###" { "vscode-notebook-cell://src/test.ipynb#0": [ { "range": { "start": { "line": 0, "character": 4 }, "end": { "line": 0, "character": 5 } }, "severity": 1, "code": "unresolved-reference", "codeDescription": { "href": "https://ty.dev/rules#unresolved-reference" }, "source": "ty", "message": "Name `a` used when not defined", "relatedInformation": [] } ], "vscode-notebook-cell://src/test.ipynb#1": [], "vscode-notebook-cell://src/test.ipynb#2": [] } "###); // Re-order the cells from `b`, `a`, `c` to `a`, `b`, `c` (swapping cell 1 and 2) server.send_notification::( lsp_types::DidChangeNotebookDocumentParams { notebook_document: lsp_types::VersionedNotebookDocumentIdentifier { version: 1, uri: notebook, }, change: lsp_types::NotebookDocumentChangeEvent { metadata: None, cells: Some(lsp_types::NotebookDocumentCellChange { structure: Some(lsp_types::NotebookDocumentCellChangeStructure { array: lsp_types::NotebookCellArrayChange { start: 0, delete_count: 2, cells: Some(vec![ lsp_types::NotebookCell { kind: NotebookCellKind::Code, document: second_cell, metadata: None, execution_summary: None, }, lsp_types::NotebookCell { kind: NotebookCellKind::Code, document: first_cell, metadata: None, execution_summary: None, }, ]), }, did_open: None, did_close: None, }), data: None, text_content: None, }), }, }, ); let diagnostics = server.collect_publish_diagnostic_notifications(3)?; assert_json_snapshot!(diagnostics, @r###" { "vscode-notebook-cell://src/test.ipynb#0": [], "vscode-notebook-cell://src/test.ipynb#1": [], "vscode-notebook-cell://src/test.ipynb#2": [] } "###); Ok(()) } fn semantic_tokens_full_for_cell( server: &mut TestServer, cell_uri: &lsp_types::Url, ) -> crate::Result> { let cell1_tokens_req_id = server.send_request::( lsp_types::SemanticTokensParams { work_done_progress_params: lsp_types::WorkDoneProgressParams::default(), partial_result_params: lsp_types::PartialResultParams::default(), text_document: lsp_types::TextDocumentIdentifier { uri: cell_uri.clone(), }, }, ); server.await_response::(&cell1_tokens_req_id) } #[derive(Debug)] pub(crate) struct NotebookBuilder { notebook_url: lsp_types::Url, // The cells: (cell_metadata, content, language_id) cells: Vec<(lsp_types::NotebookCell, String, String)>, } impl NotebookBuilder { pub(crate) fn virtual_file(name: &str) -> Self { let url: lsp_types::Url = format!("vs-code:/{name}").parse().unwrap(); Self { notebook_url: url, cells: Vec::new(), } } pub(crate) fn add_python_cell(&mut self, content: &str) -> lsp_types::Url { let index = self.cells.len(); let id = format!( "vscode-notebook-cell:/{}#{}", self.notebook_url.path(), index ); let url: lsp_types::Url = id.parse().unwrap(); self.cells.push(( lsp_types::NotebookCell { kind: NotebookCellKind::Code, document: url.clone(), metadata: None, execution_summary: None, }, content.to_string(), "python".to_string(), )); url } pub(crate) fn open(self, server: &mut TestServer) -> lsp_types::Url { server.send_notification::( lsp_types::DidOpenNotebookDocumentParams { notebook_document: lsp_types::NotebookDocument { uri: self.notebook_url.clone(), notebook_type: "jupyter-notebook".to_string(), version: 0, metadata: None, cells: self.cells.iter().map(|(cell, _, _)| cell.clone()).collect(), }, cell_text_documents: self .cells .iter() .map(|(cell, content, language_id)| lsp_types::TextDocumentItem { uri: cell.document.clone(), language_id: language_id.clone(), version: 0, text: content.clone(), }) .collect(), }, ); self.notebook_url } }