diff --git a/crates/typst-ide/src/complete.rs b/crates/typst-ide/src/complete.rs index 699ce573f..f7a404d7d 100644 --- a/crates/typst-ide/src/complete.rs +++ b/crates/typst-ide/src/complete.rs @@ -1050,7 +1050,7 @@ impl<'a> CompletionContext<'a> { /// A small window of context before the cursor. fn before_window(&self, size: usize) -> &str { - Scanner::new(self.before).from(self.cursor.saturating_sub(size)) + Scanner::new(self.before).get(self.cursor.saturating_sub(size)..self.cursor) } /// Add a prefix and suffix to all applications. @@ -1455,4 +1455,23 @@ mod tests { fn test_autocomplete_before_window_char_boundary() { test("😀😀 #text(font: \"\")", -2); } + + /// Ensure that autocompletion for `#cite(|)` completes bibligraphy labels, + /// but no other labels. + #[test] + fn test_autocomplete_cite_function() { + // First compile a working file to get a document. + let mut world = TestWorld::new("#bibliography(\"works.bib\") ") + .with_asset_by_name("works.bib"); + let doc = typst::compile(&world).output.ok(); + + // Then, add the invalid `#cite` call. Had the document been invalid + // initially, we would have no populated document to autocomplete with. + let end = world.main.len_bytes(); + world.main.edit(end..end, " #cite()"); + + test_with_world_and_doc(&world, 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 3a192b567..90fc125e7 100644 --- a/crates/typst-ide/src/lib.rs +++ b/crates/typst-ide/src/lib.rs @@ -97,6 +97,8 @@ fn summarize_font_family<'a>(variants: impl Iterator) -> Ec #[cfg(test)] mod tests { + use std::collections::HashMap; + use typst::diag::{FileError, FileResult}; use typst::foundations::{Bytes, Datetime, Smart}; use typst::layout::{Abs, Margin, PageElem}; @@ -108,6 +110,7 @@ mod tests { /// A world for IDE testing. pub struct TestWorld { pub main: Source, + assets: HashMap, base: &'static TestBase, } @@ -120,10 +123,21 @@ mod tests { let main = Source::new(Self::main_id(), text.into()); Self { main, + assets: HashMap::new(), base: singleton!(TestBase, TestBase::default()), } } + /// Add an additional 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)); + let data = typst_dev_assets::get_by_name(filename).unwrap(); + let bytes = Bytes::from_static(data); + self.assets.insert(id, bytes); + self + } + /// The ID of the main file in a `TestWorld`. pub fn main_id() -> FileId { *singleton!(FileId, FileId::new(None, VirtualPath::new("main.typ"))) @@ -152,7 +166,10 @@ mod tests { } fn file(&self, id: FileId) -> FileResult { - Err(FileError::NotFound(id.vpath().as_rootless_path().into())) + match self.assets.get(&id) { + Some(bytes) => Ok(bytes.clone()), + None => Err(FileError::NotFound(id.vpath().as_rootless_path().into())), + } } fn font(&self, index: usize) -> Option {