mod lsp_client;
use lsp_client::{
app_and_library, file_uri, fresh_dir, lock_and_cache, project, registry_app, same_uri, Client,
APP_MAIN,
};
use serde_json::{json, Value};
fn open(client: &mut Client, uri: &str, text: &str) {
client.notify(
"textDocument/didOpen",
json!({ "textDocument": { "uri": uri, "languageId": "nova", "version": 1, "text": text } }),
);
}
fn position(text: &str, byte: usize) -> Value {
let line = text[..byte].matches('\n').count();
let line_start = text[..byte].rfind('\n').map_or(0, |i| i + 1);
json!({ "line": line, "character": text[line_start..byte].encode_utf16().count() })
}
fn at(text: &str, marker: &str, n: usize) -> Value {
let start = text
.match_indices(marker)
.nth(n)
.unwrap_or_else(|| panic!("no `{marker}` number {n}"))
.0;
position(text, start)
}
fn range(text: &str, marker: &str, n: usize, word: &str) -> Value {
let start = text.match_indices(marker).nth(n).unwrap().0;
json!({ "start": position(text, start), "end": position(text, start + word.len()) })
}
fn hover(client: &mut Client, uri: &str, position: Value) -> Value {
client.request(
"textDocument/hover",
json!({ "textDocument": { "uri": uri }, "position": position }),
)
}
#[track_caller]
fn ok(response: &Value) -> Value {
assert!(response["error"].is_null(), "an error: {response}");
response["result"].clone()
}
#[track_caller]
fn markdown(response: &Value) -> String {
response["result"]["contents"]["value"]
.as_str()
.unwrap_or_else(|| panic!("no hover: {response}"))
.to_string()
}
#[track_caller]
fn code(response: &Value) -> String {
let md = markdown(response);
md.strip_prefix("```nova\n")
.and_then(|rest| rest.split("\n```").next())
.unwrap_or_else(|| panic!("no code block: {md}"))
.to_string()
}
const AREA: &str = "/// The area of a square.\n///\n/// Sides are whole numbers.\n\
pub fn area(side: Int) -> Int { side * side }\n\n\
fn main() {\n let total = area(3)\n let mut n = 0\n n = total\n}\n";
#[test]
fn hover_shows_a_functions_signature_and_its_docs() {
let dir = project("hover-docs", &[("main.nova", AREA)]);
let uri = file_uri(&dir.join("src").join("main.nova"));
let mut client = Client::start(&dir, false);
open(&mut client, &uri, AREA);
let response = hover(&mut client, &uri, at(AREA, "area(3)", 0));
assert_eq!(
markdown(&response),
"```nova\npub fn area(side: Int) -> Int\n```\n\nThe area of a square.\n\nSides are whole numbers."
);
assert_eq!(
response["result"]["range"],
range(AREA, "area(3)", 0, "area")
);
}
#[test]
fn hover_shows_a_locals_inferred_type() {
let dir = project("hover-local", &[("main.nova", AREA)]);
let uri = file_uri(&dir.join("src").join("main.nova"));
let mut client = Client::start(&dir, false);
open(&mut client, &uri, AREA);
assert_eq!(
code(&hover(&mut client, &uri, at(AREA, "total", 0))),
"let total: Int"
);
assert_eq!(
code(&hover(&mut client, &uri, at(AREA, "n = total", 0))),
"let mut n: Int"
);
}
const KINDS: &str = "/// A point.\nrecord Point { x: Int }\n\
type Shape = | Round(Int) | Flat\n\
trait Show {\n type Out\n fn show(self) -> String\n}\n\
impl Show for Point {\n type Out = Int\n fn show(self) -> String { \"p\" }\n}\n\
pub const LIMIT: Int = 3\n\
fn keep<T: Show>(t: T) -> T {\n let kept = t\n kept\n}\n\
fn main() {\n let p = Point { x: 1 }\n let s = p.show()\n let r = Round(LIMIT)\n let xs = [1]\n let n = xs.len()\n print(\"x\")\n let mut v = Vec::new()\n v.push(1)\n}\n";
#[test]
fn hover_on_each_kind_of_name() {
let dir = project("hover-kinds", &[("main.nova", KINDS)]);
let uri = file_uri(&dir.join("src").join("main.nova"));
let mut client = Client::start(&dir, false);
open(&mut client, &uri, KINDS);
let cases: &[(&str, usize, &str)] = &[
("Point { x: 1", 0, "record Point"),
("x: 1", 0, "x: Int"),
("Shape", 0, "type Shape"),
("Round(LIMIT)", 0, "Round(Int)"),
("LIMIT)", 0, "pub const LIMIT: Int"),
("show()", 0, "fn show(self) -> String"),
("Show>", 0, "trait Show"),
("Out = Int", 0, "type Out"),
("T) -> T", 0, "T: Show"),
("kept = t", 0, "let kept: T"),
("len()", 0, "fn len(self) -> Int"),
("print(", 0, "fn print(String) -> ()"),
("Int }", 0, "type Int"),
];
for (marker, n, expected) in cases {
let response = hover(&mut client, &uri, at(KINDS, marker, *n));
assert_eq!(code(&response), *expected, "at `{marker}`: {response}");
}
let point = markdown(&hover(&mut client, &uri, at(KINDS, "Point { x: 1", 0)));
assert!(point.ends_with("\n\nA point."), "{point}");
let push = code(&hover(&mut client, &uri, at(KINDS, "push(1)", 0)));
assert!(push.contains("fn push("), "{push}");
}
#[test]
fn hover_on_a_module_and_on_a_package() {
let dir = project(
"hover-module",
&[
("main.nova", "import geometry\nfn main() {}\n"),
("geometry.nova", "pub fn area() -> Int { 1 }\n"),
],
);
let uri = file_uri(&dir.join("src").join("main.nova"));
let mut client = Client::start(&dir, false);
open(&mut client, &uri, "import geometry\nfn main() {}\n");
let response = hover(&mut client, &uri, at("import geometry", "geometry", 0));
assert_eq!(code(&response), "module geometry");
let (app, _geom) = app_and_library("hover-package", "pub fn area() -> Int { 1 }\n");
let main = file_uri(&app.join("src").join("main.nova"));
let mut client = Client::start(&app, false);
open(&mut client, &main, APP_MAIN);
let response = hover(&mut client, &main, at(APP_MAIN, "geom", 0));
assert_eq!(code(&response), "package geom 0.1.0");
}
#[test]
fn hover_works_in_a_file_with_a_syntax_error() {
let text = "fn broken( {\n}\nfn main() {\n let total = 1\n let b = total\n}\n";
let dir = project("hover-broken", &[("main.nova", text)]);
let uri = file_uri(&dir.join("src").join("main.nova"));
let mut client = Client::start(&dir, false);
open(&mut client, &uri, text);
assert_eq!(
code(&hover(&mut client, &uri, at(text, "total", 1))),
"let total: Int"
);
}
#[test]
fn hover_on_no_name_or_an_unopened_document_is_null() {
let dir = project("hover-null", &[("main.nova", AREA)]);
let uri = file_uri(&dir.join("src").join("main.nova"));
let mut client = Client::start(&dir, false);
assert_eq!(
ok(&hover(&mut client, &uri, at(AREA, "area(3)", 0))),
Value::Null
);
open(&mut client, &uri, AREA);
assert_eq!(
ok(&hover(
&mut client,
&uri,
json!({ "line": 4, "character": 0 })
)),
Value::Null
);
}
fn definition(client: &mut Client, uri: &str, position: Value) -> Value {
client.request(
"textDocument/definition",
json!({ "textDocument": { "uri": uri }, "position": position }),
)
}
fn path_of(location: &Value) -> std::path::PathBuf {
let uri = location["uri"]
.as_str()
.unwrap_or_else(|| panic!("no location: {location}"));
let decoded = lsp_client::decode(uri.strip_prefix("file://").unwrap());
let trimmed = if decoded.as_bytes().get(2) == Some(&b':') {
&decoded[1..]
} else {
&decoded[..]
};
std::path::PathBuf::from(trimmed)
}
fn text_at(location: &Value) -> String {
let text = std::fs::read_to_string(path_of(location)).unwrap();
let range = &location["range"];
let offset = |p: &Value| {
let line = p["line"].as_u64().unwrap() as usize;
let character = p["character"].as_u64().unwrap() as usize;
let start = text
.split_inclusive('\n')
.take(line)
.map(str::len)
.sum::<usize>();
let rest = &text[start..];
let mut units = 0;
for (i, c) in rest.char_indices() {
if units >= character {
return start + i;
}
units += c.len_utf16();
}
text.len()
};
text[offset(&range["start"])..offset(&range["end"])].to_string()
}
const MAIN_IMPORTS_GEOMETRY: &str =
"import geometry\n\nfn main() {\n let total = area()\n let again = total\n print(\"x\")\n}\n";
const GEOMETRY: &str = "/// One.\npub fn area() -> Int { 1 }\n";
#[test]
fn definition_in_the_same_file_and_into_another_module() {
let dir = project(
"definition-modules",
&[
("main.nova", MAIN_IMPORTS_GEOMETRY),
("geometry.nova", GEOMETRY),
],
);
let main = dir.join("src").join("main.nova");
let uri = file_uri(&main);
let mut client = Client::start(&dir, false);
open(&mut client, &uri, MAIN_IMPORTS_GEOMETRY);
let local =
definition(&mut client, &uri, at(MAIN_IMPORTS_GEOMETRY, "total", 1))["result"].clone();
assert!(same_uri(local["uri"].as_str().unwrap(), &uri), "{local}");
assert_eq!(
local["range"],
range(MAIN_IMPORTS_GEOMETRY, "total", 0, "total")
);
let area =
definition(&mut client, &uri, at(MAIN_IMPORTS_GEOMETRY, "area()", 0))["result"].clone();
assert!(
same_uri(
area["uri"].as_str().unwrap(),
&file_uri(&dir.join("src").join("geometry.nova"))
),
"{area}"
);
assert_eq!(text_at(&area), "area");
let module =
definition(&mut client, &uri, at(MAIN_IMPORTS_GEOMETRY, "geometry", 0))["result"].clone();
assert!(path_of(&module).ends_with("geometry.nova"), "{module}");
assert_eq!(
module["range"]["start"],
json!({ "line": 0, "character": 0 })
);
assert_eq!(
ok(&definition(
&mut client,
&uri,
at(MAIN_IMPORTS_GEOMETRY, "print", 0)
)),
Value::Null
);
}
#[test]
fn definition_of_a_trait_dispatched_call_is_the_traits_declaration() {
let text = "trait Show {\n fn show(self) -> String\n}\nrecord A { n: Int }\n\
impl Show for A {\n fn show(self) -> String { \"a\" }\n}\n\
fn main() {\n let a = A { n: 1 }\n let s = a.show()\n}\n";
let dir = project("definition-trait", &[("main.nova", text)]);
let uri = file_uri(&dir.join("src").join("main.nova"));
let mut client = Client::start(&dir, false);
open(&mut client, &uri, text);
let found = ok(&definition(&mut client, &uri, at(text, "show()", 0)));
assert_eq!(found["range"], range(text, "show", 0, "show"));
}
#[test]
fn definition_into_a_path_dependency_is_at_its_real_path() {
let (app, geom) = app_and_library("definition-dependency", GEOMETRY);
let main = file_uri(&app.join("src").join("main.nova"));
let mut client = Client::start(&app, false);
open(&mut client, &main, APP_MAIN);
let found = definition(&mut client, &main, at(APP_MAIN, "area()", 0))["result"].clone();
assert!(
same_uri(
found["uri"].as_str().unwrap(),
&file_uri(&geom.join("src").join("lib.nova"))
),
"{found}"
);
assert_eq!(text_at(&found), "area");
}
fn home(name: &str) -> std::path::PathBuf {
fresh_dir(&format!("{name}-home"))
}
const USES_STD: &str = "fn main() {\n let mut v = Vec::new()\n v.push(1)\n}\n";
#[test]
fn definition_into_std_opens_its_cache() {
let home = home("definition-std");
let dir = project("definition-std", &[("main.nova", USES_STD)]);
let uri = file_uri(&dir.join("src").join("main.nova"));
let mut client = Client::start_with_env(&dir, false, &[("NOVA_HOME", &home)]);
open(&mut client, &uri, USES_STD);
let found = definition(&mut client, &uri, at(USES_STD, "push", 0))["result"].clone();
let file = path_of(&found);
assert!(file.starts_with(home.join("std")), "{}", file.display());
assert_eq!(file.file_name().unwrap(), "collections.nova");
assert_eq!(text_at(&found), "push");
assert!(std::fs::metadata(&file).unwrap().permissions().readonly());
let dir_of = file.parent().unwrap();
for name in ["core", "json", "test"] {
assert!(dir_of.join(format!("{name}.nova")).is_file(), "{name}");
}
}
#[test]
fn std_cache_is_reused_and_a_damaged_file_is_replaced() {
let home = home("std-cache-reuse");
let dir = project("std-cache-reuse", &[("main.nova", USES_STD)]);
let uri = file_uri(&dir.join("src").join("main.nova"));
let mut client = Client::start_with_env(&dir, false, &[("NOVA_HOME", &home)]);
open(&mut client, &uri, USES_STD);
let file = path_of(&definition(&mut client, &uri, at(USES_STD, "push", 0))["result"]);
let written = std::fs::metadata(&file).unwrap().modified().unwrap();
let text = std::fs::read_to_string(&file).unwrap();
definition(&mut client, &uri, at(USES_STD, "push", 0));
assert_eq!(
std::fs::metadata(&file).unwrap().modified().unwrap(),
written
);
let mut perms = std::fs::metadata(&file).unwrap().permissions();
#[allow(clippy::permissions_set_readonly_false)]
perms.set_readonly(false);
std::fs::set_permissions(&file, perms).unwrap();
std::fs::write(&file, "damaged").unwrap();
definition(&mut client, &uri, at(USES_STD, "push", 0));
assert_eq!(std::fs::read_to_string(&file).unwrap(), text);
}
#[test]
fn two_servers_write_std_cache_at_once() {
let home = home("std-cache-two");
let dir = project("std-cache-two", &[("main.nova", USES_STD)]);
let uri = file_uri(&dir.join("src").join("main.nova"));
let mut a = Client::start_with_env(&dir, false, &[("NOVA_HOME", &home)]);
let mut b = Client::start_with_env(&dir, false, &[("NOVA_HOME", &home)]);
open(&mut a, &uri, USES_STD);
open(&mut b, &uri, USES_STD);
let (ra, rb) = std::thread::scope(|s| {
let ta = s.spawn(|| definition(&mut a, &uri, at(USES_STD, "push", 0)));
let tb = s.spawn(|| definition(&mut b, &uri, at(USES_STD, "push", 0)));
(ta.join().unwrap(), tb.join().unwrap())
});
for found in [&ra["result"], &rb["result"]] {
assert_eq!(text_at(found), "push", "{found}");
}
}
#[test]
fn a_request_inside_std_cache_is_answered_and_nothing_is_published() {
let home = home("std-cache-inside");
let dir = project("std-cache-inside", &[("main.nova", USES_STD)]);
let uri = file_uri(&dir.join("src").join("main.nova"));
let mut client = Client::start_with_env(&dir, false, &[("NOVA_HOME", &home)]);
open(&mut client, &uri, USES_STD);
let file = path_of(&definition(&mut client, &uri, at(USES_STD, "push", 0))["result"]);
let text = std::fs::read_to_string(&file).unwrap();
let std_uri = file_uri(&file);
client.clear_unread();
open(&mut client, &std_uri, &text);
let start = text.find("record Vec").unwrap() + "record ".len();
let response = hover(&mut client, &std_uri, position(&text, start));
assert!(code(&response).contains("record Vec"), "{response}");
let sentinel_dir = fresh_dir("std-cache-inside-sentinel");
let sentinel = file_uri(&sentinel_dir.join("main.nova"));
std::fs::write(sentinel_dir.join("main.nova"), "fn main() {}\n").unwrap();
open(&mut client, &sentinel, "fn main() {}\n");
assert_eq!(client.last_diagnostics_before(&std_uri, &sentinel), None);
}
const WIDE: &str =
"fn main() {\n let s = \"ก😀\"; let total = 1\n let b = \"ก😀\"; let c = total\n}\n";
#[test]
fn ranges_count_utf16_after_thai_and_an_emoji() {
let dir = project("definition-utf16", &[("main.nova", WIDE)]);
let uri = file_uri(&dir.join("src").join("main.nova"));
let mut client = Client::start(&dir, false);
open(&mut client, &uri, WIDE);
let found = definition(&mut client, &uri, at(WIDE, "total", 1))["result"].clone();
assert_eq!(found["range"], range(WIDE, "total", 0, "total"));
let response = hover(&mut client, &uri, at(WIDE, "total", 1));
assert_eq!(
response["result"]["range"],
range(WIDE, "total", 1, "total")
);
}
#[test]
fn definition_into_a_downloaded_package() {
let (app, home) = registry_app("definition-registry", "0.1");
let geom = lock_and_cache(&app, &home, GEOMETRY);
let main = file_uri(&app.join("src").join("main.nova"));
let mut client = Client::start_with_env(&app, false, &[("NOVA_HOME", &home)]);
open(&mut client, &main, APP_MAIN);
let found = definition(&mut client, &main, at(APP_MAIN, "area()", 0))["result"].clone();
assert!(
same_uri(
found["uri"].as_str().unwrap(),
&file_uri(&geom.join("src").join("lib.nova"))
),
"{found}"
);
assert_eq!(text_at(&found), "area");
}
fn references(client: &mut Client, uri: &str, position: Value, declarations: bool) -> Vec<Value> {
let response = client.request(
"textDocument/references",
json!({
"textDocument": { "uri": uri },
"position": position,
"context": { "includeDeclaration": declarations },
}),
);
response["result"]
.as_array()
.unwrap_or_else(|| panic!("no references: {response}"))
.clone()
}
fn places(locations: &[Value]) -> Vec<(String, u64, u64)> {
locations
.iter()
.map(|l| {
let name = path_of(l)
.file_name()
.unwrap()
.to_string_lossy()
.into_owned();
let start = &l["range"]["start"];
(
name,
start["line"].as_u64().unwrap(),
start["character"].as_u64().unwrap(),
)
})
.collect()
}
#[test]
fn references_with_and_without_the_declaration() {
let dir = project(
"references-local",
&[
("main.nova", MAIN_IMPORTS_GEOMETRY),
("geometry.nova", GEOMETRY),
],
);
let uri = file_uri(&dir.join("src").join("main.nova"));
let mut client = Client::start(&dir, false);
open(&mut client, &uri, MAIN_IMPORTS_GEOMETRY);
let with = references(
&mut client,
&uri,
at(MAIN_IMPORTS_GEOMETRY, "total", 1),
true,
);
assert_eq!(
places(&with),
[
("main.nova".to_string(), 3, 8),
("main.nova".to_string(), 4, 16)
]
);
let without = references(
&mut client,
&uri,
at(MAIN_IMPORTS_GEOMETRY, "total", 0),
false,
);
assert_eq!(places(&without), [("main.nova".to_string(), 4, 16)]);
let area = references(
&mut client,
&uri,
at(MAIN_IMPORTS_GEOMETRY, "area()", 0),
true,
);
let names: Vec<String> = places(&area).into_iter().map(|p| p.0).collect();
assert_eq!(names.len(), 2, "{area:?}");
let mut sorted = area.clone();
sorted.sort_by_key(path_of);
assert_eq!(places(&sorted), places(&area));
}
#[test]
fn references_from_the_app_reach_into_its_dependency() {
let lib = "pub fn area() -> Int { 1 }\npub fn twice() -> Int { area() + area() }\n";
let (app, _geom) = app_and_library("references-dependency", lib);
let main = file_uri(&app.join("src").join("main.nova"));
let mut client = Client::start(&app, false);
open(&mut client, &main, APP_MAIN);
let found = references(&mut client, &main, at(APP_MAIN, "area()", 0), true);
let mut counts = std::collections::BTreeMap::new();
for (name, _, _) in places(&found) {
*counts.entry(name).or_insert(0) += 1;
}
assert_eq!(counts.get("lib.nova"), Some(&3), "{found:?}");
assert_eq!(counts.get("main.nova"), Some(&1), "{found:?}");
}
const FAMILY: &str = "trait Show { fn show(self) -> String }\n\
record A { n: Int }\nrecord B { n: Int }\n\
impl Show for A { fn show(self) -> String { \"a\" } }\n\
impl Show for B { fn show(self) -> String { \"b\" } }\n\
fn main() {\n let a = A { n: 1 }\n let b = B { n: 2 }\n let s = a.show()\n let t = b.show()\n}\n";
#[test]
fn references_find_a_trait_methods_family_from_each_member() {
let dir = project("references-family", &[("main.nova", FAMILY)]);
let uri = file_uri(&dir.join("src").join("main.nova"));
let mut client = Client::start(&dir, false);
open(&mut client, &uri, FAMILY);
let from_trait = places(&references(&mut client, &uri, at(FAMILY, "show", 0), true));
assert_eq!(from_trait.len(), 5, "{from_trait:?}");
let from_impl = places(&references(&mut client, &uri, at(FAMILY, "show", 1), true));
let from_call = places(&references(
&mut client,
&uri,
at(FAMILY, "show()", 1),
true,
));
assert_eq!(from_impl, from_trait);
assert_eq!(from_call, from_trait);
}
fn prepare(client: &mut Client, uri: &str, position: Value) -> Value {
client.request(
"textDocument/prepareRename",
json!({ "textDocument": { "uri": uri }, "position": position }),
)
}
fn rename(client: &mut Client, uri: &str, position: Value, new: &str) -> Value {
client.request(
"textDocument/rename",
json!({ "textDocument": { "uri": uri }, "position": position, "newName": new }),
)
}
#[track_caller]
fn refused(response: &Value) -> String {
assert_eq!(response["error"]["code"], -32803, "{response}");
response["error"]["message"].as_str().unwrap().to_string()
}
#[track_caller]
fn edits(response: &Value) -> Vec<(String, u64, u64, String)> {
let changes = response["result"]["changes"]
.as_object()
.unwrap_or_else(|| panic!("no edit: {response}"));
let mut out = Vec::new();
for (uri, list) in changes {
let name = path_of(&json!({ "uri": uri }))
.file_name()
.unwrap()
.to_string_lossy()
.into_owned();
for e in list.as_array().unwrap() {
let start = &e["range"]["start"];
out.push((
name.clone(),
start["line"].as_u64().unwrap(),
start["character"].as_u64().unwrap(),
e["newText"].as_str().unwrap().to_string(),
));
}
}
out.sort();
out
}
#[test]
fn prepare_rename_gives_the_range_and_spelling() {
let dir = project(
"prepare-range",
&[
("main.nova", MAIN_IMPORTS_GEOMETRY),
("geometry.nova", GEOMETRY),
],
);
let uri = file_uri(&dir.join("src").join("main.nova"));
let mut client = Client::start(&dir, false);
open(&mut client, &uri, MAIN_IMPORTS_GEOMETRY);
let response = prepare(&mut client, &uri, at(MAIN_IMPORTS_GEOMETRY, "total", 1));
assert_eq!(response["result"]["placeholder"], "total");
assert_eq!(
response["result"]["range"],
range(MAIN_IMPORTS_GEOMETRY, "total", 1, "total")
);
let blank = json!({ "line": 1, "character": 0 });
assert_eq!(ok(&prepare(&mut client, &uri, blank.clone())), Value::Null);
assert_eq!(ok(&rename(&mut client, &uri, blank, "x")), Value::Null);
}
const REFUSALS: &str = "import geometry\n\
record P { n: Int }\n\
impl Display for P {\n fn fmt(self) -> String { \"p\" }\n}\n\
fn main() {\n let mut v = Vec::new()\n v.push(1)\n print(\"x\")\n let n: Int = area()\n let xs = [1]\n let k = xs.len()\n}\n";
#[test]
fn prepare_rename_refuses_what_is_not_the_projects() {
let dir = project(
"prepare-refusals",
&[("main.nova", REFUSALS), ("geometry.nova", GEOMETRY)],
);
let uri = file_uri(&dir.join("src").join("main.nova"));
let home = home("prepare-refusals");
let mut client = Client::start_with_env(&dir, false, &[("NOVA_HOME", &home)]);
open(&mut client, &uri, REFUSALS);
let cases: &[(&str, usize, &str)] = &[
("push", 0, "`push` is declared in std and cannot be renamed"),
("fmt", 0, "`fmt` is declared in std and cannot be renamed"),
("print", 0, "`print` is built in and cannot be renamed"),
("len()", 0, "`len` is built in and cannot be renamed"),
("Int =", 0, "`Int` is built in and cannot be renamed"),
("self", 0, "`self` is a keyword and cannot be renamed"),
(
"geometry",
0,
"a module or package is renamed by renaming its file or its `nova.toml`",
),
];
for (marker, n, message) in cases {
let response = prepare(&mut client, &uri, at(REFUSALS, marker, *n));
assert_eq!(refused(&response), *message, "at `{marker}`");
}
let (app, _geom) = app_and_library("prepare-dependency", GEOMETRY);
let main = file_uri(&app.join("src").join("main.nova"));
let mut client = Client::start(&app, false);
open(&mut client, &main, APP_MAIN);
assert_eq!(
refused(&prepare(&mut client, &main, at(APP_MAIN, "area()", 0))),
"`area` is declared in the dependency `geom` and cannot be renamed"
);
}
#[test]
fn prepare_rename_refuses_inside_nova_s_caches() {
let home = home("prepare-cache");
let dir = project("prepare-cache", &[("main.nova", USES_STD)]);
let uri = file_uri(&dir.join("src").join("main.nova"));
let mut client = Client::start_with_env(&dir, false, &[("NOVA_HOME", &home)]);
open(&mut client, &uri, USES_STD);
let file = path_of(&definition(&mut client, &uri, at(USES_STD, "push", 0))["result"]);
let text = std::fs::read_to_string(&file).unwrap();
let std_uri = file_uri(&file);
open(&mut client, &std_uri, &text);
let start = text.find("fn push").unwrap() + "fn ".len();
assert_eq!(
refused(&prepare(&mut client, &std_uri, position(&text, start))),
"`push` is in nova's cache and cannot be renamed"
);
}
#[test]
fn rename_across_files() {
let dir = project(
"rename-files",
&[
("main.nova", MAIN_IMPORTS_GEOMETRY),
("geometry.nova", GEOMETRY),
],
);
let uri = file_uri(&dir.join("src").join("main.nova"));
let mut client = Client::start(&dir, false);
open(&mut client, &uri, MAIN_IMPORTS_GEOMETRY);
let response = rename(
&mut client,
&uri,
at(MAIN_IMPORTS_GEOMETRY, "area()", 0),
"size",
);
assert_eq!(
edits(&response),
[
("geometry.nova".to_string(), 1, 7, "size".to_string()),
("main.nova".to_string(), 3, 16, "size".to_string()),
]
);
}
const SHORTHAND: &str = "record Point { x: Int }\n\
fn main() {\n let x = 1\n let p = Point { x }\n let y = p.x\n}\n";
#[test]
fn rename_writes_out_a_shorthand() {
let dir = project("rename-shorthand", &[("main.nova", SHORTHAND)]);
let uri = file_uri(&dir.join("src").join("main.nova"));
let mut client = Client::start(&dir, false);
open(&mut client, &uri, SHORTHAND);
let field = rename(&mut client, &uri, at(SHORTHAND, "x: Int", 0), "w");
assert_eq!(
edits(&field),
[
("main.nova".to_string(), 0, 15, "w".to_string()),
("main.nova".to_string(), 3, 20, "w: x".to_string()),
("main.nova".to_string(), 4, 14, "w".to_string()),
]
);
let local = rename(&mut client, &uri, at(SHORTHAND, "x = 1", 0), "z");
assert_eq!(
edits(&local),
[
("main.nova".to_string(), 2, 8, "z".to_string()),
("main.nova".to_string(), 3, 20, "x: z".to_string()),
]
);
}
#[test]
fn rename_a_trait_methods_family() {
let dir = project("rename-family", &[("main.nova", FAMILY)]);
let uri = file_uri(&dir.join("src").join("main.nova"));
let mut client = Client::start(&dir, false);
open(&mut client, &uri, FAMILY);
let response = rename(&mut client, &uri, at(FAMILY, "show()", 0), "render");
let found = edits(&response);
assert_eq!(found.len(), 5, "{found:?}");
assert!(found.iter().all(|e| e.3 == "render"), "{found:?}");
}
#[test]
fn rename_refuses_a_new_name_that_is_not_a_name() {
let dir = project(
"rename-invalid",
&[
("main.nova", MAIN_IMPORTS_GEOMETRY),
("geometry.nova", GEOMETRY),
],
);
let uri = file_uri(&dir.join("src").join("main.nova"));
let mut client = Client::start(&dir, false);
open(&mut client, &uri, MAIN_IMPORTS_GEOMETRY);
let place = at(MAIN_IMPORTS_GEOMETRY, "total", 0);
for new in ["1x", "let", "two words", "a-b"] {
let message = refused(&rename(&mut client, &uri, place.clone(), new));
assert!(message.contains("is not a name"), "{new}: {message}");
}
assert_eq!(
refused(&rename(&mut client, &uri, place.clone(), "_")),
"`_` cannot be a name"
);
assert_eq!(
refused(&rename(&mut client, &uri, place.clone(), "Int")),
"`Int` is a built-in type's name"
);
let same = rename(&mut client, &uri, place, "total");
assert!(same["error"].is_null(), "{same}");
assert!(same["result"]["changes"].is_null(), "{same}");
}
#[test]
fn rename_edits_an_unsaved_buffer_at_its_own_offsets() {
let dir = project(
"rename-unsaved",
&[
("main.nova", MAIN_IMPORTS_GEOMETRY),
("geometry.nova", GEOMETRY),
],
);
let uri = file_uri(&dir.join("src").join("main.nova"));
let mut client = Client::start(&dir, false);
let buffer = format!("// one\n// two\n{MAIN_IMPORTS_GEOMETRY}");
open(&mut client, &uri, &buffer);
let response = rename(&mut client, &uri, at(&buffer, "total", 0), "sum");
assert_eq!(
edits(&response),
[
("main.nova".to_string(), 5, 8, "sum".to_string()),
("main.nova".to_string(), 6, 16, "sum".to_string()),
]
);
}
#[test]
fn rename_in_a_crlf_document_edits_the_right_characters() {
let crlf = MAIN_IMPORTS_GEOMETRY.replace('\n', "\r\n");
let dir = project(
"rename-crlf",
&[("main.nova", crlf.as_str()), ("geometry.nova", GEOMETRY)],
);
let uri = file_uri(&dir.join("src").join("main.nova"));
let mut client = Client::start(&dir, false);
open(&mut client, &uri, &crlf);
let response = rename(&mut client, &uri, at(&crlf, "total", 1), "sum");
assert_eq!(
edits(&response),
[
("main.nova".to_string(), 3, 8, "sum".to_string()),
("main.nova".to_string(), 4, 16, "sum".to_string()),
]
);
}
#[test]
fn rename_reaches_the_projects_tests_files() {
let dir = project(
"rename-tests",
&[("lib.nova", "pub fn area() -> Int { 1 }\n")],
);
std::fs::create_dir_all(dir.join("tests")).unwrap();
let test = "import demo\n\n@test\nfn area_is_one() {\n let a: Int = area()\n}\n";
std::fs::write(dir.join("tests").join("area.nova"), test).unwrap();
let lib = file_uri(&dir.join("src").join("lib.nova"));
let mut client = Client::start(&dir, false);
open(&mut client, &lib, "pub fn area() -> Int { 1 }\n");
let response = rename(
&mut client,
&lib,
json!({ "line": 0, "character": 7 }),
"size",
);
assert_eq!(
edits(&response),
[
("area.nova".to_string(), 4, 17, "size".to_string()),
("lib.nova".to_string(), 0, 7, "size".to_string()),
]
);
}
#[test]
fn a_rename_that_captures_a_name_is_refused() {
let text = "fn main() {\n let y = 1\n let x = 2\n let z = x + y\n}\n";
let dir = project("check-capture", &[("main.nova", text)]);
let uri = file_uri(&dir.join("src").join("main.nova"));
let mut client = Client::start(&dir, false);
open(&mut client, &uri, text);
assert_eq!(
refused(&rename(&mut client, &uri, at(text, "x = 2", 0), "y")),
"renaming `x` to `y` would make 1 other name refer to it"
);
}
#[test]
fn a_rename_that_changes_what_a_name_means_is_refused() {
let text = "fn main() {\n let x = 1\n let f = |y: Int| x + y\n}\n";
let dir = project("check-change", &[("main.nova", text)]);
let uri = file_uri(&dir.join("src").join("main.nova"));
let mut client = Client::start(&dir, false);
open(&mut client, &uri, text);
assert_eq!(
refused(&rename(&mut client, &uri, at(text, "x = 1", 0), "y")),
"renaming `x` to `y` would change what 1 name refers to"
);
}
#[test]
fn a_rename_that_adds_an_error_is_refused() {
let text =
"fn helper() -> Int { 1 }\nfn other() -> Int { 2 }\nfn main() {\n let a = helper()\n}\n";
let dir = project("check-error", &[("main.nova", text)]);
let uri = file_uri(&dir.join("src").join("main.nova"));
let mut client = Client::start(&dir, false);
open(&mut client, &uri, text);
let message = refused(&rename(&mut client, &uri, at(text, "helper", 0), "other"));
assert!(
message.starts_with("renaming `helper` to `other` would add an error: E0002"),
"{message}"
);
}
#[test]
fn a_program_with_errors_can_still_be_renamed() {
let text = "fn main() {\n let total: Int = \"s\"\n let b = total\n}\n";
let dir = project("check-broken", &[("main.nova", text)]);
let uri = file_uri(&dir.join("src").join("main.nova"));
let mut client = Client::start(&dir, false);
open(&mut client, &uri, text);
let response = rename(&mut client, &uri, at(text, "total", 0), "sum");
assert_eq!(edits(&response).len(), 2, "{response}");
}
const GATE_LIB: &str = "/// A rectangle's area.\npub fn area(w: Int, h: Int) -> Int { w * h }\n";
const GATE_UTIL: &str =
"pub record Size { w: Int, h: Int }\npub fn square(n: Int) -> Size { Size { w: n, h: n } }\n";
const GATE_MAIN: &str = "import geom\nimport util\n\n\
fn main() {\n let side = 3\n let s = square(side)\n let w = s.w\n let t = Size { w, h: s.h }\n let a = area(t.w, t.h)\n let mut v = Vec::new()\n v.push(a)\n let h = 1\n let total = side + h\n}\n";
#[test]
fn the_navigation_gate() {
let (app, geom) = app_and_library("navigation-gate", GATE_LIB);
std::fs::write(app.join("src").join("main.nova"), GATE_MAIN).unwrap();
std::fs::write(app.join("src").join("util.nova"), GATE_UTIL).unwrap();
let home = home("navigation-gate");
let main = file_uri(&app.join("src").join("main.nova"));
let mut client = Client::start_with_env(&app, false, &[("NOVA_HOME", &home)]);
open(&mut client, &main, GATE_MAIN);
assert_eq!(
code(&hover(&mut client, &main, at(GATE_MAIN, "side", 0))),
"let side: Int"
);
assert_eq!(
markdown(&hover(&mut client, &main, at(GATE_MAIN, "area(", 0))),
"```nova\npub fn area(w: Int, h: Int) -> Int\n```\n\nA rectangle's area."
);
assert_eq!(
code(&hover(&mut client, &main, at(GATE_MAIN, "w\n", 0))),
"w: Int"
);
assert!(code(&hover(&mut client, &main, at(GATE_MAIN, "push", 0))).contains("fn push("));
let local = definition(&mut client, &main, at(GATE_MAIN, "side)", 0))["result"].clone();
assert_eq!(local["range"], range(GATE_MAIN, "side", 0, "side"));
let square = definition(&mut client, &main, at(GATE_MAIN, "square", 0))["result"].clone();
assert!(path_of(&square).ends_with("util.nova"), "{square}");
let area = definition(&mut client, &main, at(GATE_MAIN, "area(", 0))["result"].clone();
assert!(
same_uri(
area["uri"].as_str().unwrap(),
&file_uri(&geom.join("src").join("lib.nova"))
),
"{area}"
);
let push = definition(&mut client, &main, at(GATE_MAIN, "push", 0))["result"].clone();
assert!(path_of(&push).starts_with(home.join("std")), "{push}");
assert_eq!(
references(&mut client, &main, at(GATE_MAIN, "side", 0), true).len(),
3
);
assert_eq!(
references(&mut client, &main, at(GATE_MAIN, "side", 0), false).len(),
2
);
let renamed = edits(&rename(
&mut client,
&main,
at(GATE_MAIN, "w\n", 0),
"width",
));
assert_eq!(renamed.len(), 5, "{renamed:?}");
assert!(
renamed
.iter()
.any(|e| e.0 == "main.nova" && e.3 == "width: w"),
"{renamed:?}"
);
assert_eq!(
renamed.iter().filter(|e| e.0 == "util.nova").count(),
2,
"{renamed:?}"
);
assert!(refused(&prepare(&mut client, &main, at(GATE_MAIN, "push", 0))).contains("in std"));
assert!(
refused(&prepare(&mut client, &main, at(GATE_MAIN, "area(", 0)))
.contains("dependency `geom`")
);
assert_eq!(
refused(&rename(
&mut client,
&main,
at(GATE_MAIN, "h = 1", 0),
"side"
)),
"renaming `h` to `side` would make 1 other name refer to it"
);
}
#[test]
fn each_request_works_in_a_broken_file_and_is_empty_for_an_unopened_one() {
let text = "fn broken( {\n}\nfn main() {\n let total = 1\n let b = total\n}\n";
let dir = project("requests-broken", &[("main.nova", text)]);
let uri = file_uri(&dir.join("src").join("main.nova"));
let other = file_uri(&dir.join("src").join("other.nova"));
let mut client = Client::start(&dir, false);
open(&mut client, &uri, text);
let use_ = at(text, "total", 1);
let found = definition(&mut client, &uri, use_.clone())["result"].clone();
assert_eq!(found["range"], range(text, "total", 0, "total"));
assert_eq!(references(&mut client, &uri, use_.clone(), true).len(), 2);
assert_eq!(
prepare(&mut client, &uri, use_.clone())["result"]["placeholder"],
"total"
);
assert_eq!(
edits(&rename(&mut client, &uri, use_.clone(), "sum")).len(),
2
);
assert_eq!(
ok(&definition(&mut client, &other, use_.clone())),
Value::Null
);
assert!(references(&mut client, &other, use_.clone(), true).is_empty());
assert_eq!(ok(&prepare(&mut client, &other, use_.clone())), Value::Null);
assert_eq!(ok(&rename(&mut client, &other, use_, "sum")), Value::Null);
}
#[test]
fn rename_refuses_the_entry_point_and_an_extern_symbol() {
let text =
"extern \"C\" {\n fn abs(value: Int) -> Int\n}\n\nfn main() {\n let n = abs(1)\n}\n";
let dir = project("rename-entry-extern", &[("main.nova", text)]);
let uri = file_uri(&dir.join("src").join("main.nova"));
let mut client = Client::start(&dir, false);
open(&mut client, &uri, text);
assert_eq!(
refused(&prepare(&mut client, &uri, at(text, "main", 0))),
"`main` is a program's entry point and cannot be renamed"
);
assert_eq!(
refused(&rename(
&mut client,
&uri,
at(text, "abs(1)", 0),
"absolute"
)),
"`abs` names a C symbol and cannot be renamed"
);
}
#[test]
fn rename_from_a_file_the_program_does_not_reach_keeps_to_its_own_names() {
let main = "import geometry\n\nfn main() {\n let a = area()\n}\n";
let scratch = "import geometry\n\nfn helper() -> Int {\n let b = area()\n b\n}\n";
let dir = project(
"rename-unreached",
&[
("main.nova", main),
("geometry.nova", GEOMETRY),
("scratch.nova", scratch),
],
);
let uri = file_uri(&dir.join("src").join("scratch.nova"));
let mut client = Client::start(&dir, false);
open(&mut client, &uri, scratch);
assert_eq!(
refused(&prepare(&mut client, &uri, at(scratch, "area()", 0))),
"`area` is declared outside this file, which the project's program does not \
reach; rename it from a file the program reaches"
);
assert_eq!(
edits(&rename(&mut client, &uri, at(scratch, "b = area", 0), "c")).len(),
2
);
}
#[test]
fn rename_in_a_loose_module_keeps_to_names_no_importer_can_see() {
let dir = fresh_dir("rename-loose-module");
let main = "import util\n\nfn main() {\n let h = helper()\n}\n";
let util = "pub fn helper() -> Int { inner() }\n\nfn inner() -> Int { 1 }\n";
std::fs::write(dir.join("main.nova"), main).unwrap();
std::fs::write(dir.join("util.nova"), util).unwrap();
let util_uri = file_uri(&dir.join("util.nova"));
let main_uri = file_uri(&dir.join("main.nova"));
let mut client = Client::start(&dir, false);
open(&mut client, &util_uri, util);
assert_eq!(
refused(&prepare(&mut client, &util_uri, at(util, "helper", 0))),
"`helper` can be used from files that import this module, which this analysis \
cannot see; rename it from one of them"
);
assert_eq!(
edits(&rename(
&mut client,
&util_uri,
at(util, "inner", 0),
"core"
))
.len(),
2
);
open(&mut client, &main_uri, main);
let found = edits(&rename(
&mut client,
&main_uri,
at(main, "helper", 0),
"assist",
));
let files: Vec<&str> = found.iter().map(|e| e.0.as_str()).collect();
assert_eq!(files, ["main.nova", "util.nova"], "{found:?}");
}
const EXAMPLE: &str = "/// Doubles `n`.\n///\n/// ```\n/// # let hidden = 1\n\
/// let shown = twice(1)\n/// ```\npub fn twice(n: Int) -> Int { n * 2 }\n\n\
fn main() {\n let d = twice(2)\n}\n";
#[test]
fn hover_leaves_out_an_examples_hidden_lines() {
let dir = project("hover-hidden-lines", &[("main.nova", EXAMPLE)]);
let uri = file_uri(&dir.join("src").join("main.nova"));
let mut client = Client::start(&dir, false);
open(&mut client, &uri, EXAMPLE);
let md = markdown(&hover(&mut client, &uri, at(EXAMPLE, "twice(2)", 0)));
assert!(!md.contains("hidden"), "{md}");
assert_eq!(
md,
"```nova\npub fn twice(n: Int) -> Int\n```\n\nDoubles `n`.\n\n```\nlet shown = twice(1)\n```"
);
}