diff --git a/Cargo.lock b/Cargo.lock index a0abfdfbe..866bd1b86 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3286,7 +3286,6 @@ dependencies = [ name = "typst-syntax" version = "0.13.1" dependencies = [ - "comemo", "ecow", "serde", "toml", diff --git a/Cargo.toml b/Cargo.toml index 792573f98..ce6c97f7a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -160,6 +160,7 @@ strip = true [workspace.lints.clippy] blocks_in_conditions = "allow" comparison_chain = "allow" +iter_over_hash_type = "warn" manual_range_contains = "allow" mutable_key_type = "allow" uninlined_format_args = "warn" diff --git a/crates/typst-cli/src/watch.rs b/crates/typst-cli/src/watch.rs index 765f86808..151c0ca1d 100644 --- a/crates/typst-cli/src/watch.rs +++ b/crates/typst-cli/src/watch.rs @@ -139,6 +139,7 @@ impl Watcher { fn update(&mut self, iter: impl IntoIterator) -> StrResult<()> { // Mark all files as not "seen" so that we may unwatch them if they // aren't in the dependency list. + #[allow(clippy::iter_over_hash_type, reason = "order does not matter")] for seen in self.watched.values_mut() { *seen = false; } diff --git a/crates/typst-cli/src/world.rs b/crates/typst-cli/src/world.rs index 42083ef20..34ebda64b 100644 --- a/crates/typst-cli/src/world.rs +++ b/crates/typst-cli/src/world.rs @@ -173,6 +173,7 @@ impl SystemWorld { /// Reset the compilation state in preparation of a new compilation. pub fn reset(&mut self) { + #[allow(clippy::iter_over_hash_type, reason = "order does not matter")] for slot in self.slots.get_mut().values_mut() { slot.reset(); } diff --git a/crates/typst-ide/src/complete.rs b/crates/typst-ide/src/complete.rs index d2c1040b4..96fd7f265 100644 --- a/crates/typst-ide/src/complete.rs +++ b/crates/typst-ide/src/complete.rs @@ -709,9 +709,11 @@ fn complete_params(ctx: &mut CompletionContext) -> bool { return true; } - // Parameters: "func(|)", "func(hi|)", "func(12,|)". + // Parameters: "func(|)", "func(hi|)", "func(12, |)", "func(12,|)" [explicit mode only] if let SyntaxKind::LeftParen | SyntaxKind::Comma = deciding.kind() - && (deciding.kind() != SyntaxKind::Comma || deciding.range().end < ctx.cursor) + && (deciding.kind() != SyntaxKind::Comma + || deciding.range().end < ctx.cursor + || ctx.explicit) { if let Some(next) = deciding.next_leaf() { ctx.from = ctx.cursor.min(next.offset()); @@ -891,7 +893,10 @@ fn complete_code(ctx: &mut CompletionContext) -> bool { } // An existing identifier: "{ pa| }". - if ctx.leaf.kind() == SyntaxKind::Ident { + // Ignores named pair keys as they are not variables (as in "(pa|: 23)"). + if ctx.leaf.kind() == SyntaxKind::Ident + && (ctx.leaf.index() > 0 || ctx.leaf.parent_kind() != Some(SyntaxKind::Named)) + { ctx.from = ctx.leaf.offset(); code_completions(ctx, false); return true; @@ -904,11 +909,19 @@ fn complete_code(ctx: &mut CompletionContext) -> bool { return true; } - // Anywhere: "{ | }". - // But not within or after an expression. + // Anywhere: "{ | }", "(|)", "(1,|)", "(a:|)". + // But not within or after an expression, and also not part of a dictionary + // key (as in "(pa: |,)") if ctx.explicit + && ctx.leaf.parent_kind() != Some(SyntaxKind::Dict) && (ctx.leaf.kind().is_trivia() - || matches!(ctx.leaf.kind(), SyntaxKind::LeftParen | SyntaxKind::LeftBrace)) + || matches!( + ctx.leaf.kind(), + SyntaxKind::LeftParen + | SyntaxKind::LeftBrace + | SyntaxKind::Comma + | SyntaxKind::Colon + )) { ctx.from = ctx.cursor; code_completions(ctx, false); @@ -1560,6 +1573,7 @@ mod tests { trait ResponseExt { fn completions(&self) -> &[Completion]; fn labels(&self) -> BTreeSet<&str>; + fn must_be_empty(&self) -> &Self; fn must_include<'a>(&self, includes: impl IntoIterator) -> &Self; fn must_exclude<'a>(&self, excludes: impl IntoIterator) -> &Self; fn must_apply<'a>(&self, label: &str, apply: impl Into>) @@ -1578,6 +1592,16 @@ mod tests { self.completions().iter().map(|c| c.label.as_str()).collect() } + #[track_caller] + fn must_be_empty(&self) -> &Self { + let labels = self.labels(); + assert!( + labels.is_empty(), + "expected no suggestions (got {labels:?} instead)" + ); + self + } + #[track_caller] fn must_include<'a>(&self, includes: impl IntoIterator) -> &Self { let labels = self.labels(); @@ -1622,7 +1646,15 @@ mod tests { let world = world.acquire(); let world = world.borrow(); let doc = typst::compile(world).output.ok(); - test_with_doc(world, pos, doc.as_ref()) + test_with_doc(world, pos, doc.as_ref(), true) + } + + #[track_caller] + fn test_implicit(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(), false) } #[track_caller] @@ -1635,7 +1667,7 @@ mod tests { let doc = typst::compile(&world).output.ok(); let end = world.main.text().len(); world.main.edit(end..end, addition); - test_with_doc(&world, pos, doc.as_ref()) + test_with_doc(&world, pos, doc.as_ref(), true) } #[track_caller] @@ -1643,11 +1675,12 @@ mod tests { world: impl WorldLike, pos: impl FilePos, doc: Option<&PagedDocument>, + explicit: bool, ) -> Response { let world = world.acquire(); let world = world.borrow(); let (source, cursor) = pos.resolve(world); - autocomplete(world, doc, &source, cursor, true) + autocomplete(world, doc, &source, cursor, explicit) } #[test] @@ -1698,7 +1731,7 @@ mod tests { let end = world.main.text().len(); world.main.edit(end..end, " #cite()"); - test_with_doc(&world, -2, doc.as_ref()) + test_with_doc(&world, -2, doc.as_ref(), true) .must_include(["netwok", "glacier-melt", "supplement"]) .must_exclude(["bib"]); } @@ -1853,26 +1886,105 @@ mod tests { #[test] fn test_autocomplete_fonts() { test("#text(font:)", -2) - .must_include(["\"Libertinus Serif\"", "\"New Computer Modern Math\""]); + .must_include([q!("Libertinus Serif"), q!("New Computer Modern Math")]); test("#show link: set text(font: )", -2) - .must_include(["\"Libertinus Serif\"", "\"New Computer Modern Math\""]); + .must_include([q!("Libertinus Serif"), q!("New Computer Modern Math")]); test("#show math.equation: set text(font: )", -2) - .must_include(["\"New Computer Modern Math\""]) - .must_exclude(["\"Libertinus Serif\""]); + .must_include([q!("New Computer Modern Math")]) + .must_exclude([q!("Libertinus Serif")]); test("#show math.equation: it => { set text(font: )\nit }", -7) - .must_include(["\"New Computer Modern Math\""]) - .must_exclude(["\"Libertinus Serif\""]); + .must_include([q!("New Computer Modern Math")]) + .must_exclude([q!("Libertinus Serif")]); } #[test] fn test_autocomplete_typed_html() { test("#html.div(translate: )", -2) .must_include(["true", "false"]) - .must_exclude(["\"yes\"", "\"no\""]); + .must_exclude([q!("yes"), q!("no")]); test("#html.input(value: )", -2).must_include(["float", "string", "red", "blue"]); - test("#html.div(role: )", -2).must_include(["\"alertdialog\""]); + test("#html.div(role: )", -2).must_include([q!("alertdialog")]); + } + + #[test] + fn test_autocomplete_in_function_params_after_comma_and_colon() { + let document = "#text(size: 12pt, [])"; + + // After colon + test(document, 11).must_include(["length"]); + test_implicit(document, 11).must_include(["length"]); + + test(document, 12).must_include(["length"]); + test_implicit(document, 12).must_include(["length"]); + + // After comma + test(document, 17).must_include(["font"]); + test_implicit(document, 17).must_be_empty(); + + test(document, 18).must_include(["font"]); + test_implicit(document, 18).must_include(["font"]); + } + + #[test] + fn test_autocomplete_in_list_literal() { + let document = "#let val = 0\n#(1, \"one\")"; + + // After opening paren + test(document, 15).must_include(["color", "val"]); + test_implicit(document, 15).must_be_empty(); + + // After first element + test(document, 16).must_be_empty(); + test_implicit(document, 16).must_be_empty(); + + // After comma + test(document, 17).must_include(["color", "val"]); + test_implicit(document, 17).must_be_empty(); + + test(document, 18).must_include(["color", "val"]); + test_implicit(document, 18).must_be_empty(); + } + + #[test] + fn test_autocomplete_in_dict_literal() { + let document = "#let first = 0\n#(first: 1, second: one)"; + + // After opening paren + test(document, 17).must_be_empty(); + test_implicit(document, 17).must_be_empty(); + + // After first key + test(document, 22).must_be_empty(); + test_implicit(document, 22).must_be_empty(); + + // After colon + test(document, 23).must_include(["align", "first"]); + test_implicit(document, 23).must_be_empty(); + + test(document, 24).must_include(["align", "first"]); + test_implicit(document, 24).must_be_empty(); + + // After first value + test(document, 25).must_be_empty(); + test_implicit(document, 25).must_be_empty(); + + // After comma + test(document, 26).must_be_empty(); + test_implicit(document, 26).must_be_empty(); + + test(document, 27).must_be_empty(); + test_implicit(document, 27).must_be_empty(); + } + + #[test] + fn test_autocomplete_in_destructuring() { + let document = "#let value = 20\n#let (va: value) = (va: 10)"; + + // At destructuring rename pattern source + test(document, 24).must_be_empty(); + test_implicit(document, 24).must_be_empty(); } } diff --git a/crates/typst-library/src/diag.rs b/crates/typst-library/src/diag.rs index 2b9ff6376..90686b25f 100644 --- a/crates/typst-library/src/diag.rs +++ b/crates/typst-library/src/diag.rs @@ -234,18 +234,23 @@ impl From for SourceDiagnostic { /// Destination for a deprecation message when accessing a deprecated value. pub trait DeprecationSink { - /// Emits the given deprecation message into this sink. - fn emit(self, message: &str); + /// Emits the given deprecation message into this sink alongside a version + /// in which the deprecated item is planned to be removed. + fn emit(self, message: &str, until: Option<&str>); } impl DeprecationSink for () { - fn emit(self, _: &str) {} + fn emit(self, _: &str, _: Option<&str>) {} } impl DeprecationSink for (&mut Engine<'_>, Span) { /// Emits the deprecation message as a warning. - fn emit(self, message: &str) { - self.0.sink.warn(SourceDiagnostic::warning(self.1, message)); + fn emit(self, message: &str, version: Option<&str>) { + self.0 + .sink + .warn(SourceDiagnostic::warning(self.1, message).with_hints( + version.map(|v| eco_format!("it will be removed in Typst {}", v)), + )); } } diff --git a/crates/typst-library/src/foundations/scope.rs b/crates/typst-library/src/foundations/scope.rs index 551e3dd78..18d972d33 100644 --- a/crates/typst-library/src/foundations/scope.rs +++ b/crates/typst-library/src/foundations/scope.rs @@ -253,8 +253,8 @@ pub struct Binding { span: Span, /// The category of the binding. category: Option, - /// A deprecation message for the definition. - deprecation: Option<&'static str>, + /// The deprecation information if this item is deprecated. + deprecation: Option>, } /// The different kinds of slots. @@ -284,8 +284,8 @@ impl Binding { } /// Marks this binding as deprecated, with the given `message`. - pub fn deprecated(&mut self, message: &'static str) -> &mut Self { - self.deprecation = Some(message); + pub fn deprecated(&mut self, deprecation: Deprecation) -> &mut Self { + self.deprecation = Some(Box::new(deprecation)); self } @@ -300,8 +300,8 @@ impl Binding { /// - pass `()` to ignore the message. /// - pass `(&mut engine, span)` to emit a warning into the engine. pub fn read_checked(&self, sink: impl DeprecationSink) -> &Value { - if let Some(message) = self.deprecation { - sink.emit(message); + if let Some(info) = &self.deprecation { + sink.emit(info.message, info.until); } &self.value } @@ -337,8 +337,8 @@ impl Binding { } /// A deprecation message for the value, if any. - pub fn deprecation(&self) -> Option<&'static str> { - self.deprecation + pub fn deprecation(&self) -> Option<&Deprecation> { + self.deprecation.as_deref() } /// The category of the value, if any. @@ -356,6 +356,51 @@ pub enum Capturer { Context, } +/// Information about a deprecated binding. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct Deprecation { + /// A deprecation message for the definition. + message: &'static str, + /// A version in which the deprecated binding is planned to be removed. + until: Option<&'static str>, +} + +impl Deprecation { + /// Creates new deprecation info with a default message to display when + /// emitting the deprecation warning. + pub fn new() -> Self { + Self { message: "item is deprecated", until: None } + } + + /// Set the message to display when emitting the deprecation warning. + pub fn with_message(mut self, message: &'static str) -> Self { + self.message = message; + self + } + + /// Set the version in which the binding is planned to be removed. + pub fn with_until(mut self, version: &'static str) -> Self { + self.until = Some(version); + self + } + + /// The message to display when emitting the deprecation warning. + pub fn message(&self) -> &'static str { + self.message + } + + /// The version in which the binding is planned to be removed. + pub fn until(&self) -> Option<&'static str> { + self.until + } +} + +impl Default for Deprecation { + fn default() -> Self { + Self::new() + } +} + /// The error message when trying to mutate a variable from the standard /// library. #[cold] diff --git a/crates/typst-library/src/foundations/symbol.rs b/crates/typst-library/src/foundations/symbol.rs index 898068afb..c3a13d07c 100644 --- a/crates/typst-library/src/foundations/symbol.rs +++ b/crates/typst-library/src/foundations/symbol.rs @@ -151,7 +151,7 @@ impl Symbol { modifiers.best_match_in(list.variants().map(|(m, _, d)| (m, d))) { if let Some(message) = deprecation { - sink.emit(message) + sink.emit(message, None) } return Ok(self); } diff --git a/crates/typst-library/src/introspection/counter.rs b/crates/typst-library/src/introspection/counter.rs index f6083303d..c7d26040c 100644 --- a/crates/typst-library/src/introspection/counter.rs +++ b/crates/typst-library/src/introspection/counter.rs @@ -412,9 +412,11 @@ impl Counter { /// - If it is a string, creates a custom counter that is only affected /// by manual updates, /// - If it is the [`page`] function, counts through pages, - /// - If it is a [selector], counts through elements that matches with the + /// - If it is a [selector], counts through elements that match the /// selector. For example, /// - provide an element function: counts elements of that type, + /// - provide a [`where`]($function.where) selector: + /// counts a type of element with specific fields, /// - provide a [`{