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