I think changes to this value are generally noise. It's hard to tell what it means and it isn't especially actionable. We already have an eval running in CI for completion ranking, so I don't think it's terribly important to care about ranking here in e2e tests _generally_.
552 lines
17 KiB
Rust
552 lines
17 KiB
Rust
use insta::assert_json_snapshot;
|
|
use lsp_types::{CompletionResponse, CompletionTriggerKind, NotebookCellKind, Position, Range};
|
|
use ruff_db::system::SystemPath;
|
|
use ty_server::ClientOptions;
|
|
|
|
use crate::{TestServer, TestServerBuilder};
|
|
|
|
static FILTERS: &[(&str, &str)] = &[(r#""sortText": "[0-9 ]+""#, r#""sortText": "[RANKING]""#)];
|
|
|
|
#[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::<lsp_types::notification::PublishDiagnostics>();
|
|
let cell2_diagnostics =
|
|
server.await_notification::<lsp_types::notification::PublishDiagnostics>();
|
|
let cell3_diagnostics =
|
|
server.await_notification::<lsp_types::notification::PublishDiagnostics>();
|
|
|
|
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::notification::DidChangeNotebookDocument>(
|
|
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::notification::DidChangeNotebookDocument>(
|
|
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(())
|
|
}
|
|
|
|
#[test]
|
|
fn auto_import() -> anyhow::Result<()> {
|
|
let mut server = TestServerBuilder::new()?
|
|
.with_workspace(
|
|
SystemPath::new("src"),
|
|
Some(ClientOptions::default().with_experimental_auto_import(true)),
|
|
)?
|
|
.build()
|
|
.wait_until_workspaces_are_initialized();
|
|
|
|
server.initialization_result().unwrap();
|
|
|
|
let mut builder = NotebookBuilder::virtual_file("src/test.ipynb");
|
|
|
|
builder.add_python_cell(
|
|
r#"from typing import TYPE_CHECKING
|
|
"#,
|
|
);
|
|
|
|
let second_cell = builder.add_python_cell(
|
|
r#"# leading comment
|
|
b: Litera
|
|
"#,
|
|
);
|
|
|
|
builder.open(&mut server);
|
|
|
|
server.collect_publish_diagnostic_notifications(2);
|
|
|
|
let completions = literal_completions(&mut server, &second_cell, Position::new(1, 9));
|
|
|
|
insta::with_settings!({
|
|
filters => FILTERS.iter().copied(),
|
|
}, {
|
|
assert_json_snapshot!(completions);
|
|
});
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn auto_import_same_cell() -> anyhow::Result<()> {
|
|
let mut server = TestServerBuilder::new()?
|
|
.with_workspace(
|
|
SystemPath::new("src"),
|
|
Some(ClientOptions::default().with_experimental_auto_import(true)),
|
|
)?
|
|
.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 TYPE_CHECKING
|
|
b: Litera
|
|
"#,
|
|
);
|
|
|
|
builder.open(&mut server);
|
|
|
|
server.collect_publish_diagnostic_notifications(1);
|
|
|
|
let completions = literal_completions(&mut server, &first_cell, Position::new(1, 9));
|
|
|
|
insta::with_settings!({
|
|
filters => FILTERS.iter().copied(),
|
|
}, {
|
|
assert_json_snapshot!(completions);
|
|
});
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn auto_import_from_future() -> anyhow::Result<()> {
|
|
let mut server = TestServerBuilder::new()?
|
|
.with_workspace(
|
|
SystemPath::new("src"),
|
|
Some(ClientOptions::default().with_experimental_auto_import(true)),
|
|
)?
|
|
.build()
|
|
.wait_until_workspaces_are_initialized();
|
|
|
|
server.initialization_result().unwrap();
|
|
|
|
let mut builder = NotebookBuilder::virtual_file("src/test.ipynb");
|
|
|
|
builder.add_python_cell(r#"from typing import TYPE_CHECKING"#);
|
|
|
|
let second_cell = builder.add_python_cell(
|
|
r#"from __future__ import annotations
|
|
b: Litera
|
|
"#,
|
|
);
|
|
|
|
builder.open(&mut server);
|
|
|
|
server.collect_publish_diagnostic_notifications(2);
|
|
|
|
let completions = literal_completions(&mut server, &second_cell, Position::new(1, 9));
|
|
|
|
insta::with_settings!({
|
|
filters => FILTERS.iter().copied(),
|
|
}, {
|
|
assert_json_snapshot!(completions);
|
|
});
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn auto_import_docstring() -> anyhow::Result<()> {
|
|
let mut server = TestServerBuilder::new()?
|
|
.with_workspace(
|
|
SystemPath::new("src"),
|
|
Some(ClientOptions::default().with_experimental_auto_import(true)),
|
|
)?
|
|
.build()
|
|
.wait_until_workspaces_are_initialized();
|
|
|
|
server.initialization_result().unwrap();
|
|
|
|
let mut builder = NotebookBuilder::virtual_file("src/test.ipynb");
|
|
|
|
builder.add_python_cell(
|
|
r#"from typing import TYPE_CHECKING
|
|
"#,
|
|
);
|
|
|
|
let second_cell = builder.add_python_cell(
|
|
r#""""A cell level docstring"""
|
|
b: Litera
|
|
"#,
|
|
);
|
|
|
|
builder.open(&mut server);
|
|
|
|
server.collect_publish_diagnostic_notifications(2);
|
|
|
|
let completions = literal_completions(&mut server, &second_cell, Position::new(1, 9));
|
|
|
|
insta::with_settings!({
|
|
filters => FILTERS.iter().copied(),
|
|
}, {
|
|
assert_json_snapshot!(completions);
|
|
});
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn semantic_tokens_full_for_cell(
|
|
server: &mut TestServer,
|
|
cell_uri: &lsp_types::Url,
|
|
) -> Option<lsp_types::SemanticTokensResult> {
|
|
let cell1_tokens_req_id = server.send_request::<lsp_types::request::SemanticTokensFullRequest>(
|
|
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::<lsp_types::request::SemanticTokensFullRequest>(&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::notification::DidOpenNotebookDocument>(
|
|
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
|
|
}
|
|
}
|
|
|
|
fn literal_completions(
|
|
server: &mut TestServer,
|
|
cell: &lsp_types::Url,
|
|
position: Position,
|
|
) -> Vec<lsp_types::CompletionItem> {
|
|
let completions_id =
|
|
server.send_request::<lsp_types::request::Completion>(lsp_types::CompletionParams {
|
|
text_document_position: lsp_types::TextDocumentPositionParams {
|
|
text_document: lsp_types::TextDocumentIdentifier { uri: cell.clone() },
|
|
position,
|
|
},
|
|
work_done_progress_params: lsp_types::WorkDoneProgressParams::default(),
|
|
partial_result_params: lsp_types::PartialResultParams::default(),
|
|
context: Some(lsp_types::CompletionContext {
|
|
trigger_kind: CompletionTriggerKind::TRIGGER_FOR_INCOMPLETE_COMPLETIONS,
|
|
trigger_character: None,
|
|
}),
|
|
});
|
|
|
|
// There are a ton of imports we don't care about in here...
|
|
// The import bit is that an edit is always restricted to the current cell. That means,
|
|
// we can't add `Literal` to the `from typing import TYPE_CHECKING` import in cell 1
|
|
let completions = server.await_response::<lsp_types::request::Completion>(&completions_id);
|
|
let mut items = match completions {
|
|
Some(CompletionResponse::Array(array)) => array,
|
|
Some(CompletionResponse::List(lsp_types::CompletionList { items, .. })) => items,
|
|
None => return vec![],
|
|
};
|
|
|
|
items.retain(|item| item.label.starts_with("Litera"));
|
|
|
|
items
|
|
}
|