diff --git a/crates/typst-ide/src/complete.rs b/crates/typst-ide/src/complete.rs index a1b5f1d2e..dc6c9abeb 100644 --- a/crates/typst-ide/src/complete.rs +++ b/crates/typst-ide/src/complete.rs @@ -1390,9 +1390,10 @@ mod tests { use std::collections::BTreeSet; use typst::model::Document; + use typst::syntax::Source; use super::{autocomplete, Completion}; - use crate::tests::TestWorld; + use crate::tests::{SourceExt, TestWorld}; type Response = Option<(usize, Vec)>; @@ -1465,21 +1466,17 @@ mod tests { #[track_caller] fn test_with_world(world: &TestWorld, cursor: isize) -> Response { let doc = typst::compile(&world).output.ok(); - test_with_world_and_doc(world, doc.as_ref(), cursor) + test_full(world, &world.main, doc.as_ref(), cursor) } #[track_caller] - fn test_with_world_and_doc( + fn test_full( world: &TestWorld, + source: &Source, doc: Option<&Document>, cursor: isize, ) -> Response { - let cursor = if cursor < 0 { - world.main.len_bytes().checked_add_signed(cursor).unwrap() - } else { - cursor as usize - }; - autocomplete(&world, doc, &world.main, cursor, true) + autocomplete(&world, doc, source, source.cursor(cursor), true) } #[test] @@ -1523,7 +1520,7 @@ mod tests { let end = world.main.len_bytes(); world.main.edit(end..end, " #cite()"); - test_with_world_and_doc(&world, doc.as_ref(), -1) + test_full(&world, &world.main, doc.as_ref(), -1) .must_include(["netwok", "glacier-melt", "supplement"]) .must_exclude(["bib"]); } diff --git a/crates/typst-ide/src/lib.rs b/crates/typst-ide/src/lib.rs index 90fc125e7..4c1542fd5 100644 --- a/crates/typst-ide/src/lib.rs +++ b/crates/typst-ide/src/lib.rs @@ -111,6 +111,7 @@ mod tests { pub struct TestWorld { pub main: Source, assets: HashMap, + sources: HashMap, base: &'static TestBase, } @@ -124,11 +125,12 @@ mod tests { Self { main, assets: HashMap::new(), + sources: HashMap::new(), base: singleton!(TestBase, TestBase::default()), } } - /// Add an additional file to the test world. + /// Add an additional asset file to the test world. #[track_caller] pub fn with_asset_by_name(mut self, filename: &str) -> Self { let id = FileId::new(None, VirtualPath::new(filename)); @@ -138,6 +140,14 @@ mod tests { self } + /// Add an additional source file to the test world. + pub fn with_source(mut self, path: &str, text: &str) -> Self { + let id = FileId::new(None, VirtualPath::new(path)); + let source = Source::new(id, text.into()); + self.sources.insert(id, source); + self + } + /// The ID of the main file in a `TestWorld`. pub fn main_id() -> FileId { *singleton!(FileId, FileId::new(None, VirtualPath::new("main.typ"))) @@ -160,6 +170,8 @@ mod tests { fn source(&self, id: FileId) -> FileResult { if id == self.main.id() { Ok(self.main.clone()) + } else if let Some(source) = self.sources.get(&id) { + Ok(source.clone()) } else { Err(FileError::NotFound(id.vpath().as_rootless_path().into())) } @@ -181,6 +193,22 @@ mod tests { } } + /// Extra methods for [`Source`]. + pub trait SourceExt { + /// Negative cursors index from the back. + fn cursor(&self, cursor: isize) -> usize; + } + + impl SourceExt for Source { + fn cursor(&self, cursor: isize) -> usize { + if cursor < 0 { + self.len_bytes().checked_add_signed(cursor).unwrap() + } else { + cursor as usize + } + } + } + /// Shared foundation of all test worlds. struct TestBase { library: LazyHash, diff --git a/crates/typst-ide/src/tooltip.rs b/crates/typst-ide/src/tooltip.rs index df93d1dc5..318ff0ce0 100644 --- a/crates/typst-ide/src/tooltip.rs +++ b/crates/typst-ide/src/tooltip.rs @@ -134,7 +134,7 @@ fn closure_tooltip(leaf: &LinkedNode) -> Option { names.sort(); let tooltip = repr::separated_list(&names, "and"); - Some(Tooltip::Text(eco_format!("This closure captures {tooltip}."))) + Some(Tooltip::Text(eco_format!("This closure captures {tooltip}"))) } /// Tooltip text for a hovered length. @@ -256,32 +256,65 @@ mod tests { use typst::syntax::Side; use super::{tooltip, Tooltip}; - use crate::tests::TestWorld; + use crate::tests::{SourceExt, TestWorld}; - fn text(text: &str) -> Option { - Some(Tooltip::Text(text.into())) + type Response = Option; + + trait ResponseExt { + fn must_be_none(&self) -> &Self; + fn must_be_text(&self, text: &str) -> &Self; + fn must_be_code(&self, code: &str) -> &Self; } - fn code(code: &str) -> Option { - Some(Tooltip::Code(code.into())) + impl ResponseExt for Response { + #[track_caller] + fn must_be_none(&self) -> &Self { + assert_eq!(*self, None); + self + } + + #[track_caller] + fn must_be_text(&self, text: &str) -> &Self { + assert_eq!(*self, Some(Tooltip::Text(text.into()))); + self + } + + #[track_caller] + fn must_be_code(&self, code: &str) -> &Self { + assert_eq!(*self, Some(Tooltip::Code(code.into()))); + self + } } #[track_caller] - fn test(text: &str, cursor: usize, side: Side, expected: Option) { + fn test(text: &str, cursor: isize, side: Side) -> Response { let world = TestWorld::new(text); + test_with_world(&world, cursor, side) + } + + #[track_caller] + fn test_with_world(world: &TestWorld, cursor: isize, side: Side) -> Response { + let source = &world.main; let doc = typst::compile(&world).output.ok(); - assert_eq!(tooltip(&world, doc.as_ref(), &world.main, cursor, side), expected); + tooltip(&world, doc.as_ref(), source, source.cursor(cursor), side) } #[test] fn test_tooltip() { - test("#let x = 1 + 2", 5, Side::After, code("3")); - test("#let x = 1 + 2", 6, Side::Before, code("3")); - test("#let f(x) = x + y", 11, Side::Before, text("This closure captures `y`.")); + test("#let x = 1 + 2", 14, Side::After).must_be_none(); + test("#let x = 1 + 2", 5, Side::After).must_be_code("3"); + test("#let x = 1 + 2", 6, Side::Before).must_be_code("3"); + test("#let x = 1 + 2", 6, Side::Before).must_be_code("3"); } #[test] - fn test_empty_contextual() { - test("#{context}", 10, Side::Before, code("context()")); + fn test_tooltip_empty_contextual() { + test("#{context}", 10, Side::Before).must_be_code("context()"); + } + + #[test] + fn test_tooltip_closure() { + test("#let f(x) = x + y", 11, Side::Before) + .must_be_text("This closure captures `y`"); } }