1use std::cmp::Reverse;
2use std::collections::BTreeMap;
3
4use ecow::{EcoString, eco_format};
5use rustc_hash::FxHashSet;
6use serde::{Deserialize, Serialize};
7use typst::foundations::{
8 AsOutput, AutoValue, CastInfo, Func, Label, NativeElement, NoneValue, Output,
9 ParamInfo, Repr, StyleChain, Styles, Type, Value, fields_on, repr,
10};
11use typst::layout::{Alignment, Dir};
12use typst::syntax::ast::AstNode;
13use typst::syntax::{
14 FileId, LinkedNode, Side, Source, SyntaxKind, SyntaxMode, ast, is_id_continue,
15 is_id_start, is_ident,
16};
17use typst::text::{FontFlags, RawElem};
18use typst::visualize::Color;
19use unscanny::Scanner;
20
21use crate::analyze::analyze_expr_with_fallback;
22use crate::docs::{find_param_docs, find_value_docs};
23use crate::utils::{check_value_recursively, globals, summarize_font_family};
24use crate::{IdeWorld, analyze_expr, analyze_import, analyze_labels, named_items};
25
26pub fn autocomplete(
38 world: &dyn IdeWorld,
39 output: Option<impl AsOutput>,
40 source: &Source,
41 cursor: usize,
42 explicit: bool,
43) -> Option<(usize, Vec<Completion>)> {
44 let leaf = LinkedNode::new(source.root()).leaf_at(cursor, Side::Before)?;
45 let mut ctx = CompletionContext::new(
46 world,
47 output.as_ref().map(|v| v.as_output()),
48 source,
49 &leaf,
50 cursor,
51 explicit,
52 );
53
54 let mode = ctx.leaf.mode_after()?;
56
57 _ = complete_field_accesses(&mut ctx)
58 || complete_open_labels(&mut ctx)
59 || complete_imports(&mut ctx)
60 || complete_rules(&mut ctx)
61 || complete_params(&mut ctx)
62 || match mode {
64 SyntaxMode::Markup => complete_markup(&mut ctx),
65 SyntaxMode::Math => complete_math(&mut ctx),
66 SyntaxMode::Code => complete_code(&mut ctx),
67 };
68
69 Some((ctx.from, ctx.completions))
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct Completion {
75 pub kind: CompletionKind,
77 pub label: EcoString,
79 pub apply: Option<EcoString>,
84 pub detail: Option<EcoString>,
86}
87
88#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
90#[serde(rename_all = "kebab-case")]
91pub enum CompletionKind {
92 Syntax,
94 Func,
96 Type,
98 Param,
100 Constant,
102 Path,
104 Package,
106 Label,
108 Font,
110 Symbol(EcoString),
112}
113
114fn complete_field_accesses(ctx: &mut CompletionContext) -> bool {
116 let (after_dot, textual_dot) = match ctx.leaf.kind() {
117 SyntaxKind::Dot => (true, false),
118 SyntaxKind::Text | SyntaxKind::MathText if ctx.leaf.leaf_text() == "." => {
119 (true, true)
120 }
121 _ => (false, false),
122 };
123
124 if after_dot
126 && let Some(prev) = ctx.leaf.prev_sibling()
127 && (!textual_dot || prev.range().end == ctx.leaf.range().start)
130 && prev.is::<ast::Expr>() && (prev.parent_kind() != Some(SyntaxKind::Markup)
133 || prev.prev_sibling_kind() == Some(SyntaxKind::Hash))
134 && let Some((value, styles)) = analyze_expr(ctx.world, &prev).into_iter().next()
135 {
136 ctx.from = ctx.cursor;
137 field_access_completions(ctx, &value, &styles);
138 return true;
139 }
140
141 if matches!(ctx.leaf.kind(), SyntaxKind::Ident | SyntaxKind::MathIdent)
143 && let Some(prev) = ctx.leaf.prev_sibling()
144 && prev.kind() == SyntaxKind::Dot
145 && let Some(prev_prev) = prev.prev_sibling()
146 && prev_prev.is::<ast::Expr>()
147 && let Some((value, styles)) =
148 analyze_expr(ctx.world, &prev_prev).into_iter().next()
149 {
150 debug_assert!(matches!(
151 ctx.leaf.parent_kind(),
152 Some(SyntaxKind::FieldAccess | SyntaxKind::MathFieldAccess),
153 ));
154 ctx.from = ctx.leaf.offset();
155 field_access_completions(ctx, &value, &styles);
156 return true;
157 }
158
159 false
160}
161
162fn field_access_completions(
164 ctx: &mut CompletionContext,
165 value: &Value,
166 styles: &Option<Styles>,
167) {
168 let scopes = {
169 let ty = value.ty().scope();
170 let elem = match value {
171 Value::Content(content) => Some(content.elem().scope()),
172 _ => None,
173 };
174 elem.into_iter().chain(Some(ty))
175 };
176
177 for (name, binding) in scopes.flat_map(|scope| scope.iter()) {
180 let Ok(func) = binding.read().clone().cast::<Func>() else { continue };
181 if let Some(param) = func.params().next()
182 && param.name() == Some("self")
183 {
184 ctx.call_completion(name.clone(), binding.read());
185 }
186 }
187
188 if let Some(scope) = value.scope() {
189 for (name, binding) in scope.iter() {
190 ctx.call_completion(name.clone(), binding.read());
191 }
192 }
193
194 for &field in fields_on(value.ty()) {
195 ctx.value_completion(field, &value.field(field, ()).unwrap());
201 }
202
203 match value {
204 Value::Symbol(symbol) => {
205 for modifier in symbol.modifiers() {
206 if let Ok(modified) = symbol.clone().modified((), modifier) {
207 ctx.completions.push(Completion {
208 kind: CompletionKind::Symbol(modified.get().into()),
209 label: modifier.into(),
210 apply: None,
211 detail: None,
212 });
213 }
214 }
215 }
216 Value::Content(content) => {
217 for (name, value) in content.fields() {
218 ctx.value_completion(name, &value);
219 }
220 }
221 Value::Dict(dict) => {
222 for (name, value) in dict.iter() {
223 ctx.value_completion(name.clone(), value);
224 }
225 }
226 Value::Args(args) => {
227 for (name, value) in args.to_named().iter() {
228 ctx.value_completion(name.clone(), value);
229 }
230 }
231 Value::Func(func) => {
232 if let Some((elem, styles)) = func.to_element().zip(styles.as_ref()) {
234 for param in elem.params().iter().filter(|param| !param.required) {
235 if let Some(value) = elem.field_id(param.name).and_then(|id| {
236 elem.field_from_styles(id, StyleChain::new(styles)).ok()
237 }) {
238 ctx.value_completion(param.name, &value);
239 }
240 }
241 }
242 }
243 _ => {}
244 }
245}
246
247fn complete_open_labels(ctx: &mut CompletionContext) -> bool {
249 if ctx.leaf.kind().is_error() && ctx.leaf.leaf_text().starts_with('<') {
251 ctx.from = ctx.leaf.offset() + 1;
252 ctx.label_completions();
253 return true;
254 }
255
256 false
257}
258
259fn complete_imports(ctx: &mut CompletionContext) -> bool {
261 if let Some(SyntaxKind::ModuleImport | SyntaxKind::ModuleInclude) =
264 ctx.leaf.parent_kind()
265 && let Some(ast::Expr::Str(str)) = ctx.leaf.cast()
266 {
267 let value = str.get();
268 ctx.from = ctx.leaf.offset();
269 if value.starts_with('@') {
270 let all_versions = value.contains(':');
271 ctx.package_completions(all_versions);
272 } else {
273 ctx.file_completions_with_extensions(&["typ"]);
274 }
275 return true;
276 }
277
278 if let Some(prev) = ctx.leaf.prev_sibling()
282 && let Some(ast::Expr::ModuleImport(import)) = prev.get().cast()
283 && let Some(ast::Imports::Items(items)) = import.imports()
284 && let Some(source) = prev.children().find(|child| child.is::<ast::Expr>())
285 {
286 ctx.from = ctx.cursor;
287 import_item_completions(ctx, items, &source);
288 return true;
289 }
290
291 if ctx.leaf.kind() == SyntaxKind::Ident
294 && let Some(parent) = ctx.leaf.parent()
295 && parent.kind() == SyntaxKind::ImportItemPath
296 && let Some(grand) = parent.parent()
297 && grand.kind() == SyntaxKind::ImportItems
298 && let Some(great) = grand.parent()
299 && let Some(ast::Expr::ModuleImport(import)) = great.get().cast()
300 && let Some(ast::Imports::Items(items)) = import.imports()
301 && let Some(source) = great.children().find(|child| child.is::<ast::Expr>())
302 {
303 ctx.from = ctx.leaf.offset();
304 import_item_completions(ctx, items, &source);
305 return true;
306 }
307
308 false
309}
310
311fn import_item_completions<'a>(
313 ctx: &mut CompletionContext<'a>,
314 existing: ast::ImportItems<'a>,
315 source: &LinkedNode,
316) {
317 let Some(value) = analyze_import(ctx.world, source) else { return };
318 let Some(scope) = value.scope() else { return };
319
320 if existing.iter().next().is_none() {
321 ctx.snippet_completion("*", "*", "Import everything.");
322 }
323
324 for (name, binding) in scope.iter() {
325 if existing.iter().all(|item| item.original_name().as_str() != name) {
326 ctx.value_completion(name.clone(), binding.read());
327 }
328 }
329}
330
331fn complete_rules(ctx: &mut CompletionContext) -> bool {
333 if !ctx.leaf.kind().is_trivia() {
335 return false;
336 }
337
338 let Some(prev) = ctx.leaf.prev_leaf() else { return false };
339
340 if matches!(prev.kind(), SyntaxKind::Set) {
342 ctx.from = ctx.cursor;
343 set_rule_completions(ctx);
344 return true;
345 }
346
347 if matches!(prev.kind(), SyntaxKind::Show) {
349 ctx.from = ctx.cursor;
350 show_rule_selector_completions(ctx);
351 return true;
352 }
353
354 if let Some(prev) = ctx.leaf.prev_leaf()
356 && matches!(prev.kind(), SyntaxKind::Colon)
357 && matches!(prev.parent_kind(), Some(SyntaxKind::ShowRule))
358 {
359 ctx.from = ctx.cursor;
360 show_rule_recipe_completions(ctx);
361 return true;
362 }
363
364 false
365}
366
367fn set_rule_completions(ctx: &mut CompletionContext) {
369 ctx.scope_completions(true, |value| {
370 matches!(
371 value,
372 Value::Func(func) if func.params().any(|param| param.settable())
373 )
374 });
375}
376
377fn show_rule_selector_completions(ctx: &mut CompletionContext) {
379 ctx.scope_completions(
380 false,
381 |value| matches!(value, Value::Func(func) if func.to_element().is_some()),
382 );
383
384 ctx.enrich("", ": ");
385
386 ctx.snippet_completion(
387 "text selector",
388 "\"${text}\": ${}",
389 "Replace occurrences of specific text.",
390 );
391
392 ctx.snippet_completion(
393 "regex selector",
394 "regex(\"${regex}\"): ${}",
395 "Replace matches of a regular expression.",
396 );
397}
398
399fn show_rule_recipe_completions(ctx: &mut CompletionContext) {
401 ctx.snippet_completion(
402 "replacement",
403 "[${content}]",
404 "Replace the selected element with content.",
405 );
406
407 ctx.snippet_completion(
408 "replacement (string)",
409 "\"${text}\"",
410 "Replace the selected element with a string of text.",
411 );
412
413 ctx.snippet_completion(
414 "transformation",
415 "element => [${content}]",
416 "Transform the element with a function.",
417 );
418
419 ctx.scope_completions(false, |value| matches!(value, Value::Func(_)));
420}
421
422fn complete_params(ctx: &mut CompletionContext) -> bool {
429 let (callee, set, args, args_linked) = if let Some(parent) = ctx.leaf.parent()
431 && let Some(parent) = match parent.kind() {
432 SyntaxKind::Named => parent.parent(),
433 _ => Some(parent),
434 }
435 && let Some(args) = parent.get().cast::<ast::Args>()
436 && let Some(grand) = parent.parent()
437 && let Some(expr) = grand.get().cast::<ast::Expr>()
438 && let set = matches!(expr, ast::Expr::SetRule(_))
439 && let Some(callee) = match expr {
440 ast::Expr::FuncCall(call) => Some(call.callee()),
441 ast::Expr::SetRule(set) => Some(set.target()),
442 _ => None,
443 }
444 && let Some(callee) = grand.find(callee.span())
445 {
446 (callee, set, args, parent)
447 } else {
448 return false;
449 };
450
451 let mut deciding = ctx.leaf.clone();
453 while !matches!(
454 deciding.kind(),
455 SyntaxKind::LeftParen
456 | SyntaxKind::RightParen
457 | SyntaxKind::Comma
458 | SyntaxKind::Colon
459 ) {
460 let Some(prev) = deciding.prev_leaf() else { break };
461 deciding = prev;
462 }
463
464 if let SyntaxKind::Colon = deciding.kind()
466 && let Some(prev) = deciding.prev_leaf()
467 && let Some(param) = prev.get().cast::<ast::Ident>()
468 {
469 if let Some(next) = deciding.next_leaf() {
470 ctx.from = ctx.cursor.min(next.offset());
471 }
472
473 named_param_value_completions(ctx, &callee, ¶m);
474 return true;
475 }
476
477 if let SyntaxKind::LeftParen | SyntaxKind::Comma = deciding.kind()
479 && (deciding.kind() != SyntaxKind::Comma
480 || deciding.range().end < ctx.cursor
481 || ctx.explicit)
482 {
483 if let Some(next) = deciding.next_leaf() {
484 ctx.from = ctx.cursor.min(next.offset());
485 }
486
487 param_completions(ctx, &callee, set, args, args_linked);
488 return true;
489 }
490
491 false
492}
493
494fn param_completions<'a>(
496 ctx: &mut CompletionContext<'a>,
497 callee: &LinkedNode<'a>,
498 set: bool,
499 args: ast::Args<'a>,
500 args_linked: &LinkedNode<'a>,
501) {
502 let Some(value) = analyze_expr_with_fallback(ctx.world, callee) else { return };
503 let Ok(func) = value.cast::<Func>() else { return };
504
505 let mut existing_positional = 0;
507 let mut existing_named = FxHashSet::default();
508 for arg in args.items() {
509 match arg {
510 ast::Arg::Pos(_) => {
511 let Some(node) = args_linked.find(arg.span()) else { continue };
512 if node.range().end < ctx.cursor {
513 existing_positional += 1;
514 }
515 }
516 ast::Arg::Named(named) => {
517 existing_named.insert(named.name().as_str());
518 }
519 _ => {}
520 }
521 }
522
523 let mut skipped_positional = 0;
524 for param in func.params() {
525 if set && !param.settable() {
526 continue;
527 }
528
529 if param.positional() {
530 if skipped_positional < existing_positional && !param.variadic() {
531 skipped_positional += 1;
532 continue;
533 }
534
535 param_value_completions(ctx, &func, ¶m);
536 }
537
538 if let Some(name) = param.name()
539 && param.named()
540 {
541 if existing_named.contains(name) {
542 continue;
543 }
544
545 let apply = if param.name() == Some("caption") {
546 eco_format!("{name}: [${{}}]")
547 } else {
548 eco_format!("{name}: ${{}}")
549 };
550
551 ctx.completions.push(Completion {
552 kind: CompletionKind::Param,
553 label: name.into(),
554 apply: Some(apply),
555 detail: find_param_docs(ctx.world, ¶m).map(|docs| docs.summary()),
556 });
557 }
558 }
559
560 if ctx.before.ends_with(',') {
561 ctx.enrich(" ", "");
562 }
563}
564
565fn named_param_value_completions<'a>(
567 ctx: &mut CompletionContext<'a>,
568 callee: &LinkedNode,
569 name: &str,
570) {
571 let Some(value) = analyze_expr_with_fallback(ctx.world, callee) else { return };
572 let Ok(func) = value.cast::<Func>() else { return };
573
574 let Some(param) = func.param(name) else { return };
575 if !param.named() {
576 return;
577 }
578
579 param_value_completions(ctx, &func, ¶m);
580
581 if ctx.before.ends_with(':') {
582 ctx.enrich(" ", "");
583 }
584}
585
586fn param_value_completions<'a>(
588 ctx: &mut CompletionContext<'a>,
589 func: &Func,
590 param: &ParamInfo,
591) {
592 if param.name() == Some("font") {
593 ctx.font_completions();
594 } else if let Some(extensions) = path_completion(func, param) {
595 ctx.file_completions_with_extensions(extensions);
596 } else if func.name() == Some("figure") && param.name() == Some("body") {
597 ctx.snippet_completion("image", "image(\"${}\"),", "An image in a figure.");
598 ctx.snippet_completion("table", "table(\n ${}\n),", "A table in a figure.");
599 }
600
601 if let ParamInfo::Native(param) = param {
602 ctx.cast_completions(¶m.input);
603 }
604}
605
606fn path_completion(func: &Func, param: &ParamInfo) -> Option<&'static [&'static str]> {
608 Some(match (func.name(), param.name().unwrap_or_default()) {
609 (Some("image"), "source") => {
610 &["png", "jpg", "jpeg", "gif", "svg", "svgz", "webp", "pdf"]
611 }
612 (Some("csv"), "source") => &["csv"],
613 (Some("plugin"), "source") => &["wasm"],
614 (Some("cbor"), "source") => &["cbor"],
615 (Some("json"), "source") => &["json"],
616 (Some("toml"), "source") => &["toml"],
617 (Some("xml"), "source") => &["xml"],
618 (Some("yaml"), "source") => &["yml", "yaml"],
619 (Some("bibliography"), "sources") => &["bib", "yml", "yaml"],
620 (Some("bibliography"), "style") => &["csl"],
621 (Some("cite"), "style") => &["csl"],
622 (Some("raw"), "syntaxes") => &["sublime-syntax"],
623 (Some("raw"), "theme") => &["tmtheme"],
624 (Some("attach"), "path") if *func == typst::pdf::AttachElem::ELEM => &[],
625 (None, "path") => &[],
626 _ => return None,
627 })
628}
629
630fn complete_markup(ctx: &mut CompletionContext) -> bool {
632 debug_assert_eq!(ctx.leaf.mode_after(), Some(SyntaxMode::Markup));
633
634 if ctx.leaf.kind() == SyntaxKind::Text && ctx.before.ends_with("@") {
636 ctx.from = ctx.cursor;
637 ctx.label_completions();
638 return true;
639 }
640
641 if ctx.leaf.kind() == SyntaxKind::RefMarker {
643 ctx.from = ctx.leaf.offset() + 1;
644 ctx.label_completions();
645 return true;
646 }
647
648 if let Some(prev) = ctx.leaf.prev_leaf()
650 && prev.kind() == SyntaxKind::Eq
651 && prev.parent_kind() == Some(SyntaxKind::LetBinding)
652 {
653 ctx.from = ctx.cursor;
654 code_completions(ctx, false);
655 return true;
656 }
657
658 if let Some(prev) = ctx.leaf.prev_leaf()
660 && prev.kind() == SyntaxKind::Context
661 {
662 ctx.from = ctx.cursor;
663 code_completions(ctx, false);
664 return true;
665 }
666
667 let mut s = Scanner::new(ctx.text);
669 s.jump(ctx.leaf.offset());
670 if s.eat_if("```") {
671 s.eat_while('`');
672 let start = s.cursor();
673 if s.eat_if(is_id_start) {
674 s.eat_while(is_id_continue);
675 }
676 if s.cursor() == ctx.cursor {
677 ctx.from = start;
678 ctx.raw_completions();
679 }
680 return true;
681 }
682
683 if ctx.explicit {
685 ctx.from = ctx.cursor;
686 markup_completions(ctx);
687 return true;
688 }
689
690 false
691}
692
693#[rustfmt::skip]
695fn markup_completions(ctx: &mut CompletionContext) {
696 ctx.snippet_completion(
697 "expression",
698 "#${}",
699 "Variables, function calls, blocks, and more.",
700 );
701
702 ctx.snippet_completion(
703 "linebreak",
704 "\\\n${}",
705 "Inserts a forced linebreak.",
706 );
707
708 ctx.snippet_completion(
709 "strong text",
710 "*${strong}*",
711 "Strongly emphasizes content by increasing the font weight.",
712 );
713
714 ctx.snippet_completion(
715 "emphasized text",
716 "_${emphasized}_",
717 "Emphasizes content by setting it in italic font style.",
718 );
719
720 ctx.snippet_completion(
721 "raw text",
722 "`${text}`",
723 "Displays text verbatim, in monospace.",
724 );
725
726 ctx.snippet_completion(
727 "code listing",
728 "```${lang}\n${code}\n```",
729 "Inserts computer code with syntax highlighting.",
730 );
731
732 ctx.snippet_completion(
733 "hyperlink",
734 "https://${example.com}",
735 "Links to a URL.",
736 );
737
738 ctx.snippet_completion(
739 "label",
740 "<${name}>",
741 "Makes the preceding element referenceable.",
742 );
743
744 ctx.snippet_completion(
745 "reference",
746 "@${name}",
747 "Inserts a reference to a label.",
748 );
749
750 ctx.snippet_completion(
751 "heading",
752 "= ${title}",
753 "Inserts a section heading.",
754 );
755
756 ctx.snippet_completion(
757 "list item",
758 "- ${item}",
759 "Inserts an item of a bullet list.",
760 );
761
762 ctx.snippet_completion(
763 "enumeration item",
764 "+ ${item}",
765 "Inserts an item of a numbered list.",
766 );
767
768 ctx.snippet_completion(
769 "enumeration item (numbered)",
770 "${number}. ${item}",
771 "Inserts an explicitly numbered list item.",
772 );
773
774 ctx.snippet_completion(
775 "term list item",
776 "/ ${term}: ${description}",
777 "Inserts an item of a term list.",
778 );
779
780 ctx.snippet_completion(
781 "math (inline)",
782 "$${x}$",
783 "Inserts an inline-level mathematical equation.",
784 );
785
786 ctx.snippet_completion(
787 "math (block)",
788 "$ ${sum_x^2} $",
789 "Inserts a block-level mathematical equation.",
790 );
791}
792
793fn complete_math(ctx: &mut CompletionContext) -> bool {
795 debug_assert_eq!(ctx.leaf.mode_after(), Some(SyntaxMode::Math));
796
797 if matches!(ctx.leaf.kind(), SyntaxKind::MathText | SyntaxKind::MathIdent) {
799 ctx.from = ctx.leaf.offset();
800 math_completions(ctx);
801 return true;
802 }
803
804 if ctx.explicit {
806 ctx.from = ctx.cursor;
807 math_completions(ctx);
808 return true;
809 }
810
811 false
812}
813
814#[rustfmt::skip]
816fn math_completions(ctx: &mut CompletionContext) {
817 ctx.scope_completions(true, |_| true);
818
819 ctx.snippet_completion(
820 "subscript",
821 "${x}_${2:2}",
822 "Sets something in subscript.",
823 );
824
825 ctx.snippet_completion(
826 "superscript",
827 "${x}^${2:2}",
828 "Sets something in superscript.",
829 );
830
831 ctx.snippet_completion(
832 "fraction",
833 "${x}/${y}",
834 "Inserts a fraction.",
835 );
836}
837
838fn complete_code(ctx: &mut CompletionContext) -> bool {
840 debug_assert_eq!(ctx.leaf.mode_after(), Some(SyntaxMode::Code));
841
842 if ctx.leaf.kind() == SyntaxKind::Hash {
845 ctx.from = ctx.cursor;
846 code_completions(ctx, true);
847 return true;
848 }
849
850 if ctx.leaf.kind() == SyntaxKind::Ident
853 && (ctx.leaf.index() > 0 || ctx.leaf.parent_kind() != Some(SyntaxKind::Named))
854 {
855 ctx.from = ctx.leaf.offset();
856 code_completions(ctx, false);
857 return true;
858 }
859
860 if ctx.explicit
864 && ctx.leaf.parent_kind() != Some(SyntaxKind::Dict)
865 && (ctx.leaf.kind().is_trivia()
866 || matches!(
867 ctx.leaf.kind(),
868 SyntaxKind::LeftParen
869 | SyntaxKind::LeftBrace
870 | SyntaxKind::Comma
871 | SyntaxKind::Colon
872 ))
873 {
874 ctx.from = ctx.cursor;
875 code_completions(ctx, false);
876 return true;
877 }
878
879 false
880}
881
882#[rustfmt::skip]
884fn code_completions(ctx: &mut CompletionContext, hash: bool) {
885 if hash {
886 ctx.scope_completions(true, |value| {
887 let ty = value.ty();
890 ty != Type::of::<Color>()
891 && ty != Type::of::<Dir>()
892 && ty != Type::of::<Alignment>()
893 });
894 } else {
895 ctx.scope_completions(true, |_| true);
896 }
897
898 ctx.snippet_completion(
899 "function call",
900 "${function}(${arguments})[${body}]",
901 "Evaluates a function.",
902 );
903
904 ctx.snippet_completion(
905 "code block",
906 "{ ${} }",
907 "Inserts a nested code block.",
908 );
909
910 ctx.snippet_completion(
911 "content block",
912 "[${content}]",
913 "Switches into markup mode.",
914 );
915
916 ctx.snippet_completion(
917 "set rule",
918 "set ${}",
919 "Sets style properties on an element.",
920 );
921
922 ctx.snippet_completion(
923 "show rule",
924 "show ${}",
925 "Redefines the look of an element.",
926 );
927
928 ctx.snippet_completion(
929 "show rule (everything)",
930 "show: ${}",
931 "Transforms everything that follows.",
932 );
933
934 ctx.snippet_completion(
935 "context expression",
936 "context ${}",
937 "Provides contextual data.",
938 );
939
940 ctx.snippet_completion(
941 "let binding",
942 "let ${name} = ${value}",
943 "Saves a value in a variable.",
944 );
945
946 ctx.snippet_completion(
947 "let binding (function)",
948 "let ${name}(${params}) = ${output}",
949 "Defines a function.",
950 );
951
952 ctx.snippet_completion(
953 "if conditional",
954 "if ${1 < 2} {\n\t${}\n}",
955 "Computes or inserts something conditionally.",
956 );
957
958 ctx.snippet_completion(
959 "if-else conditional",
960 "if ${1 < 2} {\n\t${}\n} else {\n\t${}\n}",
961 "Computes or inserts different things based on a condition.",
962 );
963
964 ctx.snippet_completion(
965 "while loop",
966 "while ${1 < 2} {\n\t${}\n}",
967 "Computes or inserts something while a condition is met.",
968 );
969
970 ctx.snippet_completion(
971 "for loop",
972 "for ${value} in ${(1, 2, 3)} {\n\t${}\n}",
973 "Computes or inserts something for each value in a collection.",
974 );
975
976 ctx.snippet_completion(
977 "for loop (with key)",
978 "for (${key}, ${value}) in ${(a: 1, b: 2)} {\n\t${}\n}",
979 "Computes or inserts something for each key and value in a collection.",
980 );
981
982 ctx.snippet_completion(
983 "break",
984 "break",
985 "Exits early from a loop.",
986 );
987
988 ctx.snippet_completion(
989 "continue",
990 "continue",
991 "Continues with the next iteration of a loop.",
992 );
993
994 ctx.snippet_completion(
995 "return",
996 "return ${output}",
997 "Returns early from a function.",
998 );
999
1000 ctx.snippet_completion(
1001 "import (file)",
1002 "import \"${}\": ${}",
1003 "Imports variables from another file.",
1004 );
1005
1006 ctx.snippet_completion(
1007 "import (package)",
1008 "import \"@${}\": ${}",
1009 "Imports variables from a package.",
1010 );
1011
1012 ctx.snippet_completion(
1013 "include (file)",
1014 "include \"${}\"",
1015 "Includes content from another file.",
1016 );
1017
1018 ctx.snippet_completion(
1019 "array literal",
1020 "(${1, 2, 3})",
1021 "Creates a sequence of values.",
1022 );
1023
1024 ctx.snippet_completion(
1025 "dictionary literal",
1026 "(${a: 1, b: 2})",
1027 "Creates a mapping from names to value.",
1028 );
1029
1030 if !hash {
1031 ctx.snippet_completion(
1032 "function",
1033 "(${params}) => ${output}",
1034 "Creates an unnamed function.",
1035 );
1036 }
1037}
1038
1039fn is_in_equation_show_rule(leaf: &LinkedNode<'_>) -> bool {
1041 let mut node = leaf;
1042 while let Some(parent) = node.parent() {
1043 if let Some(expr) = parent.get().cast::<ast::Expr>()
1044 && let ast::Expr::ShowRule(show) = expr
1045 && let Some(ast::Expr::FieldAccess(field)) = show.selector()
1046 && field.field().as_str() == "equation"
1047 {
1048 return true;
1049 }
1050 node = parent;
1051 }
1052 false
1053}
1054
1055struct CompletionContext<'a> {
1057 world: &'a (dyn IdeWorld + 'a),
1058 output: Option<&'a dyn Output>,
1059 text: &'a str,
1060 before: &'a str,
1061 after: &'a str,
1062 leaf: &'a LinkedNode<'a>,
1063 cursor: usize,
1064 explicit: bool,
1065 from: usize,
1066 completions: Vec<Completion>,
1067 seen_casts: FxHashSet<u128>,
1068}
1069
1070impl<'a> CompletionContext<'a> {
1071 fn new(
1073 world: &'a (dyn IdeWorld + 'a),
1074 output: Option<&'a dyn Output>,
1075 source: &'a Source,
1076 leaf: &'a LinkedNode<'a>,
1077 cursor: usize,
1078 explicit: bool,
1079 ) -> Self {
1080 let text = source.text();
1081 Self {
1082 world,
1083 output,
1084 text,
1085 before: &text[..cursor],
1086 after: &text[cursor..],
1087 leaf,
1088 cursor,
1089 explicit,
1090 from: cursor,
1091 completions: vec![],
1092 seen_casts: FxHashSet::default(),
1093 }
1094 }
1095
1096 fn before_window(&self, size: usize) -> &str {
1098 Scanner::new(self.before).get(self.cursor.saturating_sub(size)..self.cursor)
1099 }
1100
1101 fn enrich(&mut self, prefix: &str, suffix: &str) {
1103 for Completion { label, apply, .. } in &mut self.completions {
1104 let current = apply.as_ref().unwrap_or(label);
1105 *apply = Some(eco_format!("{prefix}{current}{suffix}"));
1106 }
1107 }
1108
1109 fn snippet_completion(
1111 &mut self,
1112 label: &'static str,
1113 snippet: &'static str,
1114 docs: &'static str,
1115 ) {
1116 self.completions.push(Completion {
1117 kind: CompletionKind::Syntax,
1118 label: label.into(),
1119 apply: Some(snippet.into()),
1120 detail: Some(docs.into()),
1121 });
1122 }
1123
1124 fn font_completions(&mut self) {
1126 let book = self.world.book();
1127 let equation = is_in_equation_show_rule(self.leaf);
1128 for (family, iter) in book.families() {
1129 let variants: Vec<_> = iter.filter_map(|id| book.info(id)).collect();
1130 let is_math = variants.iter().any(|f| f.flags.contains(FontFlags::MATH));
1131 let detail = summarize_font_family(variants);
1132 if !equation || is_math {
1133 self.str_completion(
1134 family,
1135 Some(CompletionKind::Font),
1136 Some(detail.as_str()),
1137 );
1138 }
1139 }
1140 }
1141
1142 fn package_completions(&mut self, all_versions: bool) {
1144 let mut packages: Vec<_> = self.world.packages().iter().collect();
1145 packages.sort_by_key(|(spec, _)| {
1146 (&spec.namespace, &spec.name, Reverse(spec.version))
1147 });
1148 if !all_versions {
1149 packages.dedup_by_key(|(spec, _)| (&spec.namespace, &spec.name));
1150 }
1151 for (package, description) in packages {
1152 self.str_completion(
1153 eco_format!("{package}"),
1154 Some(CompletionKind::Package),
1155 description.as_deref(),
1156 );
1157 }
1158 }
1159
1160 fn file_completions(&mut self, mut filter: impl FnMut(FileId) -> bool) {
1162 let Some(current_id) = self.leaf.span().id() else { return };
1163 let Some(current_dir) = current_id.vpath().parent() else { return };
1164
1165 let mut paths: Vec<EcoString> = self
1166 .world
1167 .files()
1168 .iter()
1169 .filter(|&&id| id != current_id && filter(id))
1170 .map(|id| id.vpath().relative_from(¤t_dir))
1171 .collect();
1172
1173 paths.sort();
1174
1175 for path in paths {
1176 self.str_completion(path, Some(CompletionKind::Path), None);
1177 }
1178 }
1179
1180 fn file_completions_with_extensions(&mut self, extensions: &[&str]) {
1184 if extensions.is_empty() {
1185 self.file_completions(|_| true);
1186 }
1187 self.file_completions(|id| {
1188 let ext = id
1189 .vpath()
1190 .extension()
1191 .map(EcoString::from)
1192 .unwrap_or_default()
1193 .to_lowercase();
1194 extensions.contains(&ext.as_str())
1195 });
1196 }
1197
1198 fn raw_completions(&mut self) {
1200 for (name, mut tags) in RawElem::languages() {
1201 let lower = name.to_lowercase();
1202 if !tags.contains(&lower.as_str()) {
1203 tags.push(lower.as_str());
1204 }
1205
1206 tags.retain(|tag| is_ident(tag));
1207 if tags.is_empty() {
1208 continue;
1209 }
1210
1211 self.completions.push(Completion {
1212 kind: CompletionKind::Constant,
1213 label: name.into(),
1214 apply: Some(tags[0].into()),
1215 detail: Some(repr::separated_list(&tags, " or ").into()),
1216 });
1217 }
1218 }
1219
1220 fn label_completions(&mut self) {
1222 let Some(output) = self.output else { return };
1223 let (labels, split) = analyze_labels(output);
1224
1225 let head = &self.text[..self.from];
1226 let at = head.ends_with('@');
1227 let open = !at && !head.ends_with('<');
1228 let close = !at && !self.after.starts_with('>');
1229 let citation = !at && self.before_window(15).contains("cite");
1230
1231 let (skip, take) = if at {
1232 (0, usize::MAX)
1233 } else if citation {
1234 (split, usize::MAX)
1235 } else {
1236 (0, split)
1237 };
1238
1239 for (label, detail) in labels.into_iter().skip(skip).take(take) {
1240 self.completions.push(Completion {
1241 kind: CompletionKind::Label,
1242 apply: (open || close).then(|| {
1243 eco_format!(
1244 "{}{}{}",
1245 if open { "<" } else { "" },
1246 label.resolve(),
1247 if close { ">" } else { "" }
1248 )
1249 }),
1250 label: label.resolve().as_str().into(),
1251 detail,
1252 });
1253 }
1254 }
1255
1256 fn value_completion(&mut self, label: impl Into<EcoString>, value: &Value) {
1258 self.value_completion_full(Some(label.into()), value, false, None, None);
1259 }
1260
1261 fn call_completion(&mut self, label: impl Into<EcoString>, value: &Value) {
1263 self.value_completion_full(Some(label.into()), value, true, None, None);
1264 }
1265
1266 fn str_completion(
1268 &mut self,
1269 string: impl Into<EcoString>,
1270 kind: Option<CompletionKind>,
1271 detail: Option<&str>,
1272 ) {
1273 let string = string.into();
1274 self.value_completion_full(None, &Value::Str(string.into()), false, kind, detail);
1275 }
1276
1277 fn value_completion_full(
1279 &mut self,
1280 label: Option<EcoString>,
1281 value: &Value,
1282 parens: bool,
1283 kind: Option<CompletionKind>,
1284 detail: Option<&str>,
1285 ) {
1286 let at = label.as_deref().is_some_and(|field| !is_ident(field));
1287 let label = label.unwrap_or_else(|| value.repr());
1288
1289 let detail = detail.map(Into::into).or_else(|| match value {
1290 Value::Symbol(_) => None,
1291 Value::Func(_) | Value::Type(_) => {
1292 find_value_docs(self.world, value).map(|docs| docs.summary())
1293 }
1294 v => {
1295 let repr = v.repr();
1296 (repr.as_str() != label).then_some(repr)
1297 }
1298 });
1299
1300 let mut apply = None;
1301 if parens
1302 && matches!(value, Value::Func(_))
1303 && !self.after.starts_with(['(', '['])
1304 {
1305 if let Value::Func(func) = value {
1306 let bracket_mode = if self.leaf.mode_after() == Some(SyntaxMode::Math) {
1307 BracketMode::RoundWithin
1308 } else {
1309 BracketMode::of(func)
1310 };
1311 apply = Some(match bracket_mode {
1312 BracketMode::RoundAfter => eco_format!("{label}()${{}}"),
1313 BracketMode::RoundWithin => eco_format!("{label}(${{}})"),
1314 BracketMode::RoundNewline => eco_format!("{label}(\n ${{}}\n)"),
1315 BracketMode::SquareWithin => eco_format!("{label}[${{}}]"),
1316 });
1317 }
1318 } else if at {
1319 apply = Some(eco_format!("at(\"{label}\")"));
1320 } else if label.starts_with('"')
1321 && self.after.starts_with('"')
1322 && let Some(trimmed) = label.strip_suffix('"')
1323 {
1324 apply = Some(trimmed.into());
1325 }
1326
1327 self.completions.push(Completion {
1328 kind: kind.unwrap_or_else(|| match value {
1329 Value::Func(_) => CompletionKind::Func,
1330 Value::Type(_) => CompletionKind::Type,
1331 Value::Symbol(s) => CompletionKind::Symbol(s.get().into()),
1332 _ => CompletionKind::Constant,
1333 }),
1334 label,
1335 apply,
1336 detail,
1337 });
1338 }
1339
1340 fn cast_completions(&mut self, cast: &CastInfo) {
1342 if !self.seen_casts.insert(typst::utils::hash128(cast)) {
1344 return;
1345 }
1346
1347 match cast {
1348 CastInfo::Any => {}
1349 CastInfo::Value(value, docs) => {
1350 self.value_completion_full(None, value, false, None, Some(docs));
1351 }
1352 CastInfo::Type(ty) => {
1353 if *ty == Type::of::<NoneValue>() {
1354 self.snippet_completion("none", "none", "Nothing.")
1355 } else if *ty == Type::of::<AutoValue>() {
1356 self.snippet_completion("auto", "auto", "A smart default.");
1357 } else if *ty == Type::of::<bool>() {
1358 self.snippet_completion("false", "false", "No / Disabled.");
1359 self.snippet_completion("true", "true", "Yes / Enabled.");
1360 } else if *ty == Type::of::<Color>() {
1361 self.snippet_completion(
1362 "luma()",
1363 "luma(${v})",
1364 "A custom grayscale color.",
1365 );
1366 self.snippet_completion(
1367 "rgb()",
1368 "rgb(${r}, ${g}, ${b}, ${a})",
1369 "A custom RGBA color.",
1370 );
1371 self.snippet_completion(
1372 "cmyk()",
1373 "cmyk(${c}, ${m}, ${y}, ${k})",
1374 "A custom CMYK color.",
1375 );
1376 self.snippet_completion(
1377 "oklab()",
1378 "oklab(${l}, ${a}, ${b}, ${alpha})",
1379 "A custom Oklab color.",
1380 );
1381 self.snippet_completion(
1382 "oklch()",
1383 "oklch(${l}, ${chroma}, ${hue}, ${alpha})",
1384 "A custom Oklch color.",
1385 );
1386 self.snippet_completion(
1387 "color.linear-rgb()",
1388 "color.linear-rgb(${r}, ${g}, ${b}, ${a})",
1389 "A custom linear RGBA color.",
1390 );
1391 self.snippet_completion(
1392 "color.hsv()",
1393 "color.hsv(${h}, ${s}, ${v}, ${a})",
1394 "A custom HSVA color.",
1395 );
1396 self.snippet_completion(
1397 "color.hsl()",
1398 "color.hsl(${h}, ${s}, ${l}, ${a})",
1399 "A custom HSLA color.",
1400 );
1401 self.scope_completions(false, |value| value.ty() == *ty);
1402 } else if *ty == Type::of::<Label>() {
1403 self.label_completions()
1404 } else if *ty == Type::of::<Func>() {
1405 self.snippet_completion(
1406 "function",
1407 "(${params}) => ${output}",
1408 "A custom function.",
1409 );
1410 } else {
1411 self.completions.push(Completion {
1412 kind: CompletionKind::Syntax,
1413 label: ty.long_name().into(),
1414 apply: Some(eco_format!("${{{ty}}}")),
1415 detail: Some(eco_format!("A value of type {ty}.")),
1416 });
1417 self.scope_completions(false, |value| value.ty() == *ty);
1418 }
1419 }
1420 CastInfo::Union(union) => {
1421 for info in union {
1422 self.cast_completions(info);
1423 }
1424 }
1425 }
1426 }
1427
1428 fn scope_completions(&mut self, parens: bool, filter: impl Fn(&Value) -> bool) {
1432 let filter = |value: &Value| check_value_recursively(value, &filter);
1437
1438 let mut defined = BTreeMap::<EcoString, Option<Value>>::new();
1439 named_items(self.world, self.leaf.clone(), |item| {
1440 let name = item.name();
1441 if !name.is_empty() && item.value().as_ref().is_none_or(filter) {
1442 defined.insert(name.clone(), item.value());
1443 }
1444
1445 None::<()>
1446 });
1447
1448 for (name, value) in &defined {
1449 if let Some(value) = value {
1450 self.value_completion(name.clone(), value);
1451 } else {
1452 self.completions.push(Completion {
1453 kind: CompletionKind::Constant,
1454 label: name.clone(),
1455 apply: None,
1456 detail: None,
1457 });
1458 }
1459 }
1460
1461 for (name, binding) in globals(self.world, self.leaf).iter() {
1462 let value = binding.read();
1463 if filter(value) && !defined.contains_key(name) {
1464 self.value_completion_full(Some(name.clone()), value, parens, None, None);
1465 }
1466 }
1467 }
1468}
1469
1470enum BracketMode {
1472 RoundWithin,
1474 RoundAfter,
1476 RoundNewline,
1478 SquareWithin,
1480}
1481
1482impl BracketMode {
1483 fn of(func: &Func) -> Self {
1484 if func.params().all(|param| param.name() == Some("self")) {
1487 return Self::RoundAfter;
1488 }
1489
1490 match func.name() {
1491 Some(
1492 "emph" | "footnote" | "quote" | "strong" | "highlight" | "overline"
1493 | "underline" | "smallcaps" | "strike" | "sub" | "super",
1494 ) => Self::SquareWithin,
1495 Some("colbreak" | "parbreak" | "linebreak" | "pagebreak") => Self::RoundAfter,
1496 Some("figure" | "table" | "grid" | "stack") => Self::RoundNewline,
1497 _ => Self::RoundWithin,
1498 }
1499 }
1500}
1501
1502#[cfg(test)]
1503mod tests {
1504 use std::borrow::Borrow;
1505 use std::collections::BTreeSet;
1506
1507 use typst::foundations::AsOutput;
1508 use typst_layout::PagedDocument;
1509
1510 use super::{Completion, CompletionKind, autocomplete};
1511 use crate::tests::{FilePos, TestWorld, WorldLike};
1512
1513 macro_rules! q {
1515 ($s:literal) => {
1516 concat!("\"", $s, "\"")
1517 };
1518 }
1519
1520 type Response = Option<(usize, Vec<Completion>)>;
1521
1522 trait ResponseExt {
1523 fn completions(&self) -> &[Completion];
1524 fn labels(&self) -> BTreeSet<&str>;
1525 fn must_be_empty(&self) -> &Self;
1526 fn must_include<'a>(&self, includes: impl IntoIterator<Item = &'a str>) -> &Self;
1527 fn must_exclude<'a>(&self, excludes: impl IntoIterator<Item = &'a str>) -> &Self;
1528 fn at(&self, label: &str) -> &Completion;
1529 }
1530
1531 impl ResponseExt for Response {
1532 fn completions(&self) -> &[Completion] {
1533 match self {
1534 Some((_, completions)) => completions.as_slice(),
1535 None => &[],
1536 }
1537 }
1538
1539 fn labels(&self) -> BTreeSet<&str> {
1540 self.completions().iter().map(|c| c.label.as_str()).collect()
1541 }
1542
1543 #[track_caller]
1544 fn must_be_empty(&self) -> &Self {
1545 let labels = self.labels();
1546 assert!(
1547 labels.is_empty(),
1548 "expected no suggestions (got {labels:?} instead)"
1549 );
1550 self
1551 }
1552
1553 #[track_caller]
1554 fn must_include<'a>(&self, includes: impl IntoIterator<Item = &'a str>) -> &Self {
1555 let labels = self.labels();
1556 for item in includes {
1557 assert!(
1558 labels.contains(item),
1559 "{item:?} was not contained in {labels:?}",
1560 );
1561 }
1562 self
1563 }
1564
1565 #[track_caller]
1566 fn must_exclude<'a>(&self, excludes: impl IntoIterator<Item = &'a str>) -> &Self {
1567 let labels = self.labels();
1568 for item in excludes {
1569 assert!(
1570 !labels.contains(item),
1571 "{item:?} was wrongly contained in {labels:?}",
1572 );
1573 }
1574 self
1575 }
1576
1577 #[track_caller]
1578 fn at(&self, label: &str) -> &Completion {
1579 self.completions()
1580 .iter()
1581 .find(|c| c.label == label)
1582 .unwrap_or_else(|| panic!("found no completion for {label:?}"))
1583 }
1584 }
1585
1586 trait CompletionExt {
1587 fn must_apply_as<'a>(&self, apply: impl Into<Option<&'a str>>) -> &Self;
1588 fn must_have_detail<'a>(&self, detail: impl Into<Option<&'a str>>) -> &Self;
1589 }
1590
1591 impl CompletionExt for Completion {
1592 #[track_caller]
1593 fn must_apply_as<'a>(&self, apply: impl Into<Option<&'a str>>) -> &Self {
1594 assert_eq!(self.apply.as_deref(), apply.into());
1595 self
1596 }
1597
1598 #[track_caller]
1599 fn must_have_detail<'a>(&self, detail: impl Into<Option<&'a str>>) -> &Self {
1600 assert_eq!(self.detail.as_deref(), detail.into());
1601 self
1602 }
1603 }
1604
1605 #[track_caller]
1606 fn test(world: impl WorldLike, pos: impl FilePos) -> Response {
1607 let world = world.acquire();
1608 let world = world.borrow();
1609 let doc = typst::compile::<PagedDocument>(world).output.ok();
1610 test_with_doc(world, pos, doc.as_ref(), true)
1611 }
1612
1613 #[track_caller]
1614 fn test_implicit(world: impl WorldLike, pos: impl FilePos) -> Response {
1615 let world = world.acquire();
1616 let world = world.borrow();
1617 let doc = typst::compile::<PagedDocument>(world).output.ok();
1618 test_with_doc(world, pos, doc.as_ref(), false)
1619 }
1620
1621 #[track_caller]
1622 fn test_with_addition(
1623 initial_text: &str,
1624 addition: &str,
1625 pos: impl FilePos,
1626 ) -> Response {
1627 let mut world = TestWorld::new(initial_text);
1628 let doc = typst::compile::<PagedDocument>(&world).output.ok();
1629 let end = world.main.text().len();
1630 world.main.edit(end..end, addition);
1631 test_with_doc(&world, pos, doc.as_ref(), true)
1632 }
1633
1634 #[track_caller]
1635 fn test_with_doc(
1636 world: impl WorldLike,
1637 pos: impl FilePos,
1638 output: Option<impl AsOutput>,
1639 explicit: bool,
1640 ) -> Response {
1641 let world = world.acquire();
1642 let world = world.borrow();
1643 let (source, cursor) = pos.resolve(world);
1644 autocomplete(world, output, &source, cursor, explicit)
1645 }
1646
1647 #[test]
1648 fn test_autocomplete_hash_expr() {
1649 test("#", -1).must_include(["int", "if conditional"]);
1650 test("#i", -1).must_include(["int", "if conditional"]);
1651 test("$#$", -2).must_include(["int", "if conditional"]);
1652 test("$#i$", -2).must_include(["int", "if conditional"]);
1653 }
1654
1655 #[test]
1656 fn test_autocomplete_array_method() {
1657 test("#().", -1).must_include(["insert", "remove", "len", "all"]);
1658 test("#{ let x = (1, 2, 3); x. }", -3).must_include(["at", "push", "pop"]);
1659 }
1660
1661 #[test]
1664 fn test_autocomplete_dot_whitespace() {
1665 test("#() .", -1).must_exclude(["insert", "remove", "len", "all"]);
1666 test("#{() .}", -2).must_include(["insert", "remove", "len", "all"]);
1667 test("$#() .$", -2).must_exclude(["insert", "remove", "len", "all"]);
1668 test("$std.array .$", -2).must_exclude(["insert", "remove", "len", "all"]);
1669 test("#() .a", -1).must_exclude(["insert", "remove", "len", "all"]);
1670 test("#{() .a}", -2).must_include(["at", "any", "all"]);
1671 test("$std.array .a$", -2).must_exclude(["insert", "remove", "len", "all"]);
1672 }
1673
1674 #[test]
1676 fn test_autocomplete_math_scope() {
1677 test("$#col$", -2).must_include(["colbreak"]).must_exclude(["colon"]);
1678 test("$col$", -2).must_include(["colon"]).must_exclude(["colbreak"]);
1679 test("$(col)$", -3).must_include(["colon"]).must_exclude(["colbreak"]);
1680 test("$1/col$", -2).must_include(["colon"]).must_exclude(["colbreak"]);
1681 }
1682
1683 #[test]
1685 fn test_autocomplete_field_access() {
1686 test("#assert.", -1).must_include(["eq", "ne"]);
1687 test("$#assert.$", -2).must_include(["eq", "ne"]);
1688 test("#assert.e", -1).must_include(["eq", "ne"]);
1690 test("#(assert.e)", -2).must_include(["eq", "ne"]);
1691 test("$#assert.e$", -2).must_include(["eq", "ne"]);
1692 test("$#std.assert.e$", -2)
1693 .must_include(["eq", "ne"])
1694 .must_exclude(["lt"]);
1695 test("$std.assert.e$", -2)
1696 .must_include(["eq", "ne"])
1697 .must_exclude(["lt"]);
1698 }
1699
1700 #[test]
1702 fn test_autocomplete_math_func_call() {
1703 test("$f(#pi)$", -3).must_include(["box"]).must_exclude(["pi"]);
1704 test("$f(pi)$", -3).must_include(["pi"]).must_exclude(["box"]);
1705 test("$pi()$", -4).must_include(["pi"]).must_exclude(["box"]);
1706 test("$pi(pi)$", -3).must_include(["pi"]).must_exclude(["box"]);
1707 test("$vec(pi)$", -3).must_include(["pi"]).must_exclude(["box"]);
1708 test("$vec(size:pi)$", -3).must_include(["pi"]).must_exclude(["box"]);
1709 test("$vec(..pi)$", -3).must_include(["pi"]).must_exclude(["box"]);
1710 }
1711
1712 #[test]
1714 fn test_autocomplete_dict_fields() {
1715 let with = |text| &*format!("#let dict = (a: (c: 1), b: 2); {text}").leak();
1716 test(with("#dict."), -1)
1717 .must_include(["a", "b", "keys"])
1718 .must_exclude(["c"]);
1719 test(with("$dict.$"), -2)
1720 .must_include(["a", "b", "keys"])
1721 .must_exclude(["c"]);
1722 test(with("#dict.b."), -1)
1723 .must_include(["bit-or"])
1724 .must_exclude(["c"]);
1725 test(with("$dict.b.$"), -2)
1726 .must_include(["bit-or"])
1727 .must_exclude(["c"]);
1728 test(with("#dict.a."), -1)
1729 .must_include(["c", "keys"])
1730 .must_exclude(["b"]);
1731 test(with("$dict.a.$"), -2)
1732 .must_include(["c", "keys"])
1733 .must_exclude(["b"]);
1734 test(with("#dict.a.c."), -1).must_include(["bit-or"]);
1735 test(with("$dict.a.c.$"), -2).must_include(["bit-or"]);
1736 }
1737
1738 #[test]
1740 fn test_autocomplete_argument_fields() {
1741 let with = |text| {
1742 &*format!("#let args = arguments(0, a: arguments(c: 1), b: 2); {text}").leak()
1743 };
1744 test(with("#args."), -1)
1745 .must_include(["a", "b", "pos", "named"])
1746 .must_exclude(["c"]);
1747 test(with("$args.$"), -2)
1748 .must_include(["a", "b", "pos", "named"])
1749 .must_exclude(["c"]);
1750 test(with("#args.b."), -1)
1751 .must_include(["bit-or"])
1752 .must_exclude(["c"]);
1753 test(with("$args.b.$"), -2)
1754 .must_include(["bit-or"])
1755 .must_exclude(["c"]);
1756 test(with("#args.at(0)."), -1).must_include(["bit-or"]);
1757 test(with("#args.a."), -1)
1758 .must_include(["c", "pos", "named"])
1759 .must_exclude(["b"]);
1760 test(with("$args.a.$"), -2)
1761 .must_include(["c", "pos", "named"])
1762 .must_exclude(["b"]);
1763 test(with("#args.a.c."), -1).must_include(["bit-or"]);
1764 test(with("$args.a.c.$"), -2).must_include(["bit-or"]);
1765 }
1766
1767 #[test]
1770 fn test_autocomplete_before_window_char_boundary() {
1771 test("😀😀 #text(font: \"\")", -3);
1772 }
1773
1774 #[test]
1777 fn test_autocomplete_cite_function() {
1778 let mut world =
1780 TestWorld::new("#bibliography(\"works.bib\") <bib>").with_asset("works.bib");
1781 let doc = typst::compile::<PagedDocument>(&world).output.ok();
1782
1783 let end = world.main.text().len();
1786 world.main.edit(end..end, " #cite()");
1787
1788 test_with_doc(&world, -2, doc.as_ref(), true)
1789 .must_include(["netwok", "glacier-melt", "supplement"])
1790 .must_exclude(["bib"]);
1791 }
1792
1793 #[test]
1794 fn test_autocomplete_ref_function() {
1795 test_with_addition("x<test>", " #ref(<)", -2).must_include(["test"]);
1796 }
1797
1798 #[test]
1799 fn test_autocomplete_ref_shorthand() {
1800 test_with_addition("x<test>", " @", -1).must_include(["test"]);
1801 }
1802
1803 #[test]
1804 fn test_autocomplete_ref_shorthand_with_partial_identifier() {
1805 test_with_addition("x<test>", " @te", -1).must_include(["test"]);
1806 }
1807
1808 #[test]
1809 fn test_autocomplete_ref_identical_labels_returns_single_completion() {
1810 let result = test_with_addition("x<test> y<test>", " @t", -1);
1811 let completions = result.completions();
1812 let label_count =
1813 completions.iter().filter(|c| c.kind == CompletionKind::Label).count();
1814 assert_eq!(label_count, 1);
1815 }
1816
1817 #[test]
1820 fn test_autocomplete_bracket_mode() {
1821 test("#", 1).at("list").must_apply_as("list(${})");
1822 test("#", 1).at("linebreak").must_apply_as("linebreak()${}");
1823 test("#", 1).at("strong").must_apply_as("strong[${}]");
1824 test("#", 1).at("footnote").must_apply_as("footnote[${}]");
1825 test("#", 1).at("figure").must_apply_as("figure(\n ${}\n)");
1826 test("#", 1).at("table").must_apply_as("table(\n ${}\n)");
1827 test("#()", 1).at("list").must_apply_as(None);
1828 test("#[]", 1).at("strong").must_apply_as(None);
1829 test("$$", 1).at("overline").must_apply_as("overline(${})");
1830 }
1831
1832 #[test]
1835 fn test_autocomplete_positional_param() {
1836 test("#numbering()", -2).must_include(["string", "integer"]);
1838 test("#numbering(\"foo\", )", -2)
1840 .must_include(["integer"])
1841 .must_exclude(["string"]);
1842 test("#numbering(\"foo\", 1, )", -2)
1844 .must_include(["integer"])
1845 .must_exclude(["string"]);
1846 test("#numbering()", -1).must_exclude(["string"]);
1848 }
1849
1850 #[test]
1853 fn test_autocomplete_value_filter() {
1854 let world = TestWorld::new("#import \"design.typ\": clrs; #rect(fill: )")
1855 .with_source(
1856 "design.typ",
1857 "#let clrs = (a: red, b: blue); #let nums = (a: 1, b: 2)",
1858 );
1859
1860 test(&world, -2)
1861 .must_include(["clrs", "aqua"])
1862 .must_exclude(["nums", "a", "b"]);
1863 }
1864
1865 #[test]
1866 fn test_autocomplete_packages() {
1867 test("#import \"@\"", -2).must_include([q!("@preview/example:0.1.0")]);
1868 }
1869
1870 #[test]
1871 fn test_autocomplete_file_path() {
1872 let world = TestWorld::new("#include \"\"")
1873 .with_source("utils.typ", "")
1874 .with_source("content/a.typ", "#image()")
1875 .with_source("content/b.typ", "#csv(\"\")")
1876 .with_source("content/c.typ", "#include \"\"")
1877 .with_source("content/d.typ", "#pdf.attach(\"\")")
1878 .with_source("content/e.typ", "#math.attach(\"\")")
1879 .with_asset_at("assets/tiger.jpg", "tiger.jpg")
1880 .with_asset_at("assets/rhino.png", "rhino.png")
1881 .with_asset_at("data/example.csv", "example.csv");
1882
1883 test(&world, -2)
1884 .must_include([q!("content/a.typ"), q!("content/b.typ"), q!("utils.typ")])
1885 .must_exclude([q!("assets/tiger.jpg")]);
1886
1887 test(&world, ("content/a.typ", -2))
1888 .must_include([q!("../assets/tiger.jpg"), q!("../assets/rhino.png")])
1889 .must_exclude([q!("../data/example.csv"), q!("b.typ")]);
1890
1891 test(&world, ("content/b.typ", -3)).must_include([q!("../data/example.csv")]);
1892
1893 test(&world, ("content/c.typ", -2))
1894 .must_include([q!("../main.typ"), q!("a.typ"), q!("b.typ")])
1895 .must_exclude([q!("c.typ")]);
1896
1897 test(&world, ("content/d.typ", -2))
1898 .must_include([q!("../assets/tiger.jpg"), q!("../data/example.csv")]);
1899
1900 test(&world, ("content/e.typ", -2)).must_exclude([q!("data/example.csv")]);
1901 }
1902
1903 #[test]
1904 fn test_autocomplete_figure_snippets() {
1905 let res = test("#figure()", -2);
1906 res.at("image").must_apply_as("image(\"${}\"),");
1907 res.at("table").must_apply_as("table(\n ${}\n),");
1908
1909 test("#figure(cap)", -2).at("caption").must_apply_as("caption: [${}]");
1910 }
1911
1912 #[test]
1913 fn test_autocomplete_import_items() {
1914 let world = TestWorld::new("#import \"other.typ\": ")
1915 .with_source("second.typ", "#import \"other.typ\": th")
1916 .with_source("other.typ", "#let this = 1; #let that = 2");
1917
1918 test(&world, ("main.typ", 21))
1919 .must_include(["*", "this", "that"])
1920 .must_exclude(["figure"]);
1921 test(&world, ("second.typ", 23))
1922 .must_include(["this", "that"])
1923 .must_exclude(["*", "figure"]);
1924 }
1925
1926 #[test]
1927 fn test_autocomplete_type_methods() {
1928 test("#\"hello\".", -1).must_include(["len", "contains"]);
1929 test("#table().", -1).must_exclude(["cell"]);
1930 }
1931
1932 #[test]
1933 fn test_autocomplete_content_methods() {
1934 test("#show outline.entry: it => it.\n#outline()\n= Hi", 30)
1935 .must_include(["indented", "body", "page"]);
1936 }
1937
1938 #[test]
1939 fn test_autocomplete_symbol_variants() {
1940 test("#sym.arrow.", -1)
1941 .must_include(["r", "dashed"])
1942 .must_exclude(["cases"]);
1943 test("$ arrow. $", -3)
1944 .must_include(["r", "dashed"])
1945 .must_exclude(["cases"]);
1946 }
1947
1948 #[test]
1949 fn test_autocomplete_fonts() {
1950 test("#text(font:)", -2)
1951 .must_include([q!("Libertinus Serif"), q!("New Computer Modern Math")]);
1952
1953 test("#show link: set text(font: )", -2)
1954 .must_include([q!("Libertinus Serif"), q!("New Computer Modern Math")]);
1955
1956 test("#show math.equation: set text(font: )", -2)
1957 .must_include([q!("New Computer Modern Math")])
1958 .must_exclude([q!("Libertinus Serif")]);
1959
1960 test("#show math.equation: it => { set text(font: )\nit }", -7)
1961 .must_include([q!("New Computer Modern Math")])
1962 .must_exclude([q!("Libertinus Serif")]);
1963 }
1964
1965 #[test]
1966 fn test_autocomplete_typed_html() {
1967 test("#html.div(translate: )", -2)
1968 .must_include(["true", "false"])
1969 .must_exclude([q!("yes"), q!("no")]);
1970 test("#html.input(value: )", -2).must_include(["float", "string", "red", "blue"]);
1971 test("#html.div(role: )", -2).must_include([q!("alertdialog")]);
1972 }
1973
1974 #[test]
1975 fn test_autocomplete_in_function_params_after_comma_and_colon() {
1976 let document = "#text(size: 12pt, [])";
1977
1978 test(document, 11).must_include(["length"]);
1980 test_implicit(document, 11).must_include(["length"]);
1981
1982 test(document, 12).must_include(["length"]);
1983 test_implicit(document, 12).must_include(["length"]);
1984
1985 test(document, 17).must_include(["font"]);
1987 test_implicit(document, 17).must_be_empty();
1988
1989 test(document, 18).must_include(["font"]);
1990 test_implicit(document, 18).must_include(["font"]);
1991 }
1992
1993 #[test]
1994 fn test_autocomplete_in_list_literal() {
1995 let document = "#let val = 0\n#(1, \"one\")";
1996
1997 test(document, 15).must_include(["color", "val"]);
1999 test_implicit(document, 15).must_be_empty();
2000
2001 test(document, 16).must_be_empty();
2003 test_implicit(document, 16).must_be_empty();
2004
2005 test(document, 17).must_include(["color", "val"]);
2007 test_implicit(document, 17).must_be_empty();
2008
2009 test(document, 18).must_include(["color", "val"]);
2010 test_implicit(document, 18).must_be_empty();
2011 }
2012
2013 #[test]
2014 fn test_autocomplete_in_dict_literal() {
2015 let document = "#let first = 0\n#(first: 1, second: one)";
2016
2017 test(document, 17).must_be_empty();
2019 test_implicit(document, 17).must_be_empty();
2020
2021 test(document, 22).must_be_empty();
2023 test_implicit(document, 22).must_be_empty();
2024
2025 test(document, 23).must_include(["align", "first"]);
2027 test_implicit(document, 23).must_be_empty();
2028
2029 test(document, 24).must_include(["align", "first"]);
2030 test_implicit(document, 24).must_be_empty();
2031
2032 test(document, 25).must_be_empty();
2034 test_implicit(document, 25).must_be_empty();
2035
2036 test(document, 26).must_be_empty();
2038 test_implicit(document, 26).must_be_empty();
2039
2040 test(document, 27).must_be_empty();
2041 test_implicit(document, 27).must_be_empty();
2042 }
2043
2044 #[test]
2045 fn test_autocomplete_in_destructuring() {
2046 let document = "#let value = 20\n#let (va: value) = (va: 10)";
2047
2048 test(document, 24).must_be_empty();
2050 test_implicit(document, 24).must_be_empty();
2051 }
2052
2053 #[test]
2054 fn test_autocomplete_user_function() {
2055 let world = TestWorld::new("#import \"lib.typ\"\n#lib.")
2056 .with_source("lib.typ", crate::tests::EXAMPLE_CLOSURE);
2057 let res = test(&world, -1);
2058 res.must_include(["foo"]);
2059 res.at("foo").must_have_detail("A useful function.");
2060 }
2061
2062 #[test]
2063 fn test_autocomplete_user_function_params() {
2064 let world = TestWorld::new("#import \"lib.typ\": *\n#foo()")
2065 .with_source("lib.typ", crate::tests::EXAMPLE_CLOSURE);
2066 let res = test(&world, -2);
2067 res.must_include(["forest", "tree"]);
2068 res.at("forest").must_have_detail("More trees.");
2069 res.at("tree").must_have_detail("Tree with three slashes.");
2070 }
2071}