mirror of
https://github.com/erg-lang/erg.git
synced 2025-09-28 04:09:05 +00:00
442 lines
16 KiB
Rust
442 lines
16 KiB
Rust
use std::path::Path;
|
|
|
|
use erg_common::spawn::safe_yield;
|
|
use lsp_types::{
|
|
CompletionResponse, DiagnosticSeverity, DocumentSymbolResponse, FoldingRange, FoldingRangeKind,
|
|
GotoDefinitionResponse, HoverContents, InlayHintLabel, MarkedString,
|
|
};
|
|
const FILE_A: &str = "tests/a.er";
|
|
const FILE_B: &str = "tests/b.er";
|
|
const FILE_C: &str = "tests/c.er";
|
|
const FILE_IMPORTS: &str = "tests/imports.er";
|
|
const FILE_INVALID_SYNTAX: &str = "tests/invalid_syntax.er";
|
|
const FILE_RETRIGGER: &str = "tests/retrigger.er";
|
|
const FILE_TOLERANT_COMPLETION: &str = "tests/tolerant_completion.er";
|
|
|
|
use els::{NormalizedUrl, Server};
|
|
use erg_proc_macros::exec_new_thread;
|
|
use molc::{add_char, delete_line, oneline_range};
|
|
|
|
#[test]
|
|
fn test_open() -> Result<(), Box<dyn std::error::Error>> {
|
|
let mut client = Server::bind_fake_client();
|
|
client.request_initialize()?;
|
|
client.notify_initialized()?;
|
|
client.wait_messages(3)?;
|
|
client.notify_open(FILE_A)?;
|
|
// log, work-done, etc.
|
|
client.wait_messages(6)?;
|
|
assert!(client.responses.iter().any(|val| val
|
|
.to_string()
|
|
.contains("tests/a.er passed, found warns: 0")));
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_completion() -> Result<(), Box<dyn std::error::Error>> {
|
|
let mut client = Server::bind_fake_client();
|
|
client.request_initialize()?;
|
|
client.notify_initialized()?;
|
|
let uri_a = NormalizedUrl::from_file_path(Path::new(FILE_A).canonicalize()?)?;
|
|
let uri_b = NormalizedUrl::from_file_path(Path::new(FILE_B).canonicalize()?)?;
|
|
client.notify_open(FILE_A)?;
|
|
client.notify_change(uri_a.clone().raw(), add_char(2, 0, "x"))?;
|
|
client.notify_change(uri_a.clone().raw(), add_char(2, 1, "."))?;
|
|
let resp = client.request_completion(uri_a.raw(), 2, 2, ".")?;
|
|
if let Some(CompletionResponse::Array(items)) = resp {
|
|
assert!(items.len() >= 40);
|
|
assert!(items.iter().any(|item| item.label == "abs"));
|
|
} else {
|
|
return Err(format!("{}: not items: {resp:?}", line!()).into());
|
|
}
|
|
client.notify_open(FILE_B)?;
|
|
client.notify_change(uri_b.clone().raw(), add_char(6, 20, "."))?;
|
|
let resp = client.request_completion(uri_b.raw(), 6, 21, ".")?;
|
|
if let Some(CompletionResponse::Array(items)) = resp {
|
|
assert!(items.iter().any(|item| item.label == "a"));
|
|
} else {
|
|
return Err(format!("{}: not items: {resp:?}", line!()).into());
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_completion_item_resolve() -> Result<(), Box<dyn std::error::Error>> {
|
|
let mut client = Server::bind_fake_client();
|
|
client.request_initialize()?;
|
|
client.notify_initialized()?;
|
|
let uri_a = NormalizedUrl::from_file_path(Path::new(FILE_A).canonicalize()?)?;
|
|
client.notify_open(FILE_A)?;
|
|
client.notify_change(uri_a.clone().raw(), add_char(2, 0, "x"))?;
|
|
client.notify_change(uri_a.clone().raw(), add_char(2, 1, "."))?;
|
|
let resp = client.request_completion(uri_a.clone().raw(), 2, 2, ".")?;
|
|
if let Some(CompletionResponse::Array(items)) = resp {
|
|
assert!(items.len() >= 40);
|
|
assert!(items.iter().any(|item| item.label == "abs"));
|
|
let Some(item) = items.into_iter().find(|item| item.label == "bit_count") else {
|
|
return Err("`bit_count` method not found".into());
|
|
};
|
|
assert!(item.documentation.is_none());
|
|
let resolved = client.request_completion_item_resolve(item)?;
|
|
assert_eq!(resolved.label, "bit_count");
|
|
assert!(resolved.documentation.is_some());
|
|
} else {
|
|
return Err(format!("not items: {resp:?}").into());
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_neighbor_completion() -> Result<(), Box<dyn std::error::Error>> {
|
|
let mut client = Server::bind_fake_client();
|
|
client.request_initialize()?;
|
|
client.notify_initialized()?;
|
|
let uri = NormalizedUrl::from_file_path(Path::new(FILE_A).canonicalize()?)?;
|
|
client.notify_open(FILE_A)?;
|
|
client.notify_open(FILE_B)?;
|
|
let resp = client.request_completion(uri.raw(), 2, 0, "n")?;
|
|
if let Some(CompletionResponse::Array(items)) = resp {
|
|
assert!(items.len() >= 40);
|
|
assert!(items
|
|
.iter()
|
|
.any(|item| item.label == "neighbor (import from b)"));
|
|
Ok(())
|
|
} else {
|
|
Err(format!("not items: {resp:?}").into())
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_pymodule_completion() -> Result<(), Box<dyn std::error::Error>> {
|
|
let mut client = Server::bind_fake_client();
|
|
client.request_initialize()?;
|
|
client.notify_initialized()?;
|
|
let uri = NormalizedUrl::from_file_path(Path::new(FILE_A).canonicalize()?)?;
|
|
client.notify_open(FILE_A)?;
|
|
while !client.server.flags.builtin_modules_loaded() {
|
|
safe_yield();
|
|
}
|
|
client.notify_change(uri.clone().raw(), add_char(2, 0, "c"))?;
|
|
let resp = client.request_completion(uri.raw(), 2, 0, "c")?;
|
|
if let Some(CompletionResponse::Array(items)) = resp {
|
|
assert!(items.len() >= 100);
|
|
assert!(items
|
|
.iter()
|
|
.any(|item| item.label == "cos (import from math)"));
|
|
Ok(())
|
|
} else {
|
|
Err(format!("not items: {resp:?}").into())
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_completion_retrigger() -> Result<(), Box<dyn std::error::Error>> {
|
|
let mut client = Server::bind_fake_client();
|
|
client.request_initialize()?;
|
|
client.notify_initialized()?;
|
|
let uri = NormalizedUrl::from_file_path(Path::new(FILE_RETRIGGER).canonicalize()?)?;
|
|
client.notify_open(FILE_RETRIGGER)?;
|
|
let _ = client.wait_diagnostics()?;
|
|
client.notify_change(uri.clone().raw(), add_char(2, 7, "n"))?;
|
|
let resp = client.request_completion(uri.clone().raw(), 2, 7, "n")?;
|
|
if let Some(CompletionResponse::Array(items)) = resp {
|
|
assert!(!items.is_empty());
|
|
assert!(items.iter().any(|item| item.label == "print!"));
|
|
} else {
|
|
return Err(format!("{}: not items: {resp:?}", line!()).into());
|
|
}
|
|
client.notify_change(uri.clone().raw(), add_char(3, 15, "t"))?;
|
|
let resp = client.request_completion(uri.raw(), 3, 15, "t")?;
|
|
if let Some(CompletionResponse::Array(items)) = resp {
|
|
assert!(!items.is_empty());
|
|
assert!(items.iter().any(|item| item.label == "bit_count"));
|
|
assert!(items.iter().any(|item| item.label == "bit_length"));
|
|
} else {
|
|
return Err(format!("{}: not items: {resp:?}", line!()).into());
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_tolerant_completion() -> Result<(), Box<dyn std::error::Error>> {
|
|
let mut client = Server::bind_fake_client();
|
|
client.request_initialize()?;
|
|
client.notify_initialized()?;
|
|
let uri = NormalizedUrl::from_file_path(Path::new(FILE_TOLERANT_COMPLETION).canonicalize()?)?;
|
|
client.notify_open(FILE_TOLERANT_COMPLETION)?;
|
|
let _ = client.wait_diagnostics()?;
|
|
client.notify_change(uri.clone().raw(), add_char(5, 16, "."))?;
|
|
let resp = client.request_completion(uri.clone().raw(), 5, 17, ".")?;
|
|
if let Some(CompletionResponse::Array(items)) = resp {
|
|
assert!(items.len() >= 40);
|
|
assert!(items.iter().any(|item| item.label == "capitalize"));
|
|
} else {
|
|
return Err(format!("{}: not items: {resp:?}", line!()).into());
|
|
}
|
|
client.notify_change(uri.clone().raw(), add_char(5, 14, "."))?;
|
|
let resp = client.request_completion(uri.clone().raw(), 5, 15, ".")?;
|
|
if let Some(CompletionResponse::Array(items)) = resp {
|
|
assert!(items.len() >= 40);
|
|
assert!(items.iter().any(|item| item.label == "abs"));
|
|
} else {
|
|
return Err(format!("{}: not items: {resp:?}", line!()).into());
|
|
}
|
|
client.notify_change(uri.clone().raw(), add_char(2, 9, "."))?;
|
|
let resp = client.request_completion(uri.raw(), 2, 10, ".")?;
|
|
if let Some(CompletionResponse::Array(items)) = resp {
|
|
assert!(items.len() >= 10);
|
|
assert!(items.iter().any(|item| item.label == "pi"));
|
|
Ok(())
|
|
} else {
|
|
Err(format!("{}: not items: {resp:?}", line!()).into())
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
#[exec_new_thread]
|
|
fn test_rename() -> Result<(), Box<dyn std::error::Error>> {
|
|
let mut client = Server::bind_fake_client();
|
|
client.request_initialize()?;
|
|
client.notify_initialized()?;
|
|
let uri = NormalizedUrl::from_file_path(Path::new(FILE_A).canonicalize()?)?;
|
|
client.notify_open(FILE_A)?;
|
|
let edit = client
|
|
.request_rename(uri.clone().raw(), 1, 5, "y")?
|
|
.unwrap();
|
|
assert!(edit
|
|
.changes
|
|
.is_some_and(|changes| changes.values().next().unwrap().len() == 2));
|
|
client.notify_open(FILE_C)?;
|
|
client.notify_open(FILE_B)?;
|
|
let uri_b = NormalizedUrl::from_file_path(Path::new(FILE_B).canonicalize()?)?;
|
|
// let uri_c = NormalizedUrl::from_file_path(Path::new(FILE_C).canonicalize()?)?;
|
|
let edit = client
|
|
.request_rename(uri_b.clone().raw(), 2, 1, "y")?
|
|
.unwrap();
|
|
assert_eq!(edit.changes.as_ref().unwrap().iter().count(), 2);
|
|
for (_, change) in edit.changes.unwrap() {
|
|
assert_eq!(change.len(), 1);
|
|
}
|
|
client.notify_save(uri_b.clone().raw())?;
|
|
client.wait_diagnostics()?;
|
|
let edit = client
|
|
.request_rename(uri_b.clone().raw(), 4, 14, "b")?
|
|
.unwrap();
|
|
assert_eq!(edit.changes.as_ref().unwrap().iter().count(), 2);
|
|
for (uri, change) in edit.changes.unwrap() {
|
|
if uri.as_str().ends_with("b.er") {
|
|
assert_eq!(change.len(), 2);
|
|
} else {
|
|
assert_eq!(change.len(), 1); // c.er
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_signature_help() -> Result<(), Box<dyn std::error::Error>> {
|
|
let mut client = Server::bind_fake_client();
|
|
client.request_initialize()?;
|
|
client.notify_initialized()?;
|
|
let uri = NormalizedUrl::from_file_path(Path::new(FILE_A).canonicalize()?)?;
|
|
client.notify_open(FILE_A)?;
|
|
client.notify_change(uri.clone().raw(), add_char(2, 0, "assert"))?;
|
|
client.notify_change(uri.clone().raw(), add_char(2, 6, "("))?;
|
|
let help = client
|
|
.request_signature_help(uri.raw(), 2, 7, "(")?
|
|
.unwrap();
|
|
assert_eq!(help.signatures.len(), 1);
|
|
let sig = &help.signatures[0];
|
|
assert_eq!(sig.label, "::assert: (test: Bool, msg := Str) -> NoneType");
|
|
assert_eq!(sig.active_parameter, Some(0));
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_hover() -> Result<(), Box<dyn std::error::Error>> {
|
|
let mut client = Server::bind_fake_client();
|
|
client.request_initialize()?;
|
|
client.notify_initialized()?;
|
|
let uri = NormalizedUrl::from_file_path(Path::new(FILE_A).canonicalize()?)?;
|
|
client.notify_open(FILE_A)?;
|
|
let hover = client.request_hover(uri.raw(), 1, 4)?.unwrap();
|
|
let HoverContents::Array(contents) = hover.contents else {
|
|
todo!()
|
|
};
|
|
assert_eq!(contents.len(), 2);
|
|
let MarkedString::LanguageString(content) = &contents[0] else {
|
|
todo!()
|
|
};
|
|
assert!(
|
|
content.value == "# tests/a.er, line 1\nx = 1"
|
|
|| content.value == "# tests\\a.er, line 1\nx = 1"
|
|
);
|
|
let MarkedString::LanguageString(content) = &contents[1] else {
|
|
todo!()
|
|
};
|
|
assert_eq!(content.value, "x: {1}");
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
#[exec_new_thread]
|
|
fn test_references() -> Result<(), Box<dyn std::error::Error>> {
|
|
let mut client = Server::bind_fake_client();
|
|
client.request_initialize()?;
|
|
client.notify_initialized()?;
|
|
let uri = NormalizedUrl::from_file_path(Path::new(FILE_A).canonicalize()?)?;
|
|
client.notify_open(FILE_A)?;
|
|
let locations = client.request_references(uri.raw(), 1, 4)?.unwrap();
|
|
assert_eq!(locations.len(), 1);
|
|
assert_eq!(&locations[0].range, &oneline_range(1, 4, 5));
|
|
client.notify_open(FILE_C)?;
|
|
client.notify_open(FILE_B)?;
|
|
let uri_b = NormalizedUrl::from_file_path(Path::new(FILE_B).canonicalize()?)?;
|
|
let uri_c = NormalizedUrl::from_file_path(Path::new(FILE_C).canonicalize()?)?;
|
|
let locations = client.request_references(uri_b.raw(), 0, 2)?.unwrap();
|
|
assert_eq!(locations.len(), 1);
|
|
assert_eq!(NormalizedUrl::new(locations[0].uri.clone()), uri_c);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_goto_definition() -> Result<(), Box<dyn std::error::Error>> {
|
|
let mut client = Server::bind_fake_client();
|
|
client.request_initialize()?;
|
|
client.notify_initialized()?;
|
|
let uri = NormalizedUrl::from_file_path(Path::new(FILE_A).canonicalize()?)?;
|
|
client.notify_open(FILE_A)?;
|
|
let Some(GotoDefinitionResponse::Scalar(location)) =
|
|
client.request_goto_definition(uri.raw(), 1, 4)?
|
|
else {
|
|
todo!()
|
|
};
|
|
assert_eq!(&location.range, &oneline_range(0, 0, 1));
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
#[exec_new_thread]
|
|
fn test_folding_range() -> Result<(), Box<dyn std::error::Error>> {
|
|
let mut client = Server::bind_fake_client();
|
|
client.request_initialize()?;
|
|
client.notify_initialized()?;
|
|
let uri = NormalizedUrl::from_file_path(Path::new(FILE_IMPORTS).canonicalize()?)?;
|
|
client.notify_open(FILE_IMPORTS)?;
|
|
let ranges = client.request_folding_range(uri.raw())?.unwrap();
|
|
assert_eq!(ranges.len(), 1);
|
|
assert_eq!(
|
|
&ranges[0],
|
|
&FoldingRange {
|
|
start_line: 0,
|
|
start_character: Some(0),
|
|
end_line: 3,
|
|
end_character: Some(22),
|
|
kind: Some(FoldingRangeKind::Imports),
|
|
}
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_document_symbol() -> Result<(), Box<dyn std::error::Error>> {
|
|
let mut client = Server::bind_fake_client();
|
|
client.request_initialize()?;
|
|
client.notify_initialized()?;
|
|
let uri = NormalizedUrl::from_file_path(Path::new(FILE_A).canonicalize()?)?;
|
|
client.notify_open(FILE_A)?;
|
|
let Some(DocumentSymbolResponse::Nested(symbols)) =
|
|
client.request_document_symbols(uri.raw())?
|
|
else {
|
|
todo!()
|
|
};
|
|
assert_eq!(symbols.len(), 2);
|
|
assert_eq!(&symbols[0].name, "x");
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_inlay_hint() -> Result<(), Box<dyn std::error::Error>> {
|
|
let mut client = Server::bind_fake_client();
|
|
client.request_initialize()?;
|
|
client.notify_initialized()?;
|
|
let uri = NormalizedUrl::from_file_path(Path::new(FILE_A).canonicalize()?)?;
|
|
client.notify_open(FILE_A)?;
|
|
let hints = client.request_inlay_hint(uri.raw())?.unwrap();
|
|
assert_eq!(hints.len(), 2);
|
|
let InlayHintLabel::String(label) = &hints[0].label else {
|
|
todo!()
|
|
};
|
|
assert_eq!(label, ": {1}");
|
|
let InlayHintLabel::String(label) = &hints[1].label else {
|
|
todo!()
|
|
};
|
|
assert_eq!(label, ": Nat");
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
#[exec_new_thread]
|
|
fn test_dependents_check() -> Result<(), Box<dyn std::error::Error>> {
|
|
let mut client = Server::bind_fake_client();
|
|
client.request_initialize()?;
|
|
client.notify_initialized()?;
|
|
client.wait_messages(3)?;
|
|
client.notify_open(FILE_B)?;
|
|
client.wait_messages(6)?;
|
|
client.notify_open(FILE_C)?;
|
|
client.wait_messages(6)?;
|
|
let uri_b = NormalizedUrl::from_file_path(Path::new(FILE_B).canonicalize()?)?;
|
|
// delete b.er:3, causing an error in c.er
|
|
client.notify_change(uri_b.clone().raw(), delete_line(2))?;
|
|
client.wait_messages(2)?;
|
|
client.responses.clear();
|
|
client.notify_save(uri_b.clone().raw())?;
|
|
let b_diags = client.wait_diagnostics()?;
|
|
assert!(b_diags.diagnostics.is_empty(), "{:?}", b_diags.diagnostics);
|
|
let c_diags = client.wait_diagnostics()?;
|
|
assert_eq!(
|
|
c_diags.diagnostics.len(),
|
|
1,
|
|
"{:?} / {:?}",
|
|
b_diags.diagnostics,
|
|
c_diags.diagnostics
|
|
);
|
|
assert_eq!(
|
|
c_diags.diagnostics[0].severity,
|
|
Some(DiagnosticSeverity::ERROR)
|
|
);
|
|
// insert invalid code to b.er:8, causing a syntax error in b.er but not c.er
|
|
client.notify_change(uri_b.clone().raw(), add_char(7, 0, "a.\n"))?;
|
|
client.wait_messages(1)?;
|
|
client.responses.clear();
|
|
client.notify_save(uri_b.clone().raw())?;
|
|
let b_diags = client.wait_diagnostics()?;
|
|
assert_eq!(b_diags.diagnostics.len(), 1, "{:?}", b_diags.diagnostics,);
|
|
let c_diags = client.wait_diagnostics()?;
|
|
assert!(c_diags
|
|
.diagnostics
|
|
.iter()
|
|
.all(|diag| !diag.message.contains("expected: Indent, got: EOF")));
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_fix_error() -> Result<(), Box<dyn std::error::Error>> {
|
|
let mut client = Server::bind_fake_client();
|
|
client.request_initialize()?;
|
|
client.notify_initialized()?;
|
|
client.notify_open(FILE_INVALID_SYNTAX)?;
|
|
let diags = client.wait_diagnostics()?;
|
|
assert_eq!(diags.diagnostics.len(), 1);
|
|
assert_eq!(
|
|
diags.diagnostics[0].severity,
|
|
Some(DiagnosticSeverity::ERROR)
|
|
);
|
|
let uri = NormalizedUrl::from_file_path(Path::new(FILE_INVALID_SYNTAX).canonicalize()?)?;
|
|
client.notify_change(uri.clone().raw(), add_char(0, 10, " 1"))?;
|
|
client.notify_save(uri.clone().raw())?;
|
|
let diags = client.wait_diagnostics()?;
|
|
assert_eq!(diags.diagnostics.len(), 0);
|
|
Ok(())
|
|
}
|