mirror of
https://github.com/typst/typst
synced 2025-05-13 12:36:23 +08:00
Further improve IDE tests (#5602)
This commit is contained in:
parent
45c866fbb9
commit
db06dbf976
@ -1506,14 +1506,13 @@ impl BracketMode {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::borrow::Borrow;
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use typst::layout::PagedDocument;
|
||||
use typst::syntax::{FileId, Source, VirtualPath};
|
||||
use typst::World;
|
||||
|
||||
use super::{autocomplete, Completion};
|
||||
use crate::tests::{SourceExt, TestWorld};
|
||||
use crate::tests::{FilePos, TestWorld, WorldLike};
|
||||
|
||||
/// Quote a string.
|
||||
macro_rules! q {
|
||||
@ -1585,60 +1584,50 @@ mod tests {
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn test(text: &str, cursor: isize) -> Response {
|
||||
let world = TestWorld::new(text);
|
||||
test_with_world(&world, cursor)
|
||||
fn test(world: impl WorldLike, pos: impl FilePos) -> Response {
|
||||
let world = world.acquire();
|
||||
let world = world.borrow();
|
||||
let doc = typst::compile(world).output.ok();
|
||||
test_with_doc(world, pos, doc.as_ref())
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn test_with_world(world: &TestWorld, cursor: isize) -> Response {
|
||||
let doc = typst::compile(&world).output.ok();
|
||||
test_full(world, &world.main, doc.as_ref(), cursor)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn test_with_path(world: &TestWorld, path: &str, cursor: isize) -> Response {
|
||||
let doc = typst::compile(&world).output.ok();
|
||||
let id = FileId::new(None, VirtualPath::new(path));
|
||||
let source = world.source(id).unwrap();
|
||||
test_full(world, &source, doc.as_ref(), cursor)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn test_full(
|
||||
world: &TestWorld,
|
||||
source: &Source,
|
||||
fn test_with_doc(
|
||||
world: impl WorldLike,
|
||||
pos: impl FilePos,
|
||||
doc: Option<&PagedDocument>,
|
||||
cursor: isize,
|
||||
) -> Response {
|
||||
autocomplete(world, doc, source, source.cursor(cursor), true)
|
||||
let world = world.acquire();
|
||||
let world = world.borrow();
|
||||
let (source, cursor) = pos.resolve(world);
|
||||
autocomplete(world, doc, &source, cursor, true)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_autocomplete_hash_expr() {
|
||||
test("#i", 2).must_include(["int", "if conditional"]);
|
||||
test("#i", -1).must_include(["int", "if conditional"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_autocomplete_array_method() {
|
||||
test("#().", 4).must_include(["insert", "remove", "len", "all"]);
|
||||
test("#{ let x = (1, 2, 3); x. }", -2).must_include(["at", "push", "pop"]);
|
||||
test("#().", -1).must_include(["insert", "remove", "len", "all"]);
|
||||
test("#{ let x = (1, 2, 3); x. }", -3).must_include(["at", "push", "pop"]);
|
||||
}
|
||||
|
||||
/// Test that extra space before '.' is handled correctly.
|
||||
#[test]
|
||||
fn test_autocomplete_whitespace() {
|
||||
test("#() .", 5).must_exclude(["insert", "remove", "len", "all"]);
|
||||
test("#{() .}", 6).must_include(["insert", "remove", "len", "all"]);
|
||||
test("#() .a", 6).must_exclude(["insert", "remove", "len", "all"]);
|
||||
test("#{() .a}", 7).must_include(["at", "any", "all"]);
|
||||
test("#() .", -1).must_exclude(["insert", "remove", "len", "all"]);
|
||||
test("#{() .}", -2).must_include(["insert", "remove", "len", "all"]);
|
||||
test("#() .a", -1).must_exclude(["insert", "remove", "len", "all"]);
|
||||
test("#{() .a}", -2).must_include(["at", "any", "all"]);
|
||||
}
|
||||
|
||||
/// Test that the `before_window` doesn't slice into invalid byte
|
||||
/// boundaries.
|
||||
#[test]
|
||||
fn test_autocomplete_before_window_char_boundary() {
|
||||
test("😀😀 #text(font: \"\")", -2);
|
||||
test("😀😀 #text(font: \"\")", -3);
|
||||
}
|
||||
|
||||
/// Ensure that autocompletion for `#cite(|)` completes bibligraphy labels,
|
||||
@ -1655,7 +1644,7 @@ mod tests {
|
||||
let end = world.main.len_bytes();
|
||||
world.main.edit(end..end, " #cite()");
|
||||
|
||||
test_full(&world, &world.main, doc.as_ref(), -1)
|
||||
test_with_doc(&world, -2, doc.as_ref())
|
||||
.must_include(["netwok", "glacier-melt", "supplement"])
|
||||
.must_exclude(["bib"]);
|
||||
}
|
||||
@ -1679,13 +1668,13 @@ mod tests {
|
||||
#[test]
|
||||
fn test_autocomplete_positional_param() {
|
||||
// No string given yet.
|
||||
test("#numbering()", -1).must_include(["string", "integer"]);
|
||||
test("#numbering()", -2).must_include(["string", "integer"]);
|
||||
// String is already given.
|
||||
test("#numbering(\"foo\", )", -1)
|
||||
test("#numbering(\"foo\", )", -2)
|
||||
.must_include(["integer"])
|
||||
.must_exclude(["string"]);
|
||||
// Integer is already given, but numbering is variadic.
|
||||
test("#numbering(\"foo\", 1, )", -1)
|
||||
test("#numbering(\"foo\", 1, )", -2)
|
||||
.must_include(["integer"])
|
||||
.must_exclude(["string"]);
|
||||
}
|
||||
@ -1700,14 +1689,14 @@ mod tests {
|
||||
"#let clrs = (a: red, b: blue); #let nums = (a: 1, b: 2)",
|
||||
);
|
||||
|
||||
test_with_world(&world, -1)
|
||||
test(&world, -2)
|
||||
.must_include(["clrs", "aqua"])
|
||||
.must_exclude(["nums", "a", "b"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_autocomplete_packages() {
|
||||
test("#import \"@\"", -1).must_include([q!("@preview/example:0.1.0")]);
|
||||
test("#import \"@\"", -2).must_include([q!("@preview/example:0.1.0")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -1721,29 +1710,28 @@ mod tests {
|
||||
.with_asset_at("assets/rhino.png", "rhino.png")
|
||||
.with_asset_at("data/example.csv", "example.csv");
|
||||
|
||||
test_with_path(&world, "main.typ", -1)
|
||||
test(&world, -2)
|
||||
.must_include([q!("content/a.typ"), q!("content/b.typ"), q!("utils.typ")])
|
||||
.must_exclude([q!("assets/tiger.jpg")]);
|
||||
|
||||
test_with_path(&world, "content/c.typ", -1)
|
||||
test(&world, ("content/c.typ", -2))
|
||||
.must_include([q!("../main.typ"), q!("a.typ"), q!("b.typ")])
|
||||
.must_exclude([q!("c.typ")]);
|
||||
|
||||
test_with_path(&world, "content/a.typ", -1)
|
||||
test(&world, ("content/a.typ", -2))
|
||||
.must_include([q!("../assets/tiger.jpg"), q!("../assets/rhino.png")])
|
||||
.must_exclude([q!("../data/example.csv"), q!("b.typ")]);
|
||||
|
||||
test_with_path(&world, "content/b.typ", -2)
|
||||
.must_include([q!("../data/example.csv")]);
|
||||
test(&world, ("content/b.typ", -3)).must_include([q!("../data/example.csv")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_autocomplete_figure_snippets() {
|
||||
test("#figure()", -1)
|
||||
test("#figure()", -2)
|
||||
.must_apply("image", "image(\"${}\"),")
|
||||
.must_apply("table", "table(\n ${}\n),");
|
||||
|
||||
test("#figure(cap)", -1).must_apply("caption", "caption: [${}]");
|
||||
test("#figure(cap)", -2).must_apply("caption", "caption: [${}]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -1752,10 +1740,10 @@ mod tests {
|
||||
.with_source("second.typ", "#import \"other.typ\": th")
|
||||
.with_source("other.typ", "#let this = 1; #let that = 2");
|
||||
|
||||
test_with_path(&world, "main.typ", 21)
|
||||
test(&world, ("main.typ", 21))
|
||||
.must_include(["*", "this", "that"])
|
||||
.must_exclude(["figure"]);
|
||||
test_with_path(&world, "second.typ", 23)
|
||||
test(&world, ("second.typ", 23))
|
||||
.must_include(["this", "that"])
|
||||
.must_exclude(["*", "figure"]);
|
||||
}
|
||||
|
@ -86,6 +86,7 @@ pub fn definition(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::borrow::Borrow;
|
||||
use std::ops::Range;
|
||||
|
||||
use typst::foundations::{IntoValue, NativeElement};
|
||||
@ -93,7 +94,7 @@ mod tests {
|
||||
use typst::WorldExt;
|
||||
|
||||
use super::{definition, Definition};
|
||||
use crate::tests::{SourceExt, TestWorld};
|
||||
use crate::tests::{FilePos, TestWorld, WorldLike};
|
||||
|
||||
type Response = (TestWorld, Option<Definition>);
|
||||
|
||||
@ -132,23 +133,19 @@ mod tests {
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
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 doc = typst::compile(&world).output.ok();
|
||||
let source = &world.main;
|
||||
let def = definition(&world, doc.as_ref(), source, source.cursor(cursor), side);
|
||||
(world, def)
|
||||
fn test(world: impl WorldLike, pos: impl FilePos, side: Side) -> Response {
|
||||
let world = world.acquire();
|
||||
let world = world.borrow();
|
||||
let doc = typst::compile(world).output.ok();
|
||||
let (source, cursor) = pos.resolve(world);
|
||||
let def = definition(world, doc.as_ref(), &source, cursor, side);
|
||||
(world.clone(), def)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_definition_let() {
|
||||
test("#let x; #x", 9, Side::After).must_be_at("main.typ", 5..6);
|
||||
test("#let x() = {}; #x", 16, Side::After).must_be_at("main.typ", 5..6);
|
||||
test("#let x; #x", -2, Side::After).must_be_at("main.typ", 5..6);
|
||||
test("#let x() = {}; #x", -2, Side::After).must_be_at("main.typ", 5..6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -158,33 +155,33 @@ mod tests {
|
||||
|
||||
// The span is at the args here because that's what the function value's
|
||||
// span is. Not ideal, but also not too big of a big deal.
|
||||
test_with_world(world, -1, Side::Before).must_be_at("other.typ", 8..11);
|
||||
test(&world, -2, Side::Before).must_be_at("other.typ", 8..11);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_definition_cross_file() {
|
||||
let world = TestWorld::new("#import \"other.typ\": x; #x")
|
||||
.with_source("other.typ", "#let x = 1");
|
||||
test_with_world(world, -1, Side::After).must_be_at("other.typ", 5..6);
|
||||
test(&world, -2, Side::After).must_be_at("other.typ", 5..6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_definition_import() {
|
||||
let world = TestWorld::new("#import \"other.typ\" as o: x")
|
||||
.with_source("other.typ", "#let x = 1");
|
||||
test_with_world(world, 14, Side::Before).must_be_at("other.typ", 0..0);
|
||||
test(&world, 14, Side::Before).must_be_at("other.typ", 0..0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_definition_include() {
|
||||
let world = TestWorld::new("#include \"other.typ\"")
|
||||
.with_source("other.typ", "Hello there");
|
||||
test_with_world(world, 14, Side::Before).must_be_at("other.typ", 0..0);
|
||||
test(&world, 14, Side::Before).must_be_at("other.typ", 0..0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_definition_ref() {
|
||||
test("#figure[] <hi> See @hi", 21, Side::After).must_be_at("main.typ", 1..9);
|
||||
test("#figure[] <hi> See @hi", -2, Side::After).must_be_at("main.typ", 1..9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -182,12 +182,13 @@ mod tests {
|
||||
//! ))
|
||||
//! ```
|
||||
|
||||
use std::borrow::Borrow;
|
||||
use std::num::NonZeroUsize;
|
||||
|
||||
use typst::layout::{Abs, Point, Position};
|
||||
|
||||
use super::{jump_from_click, jump_from_cursor, Jump};
|
||||
use crate::tests::TestWorld;
|
||||
use crate::tests::{FilePos, TestWorld, WorldLike};
|
||||
|
||||
fn point(x: f64, y: f64) -> Point {
|
||||
Point::new(Abs::pt(x), Abs::pt(y))
|
||||
@ -211,10 +212,11 @@ mod tests {
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn test_click(text: &str, click: Point, expected: Option<Jump>) {
|
||||
let world = TestWorld::new(text);
|
||||
let doc = typst::compile(&world).output.unwrap();
|
||||
let jump = jump_from_click(&world, &doc, &doc.pages[0].frame, click);
|
||||
fn test_click(world: impl WorldLike, click: Point, expected: Option<Jump>) {
|
||||
let world = world.acquire();
|
||||
let world = world.borrow();
|
||||
let doc = typst::compile(world).output.unwrap();
|
||||
let jump = jump_from_click(world, &doc, &doc.pages[0].frame, click);
|
||||
if let (Some(Jump::Position(pos)), Some(Jump::Position(expected))) =
|
||||
(&jump, &expected)
|
||||
{
|
||||
@ -227,10 +229,12 @@ mod tests {
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn test_cursor(text: &str, cursor: usize, expected: Option<Position>) {
|
||||
let world = TestWorld::new(text);
|
||||
let doc = typst::compile(&world).output.unwrap();
|
||||
let pos = jump_from_cursor(&doc, &world.main, cursor);
|
||||
fn test_cursor(world: impl WorldLike, pos: impl FilePos, expected: Option<Position>) {
|
||||
let world = world.acquire();
|
||||
let world = world.borrow();
|
||||
let doc = typst::compile(world).output.unwrap();
|
||||
let (source, cursor) = pos.resolve(world);
|
||||
let pos = jump_from_cursor(&doc, &source, cursor);
|
||||
assert_eq!(!pos.is_empty(), expected.is_some());
|
||||
if let (Some(pos), Some(expected)) = (pos.first(), expected) {
|
||||
assert_eq!(pos.page, expected.page);
|
||||
|
@ -266,53 +266,79 @@ pub enum DerefTarget<'a> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::borrow::Borrow;
|
||||
|
||||
use ecow::EcoString;
|
||||
use typst::syntax::{LinkedNode, Side};
|
||||
|
||||
use crate::{named_items, tests::TestWorld};
|
||||
use super::named_items;
|
||||
use crate::tests::{FilePos, WorldLike};
|
||||
|
||||
type Response = Vec<EcoString>;
|
||||
|
||||
trait ResponseExt {
|
||||
fn must_include<'a>(&self, includes: impl IntoIterator<Item = &'a str>) -> &Self;
|
||||
fn must_exclude<'a>(&self, excludes: impl IntoIterator<Item = &'a str>) -> &Self;
|
||||
}
|
||||
|
||||
impl ResponseExt for Response {
|
||||
#[track_caller]
|
||||
fn must_include<'a>(&self, includes: impl IntoIterator<Item = &'a str>) -> &Self {
|
||||
for item in includes {
|
||||
assert!(
|
||||
self.iter().any(|v| v == item),
|
||||
"{item:?} was not contained in {self:?}",
|
||||
);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn must_exclude<'a>(&self, excludes: impl IntoIterator<Item = &'a str>) -> &Self {
|
||||
for item in excludes {
|
||||
assert!(
|
||||
!self.iter().any(|v| v == item),
|
||||
"{item:?} was wrongly contained in {self:?}",
|
||||
);
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn has_named_items(text: &str, cursor: usize, containing: &str) -> bool {
|
||||
let world = TestWorld::new(text);
|
||||
|
||||
let src = world.main.clone();
|
||||
let node = LinkedNode::new(src.root());
|
||||
fn test(world: impl WorldLike, pos: impl FilePos) -> Response {
|
||||
let world = world.acquire();
|
||||
let world = world.borrow();
|
||||
let (source, cursor) = pos.resolve(world);
|
||||
let node = LinkedNode::new(source.root());
|
||||
let leaf = node.leaf_at(cursor, Side::After).unwrap();
|
||||
|
||||
let res = named_items(&world, leaf, |s| {
|
||||
if containing == s.name() {
|
||||
return Some(true);
|
||||
}
|
||||
|
||||
None
|
||||
let mut items = vec![];
|
||||
named_items(world, leaf, |s| {
|
||||
items.push(s.name().clone());
|
||||
None::<()>
|
||||
});
|
||||
|
||||
res.unwrap_or_default()
|
||||
items
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simple_named_items() {
|
||||
// Has named items
|
||||
assert!(has_named_items(r#"#let a = 1;#let b = 2;"#, 8, "a"));
|
||||
assert!(has_named_items(r#"#let a = 1;#let b = 2;"#, 15, "a"));
|
||||
|
||||
// Doesn't have named items
|
||||
assert!(!has_named_items(r#"#let a = 1;#let b = 2;"#, 8, "b"));
|
||||
fn test_named_items_simple() {
|
||||
let s = "#let a = 1;#let b = 2;";
|
||||
test(s, 8).must_include(["a"]).must_exclude(["b"]);
|
||||
test(s, 15).must_include(["b"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_param_named_items() {
|
||||
// Has named items
|
||||
assert!(has_named_items(r#"#let f(a) = 1;#let b = 2;"#, 12, "a"));
|
||||
assert!(has_named_items(r#"#let f(a: b) = 1;#let b = 2;"#, 15, "a"));
|
||||
fn test_named_items_param() {
|
||||
let pos = "#let f(a) = 1;#let b = 2;";
|
||||
test(pos, 12).must_include(["a"]);
|
||||
test(pos, 19).must_include(["b", "f"]).must_exclude(["a"]);
|
||||
|
||||
// Doesn't have named items
|
||||
assert!(!has_named_items(r#"#let f(a) = 1;#let b = 2;"#, 19, "a"));
|
||||
assert!(!has_named_items(r#"#let f(a: b) = 1;#let b = 2;"#, 15, "b"));
|
||||
let named = "#let f(a: b) = 1;#let b = 2;";
|
||||
test(named, 15).must_include(["a", "f"]).must_exclude(["b"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_import_named_items() {
|
||||
// Cannot test much.
|
||||
assert!(has_named_items(r#"#import "foo.typ": a; #(a);"#, 24, "a"));
|
||||
fn test_named_items_import() {
|
||||
test("#import \"foo.typ\": a; #(a);", 2).must_include(["a"]);
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,6 @@
|
||||
use std::borrow::Borrow;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use ecow::EcoString;
|
||||
use typst::diag::{FileError, FileResult};
|
||||
@ -13,10 +15,10 @@ use typst::{Library, World};
|
||||
use crate::IdeWorld;
|
||||
|
||||
/// A world for IDE testing.
|
||||
#[derive(Clone)]
|
||||
pub struct TestWorld {
|
||||
pub main: Source,
|
||||
assets: HashMap<FileId, Bytes>,
|
||||
sources: HashMap<FileId, Source>,
|
||||
files: Arc<TestFiles>,
|
||||
base: &'static TestBase,
|
||||
}
|
||||
|
||||
@ -29,8 +31,7 @@ impl TestWorld {
|
||||
let main = Source::new(Self::main_id(), text.into());
|
||||
Self {
|
||||
main,
|
||||
assets: HashMap::new(),
|
||||
sources: HashMap::new(),
|
||||
files: Arc::new(TestFiles::default()),
|
||||
base: singleton!(TestBase, TestBase::default()),
|
||||
}
|
||||
}
|
||||
@ -39,7 +40,7 @@ impl TestWorld {
|
||||
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);
|
||||
Arc::make_mut(&mut self.files).sources.insert(id, source);
|
||||
self
|
||||
}
|
||||
|
||||
@ -55,7 +56,7 @@ impl TestWorld {
|
||||
let id = FileId::new(None, VirtualPath::new(path));
|
||||
let data = typst_dev_assets::get_by_name(filename).unwrap();
|
||||
let bytes = Bytes::from_static(data);
|
||||
self.assets.insert(id, bytes);
|
||||
Arc::make_mut(&mut self.files).assets.insert(id, bytes);
|
||||
self
|
||||
}
|
||||
|
||||
@ -81,7 +82,7 @@ impl World for TestWorld {
|
||||
fn source(&self, id: FileId) -> FileResult<Source> {
|
||||
if id == self.main.id() {
|
||||
Ok(self.main.clone())
|
||||
} else if let Some(source) = self.sources.get(&id) {
|
||||
} else if let Some(source) = self.files.sources.get(&id) {
|
||||
Ok(source.clone())
|
||||
} else {
|
||||
Err(FileError::NotFound(id.vpath().as_rootless_path().into()))
|
||||
@ -89,7 +90,7 @@ impl World for TestWorld {
|
||||
}
|
||||
|
||||
fn file(&self, id: FileId) -> FileResult<Bytes> {
|
||||
match self.assets.get(&id) {
|
||||
match self.files.assets.get(&id) {
|
||||
Some(bytes) => Ok(bytes.clone()),
|
||||
None => Err(FileError::NotFound(id.vpath().as_rootless_path().into())),
|
||||
}
|
||||
@ -111,8 +112,8 @@ impl IdeWorld for TestWorld {
|
||||
|
||||
fn files(&self) -> Vec<FileId> {
|
||||
std::iter::once(self.main.id())
|
||||
.chain(self.sources.keys().copied())
|
||||
.chain(self.assets.keys().copied())
|
||||
.chain(self.files.sources.keys().copied())
|
||||
.chain(self.files.assets.keys().copied())
|
||||
.collect()
|
||||
}
|
||||
|
||||
@ -133,20 +134,11 @@ impl IdeWorld for TestWorld {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
}
|
||||
/// Test-specific files.
|
||||
#[derive(Default, Clone)]
|
||||
struct TestFiles {
|
||||
assets: HashMap<FileId, Bytes>,
|
||||
sources: HashMap<FileId, Source>,
|
||||
}
|
||||
|
||||
/// Shared foundation of all test worlds.
|
||||
@ -186,3 +178,58 @@ fn library() -> Library {
|
||||
lib.styles.set(TextElem::set_size(TextSize(Abs::pt(10.0).into())));
|
||||
lib
|
||||
}
|
||||
|
||||
/// The input to a test: Either just a string or a full `TestWorld`.
|
||||
pub trait WorldLike {
|
||||
type World: Borrow<TestWorld>;
|
||||
|
||||
fn acquire(self) -> Self::World;
|
||||
}
|
||||
|
||||
impl<'a> WorldLike for &'a TestWorld {
|
||||
type World = &'a TestWorld;
|
||||
|
||||
fn acquire(self) -> Self::World {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl WorldLike for &str {
|
||||
type World = TestWorld;
|
||||
|
||||
fn acquire(self) -> Self::World {
|
||||
TestWorld::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// Specifies a position in a file for a test.
|
||||
pub trait FilePos {
|
||||
fn resolve(self, world: &TestWorld) -> (Source, usize);
|
||||
}
|
||||
|
||||
impl FilePos for isize {
|
||||
#[track_caller]
|
||||
fn resolve(self, world: &TestWorld) -> (Source, usize) {
|
||||
(world.main.clone(), cursor(&world.main, self))
|
||||
}
|
||||
}
|
||||
|
||||
impl FilePos for (&str, isize) {
|
||||
#[track_caller]
|
||||
fn resolve(self, world: &TestWorld) -> (Source, usize) {
|
||||
let id = FileId::new(None, VirtualPath::new(self.0));
|
||||
let source = world.source(id).unwrap();
|
||||
let cursor = cursor(&source, self.1);
|
||||
(source, cursor)
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve a signed index (negative from the back) to a unsigned index.
|
||||
#[track_caller]
|
||||
fn cursor(source: &Source, cursor: isize) -> usize {
|
||||
if cursor < 0 {
|
||||
source.len_bytes().checked_add_signed(cursor + 1).unwrap()
|
||||
} else {
|
||||
cursor as usize
|
||||
}
|
||||
}
|
||||
|
@ -274,10 +274,12 @@ fn font_tooltip(world: &dyn IdeWorld, leaf: &LinkedNode) -> Option<Tooltip> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::borrow::Borrow;
|
||||
|
||||
use typst::syntax::Side;
|
||||
|
||||
use super::{tooltip, Tooltip};
|
||||
use crate::tests::{SourceExt, TestWorld};
|
||||
use crate::tests::{FilePos, TestWorld, WorldLike};
|
||||
|
||||
type Response = Option<Tooltip>;
|
||||
|
||||
@ -308,21 +310,17 @@ mod tests {
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
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();
|
||||
tooltip(world, doc.as_ref(), source, source.cursor(cursor), side)
|
||||
fn test(world: impl WorldLike, pos: impl FilePos, side: Side) -> Response {
|
||||
let world = world.acquire();
|
||||
let world = world.borrow();
|
||||
let (source, cursor) = pos.resolve(world);
|
||||
let doc = typst::compile(world).output.ok();
|
||||
tooltip(world, doc.as_ref(), &source, cursor, side)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tooltip() {
|
||||
test("#let x = 1 + 2", 14, Side::After).must_be_none();
|
||||
test("#let x = 1 + 2", -1, 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");
|
||||
@ -330,7 +328,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_tooltip_empty_contextual() {
|
||||
test("#{context}", 10, Side::Before).must_be_code("context()");
|
||||
test("#{context}", -1, Side::Before).must_be_code("context()");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -358,8 +356,7 @@ mod tests {
|
||||
fn test_tooltip_star_import() {
|
||||
let world = TestWorld::new("#import \"other.typ\": *")
|
||||
.with_source("other.typ", "#let (a, b, c) = (1, 2, 3)");
|
||||
test_with_world(&world, 21, Side::Before).must_be_none();
|
||||
test_with_world(&world, 21, Side::After)
|
||||
.must_be_text("This star imports `a`, `b`, and `c`");
|
||||
test(&world, -2, Side::Before).must_be_none();
|
||||
test(&world, -2, Side::After).must_be_text("This star imports `a`, `b`, and `c`");
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user