1use super::*;
2
3#[derive(Debug)]
4pub struct Document {
5 pub content: Rope,
6 pub tree: Option<Tree>,
7 pub uri: lsp::Url,
8 pub version: i32,
9}
10
11impl From<&str> for Document {
12 fn from(value: &str) -> Self {
13 let mut document = Self {
14 content: value.into(),
15 tree: None,
16 uri: lsp::Url::parse("file:///test.just").unwrap(),
17 version: 1,
18 };
19
20 document.parse().unwrap();
21
22 document
23 }
24}
25
26impl TryFrom<lsp::DidOpenTextDocumentParams> for Document {
27 type Error = Error;
28
29 fn try_from(params: lsp::DidOpenTextDocumentParams) -> Result<Self> {
30 let lsp::TextDocumentItem {
31 text, uri, version, ..
32 } = params.text_document;
33
34 let mut document = Self {
35 content: Rope::from_str(&text),
36 tree: None,
37 uri,
38 version,
39 };
40
41 document.parse()?;
42
43 Ok(document)
44 }
45}
46
47impl Document {
48 #[must_use]
49 pub fn aliases(&self) -> Vec<Alias> {
50 self.tree.as_ref().map_or(Vec::new(), |tree| {
51 tree
52 .root_node()
53 .find_all("alias")
54 .iter()
55 .filter_map(|alias_node| {
56 let left_node = alias_node.child_by_field_name("left")?;
57 let right_node = alias_node.child_by_field_name("right")?;
58
59 Some(Alias {
60 name: TextNode {
61 value: self.get_node_text(&left_node),
62 range: left_node.get_range(self),
63 },
64 value: TextNode {
65 value: self.get_node_text(&right_node),
66 range: right_node.get_range(self),
67 },
68 range: alias_node.get_range(self),
69 })
70 })
71 .collect()
72 })
73 }
74
75 pub fn apply_change(
81 &mut self,
82 params: lsp::DidChangeTextDocumentParams,
83 ) -> Result {
84 let lsp::DidChangeTextDocumentParams {
85 content_changes,
86 text_document: lsp::VersionedTextDocumentIdentifier { version, .. },
87 ..
88 } = params;
89
90 self.version = version;
91
92 for change in content_changes {
93 let edit = self.content.build_edit(&change);
94
95 self.content.apply_edit(&edit);
96
97 if let Some(tree) = &mut self.tree {
98 tree.edit(&edit.input_edit);
99 }
100 }
101
102 self.parse()?;
103
104 Ok(())
105 }
106
107 #[must_use]
108 pub fn attributes(&self) -> Vec<Attribute> {
109 self.tree.as_ref().map_or(Vec::new(), |tree| {
110 tree
111 .root_node()
112 .find_all("attribute")
113 .into_iter()
114 .flat_map(|attribute_node| {
115 let target = attribute_node
116 .parent()
117 .and_then(|parent| AttributeTarget::try_from_kind(parent.kind()));
118
119 attribute_node
120 .find_all("^identifier")
121 .into_iter()
122 .map(move |identifier_node| {
123 let arguments = identifier_node
124 .siblings()
125 .take_while(|sibling| sibling.kind() != "identifier")
126 .filter(|sibling| {
127 sibling.start_byte() != sibling.end_byte()
128 && matches!(
129 sibling.kind(),
130 "string" | "expression" | "attribute_named_param"
131 )
132 })
133 .map(|argument_node| TextNode {
134 value: self.get_node_text(&argument_node),
135 range: argument_node.get_range(self),
136 })
137 .collect::<Vec<_>>();
138
139 Attribute {
140 name: TextNode {
141 value: self.get_node_text(&identifier_node),
142 range: identifier_node.get_range(self),
143 },
144 arguments,
145 target,
146 range: attribute_node.get_range(self),
147 }
148 })
149 .collect::<Vec<_>>()
150 })
151 .collect()
152 })
153 }
154
155 #[must_use]
156 pub fn find_function(&self, name: &str) -> Option<Function> {
157 self
158 .functions()
159 .into_iter()
160 .find(|function| function.name.value == name)
161 }
162
163 #[must_use]
164 pub fn find_recipe(&self, name: &str) -> Option<Recipe> {
165 self
166 .recipes()
167 .into_iter()
168 .find(|recipe| recipe.name.value == name)
169 }
170
171 #[must_use]
172 pub fn find_variable(&self, name: &str) -> Option<Variable> {
173 self
174 .variables()
175 .into_iter()
176 .find(|var| var.name.value == name)
177 }
178
179 pub fn format(&self) -> Result<String> {
183 let file = if let Ok(path) = self.uri.to_file_path() {
184 tempfile::Builder::new()
185 .prefix(".justfile-fmt-")
186 .tempfile_in(
187 path
188 .parent()
189 .ok_or_else(|| Error::Format("file path has no parent".into()))?,
190 )?
191 } else {
192 tempfile::Builder::new()
193 .prefix(".justfile-fmt-")
194 .tempfile()?
195 };
196
197 let content = self.content.to_string();
198
199 fs::write(&file, content.as_bytes())?;
200
201 let output = std::process::Command::new("just")
202 .arg("--fmt")
203 .arg("--unstable")
204 .arg("--quiet")
205 .arg("--justfile")
206 .arg(file.path())
207 .output()?;
208
209 if !output.status.success() {
210 return Err(Error::Format(format!(
211 "just formatting failed: {}",
212 String::from_utf8_lossy(&output.stderr)
213 )));
214 }
215
216 Ok(fs::read_to_string(&file)?)
217 }
218
219 #[must_use]
220 pub fn function_calls(&self) -> Vec<FunctionCall> {
221 self.tree.as_ref().map_or(Vec::new(), |tree| {
222 tree
223 .root_node()
224 .find_all("function_call")
225 .into_iter()
226 .filter_map(|function_call_node| {
227 let identifier_node = function_call_node.find("identifier")?;
228
229 let arguments = function_call_node
230 .find("sequence")
231 .map(|sequence| {
232 sequence
233 .find_all("^expression")
234 .into_iter()
235 .map(|argument_node| TextNode {
236 value: self.get_node_text(&argument_node),
237 range: argument_node.get_range(self),
238 })
239 .collect::<Vec<_>>()
240 })
241 .unwrap_or_default();
242
243 Some(FunctionCall {
244 name: TextNode {
245 value: self.get_node_text(&identifier_node),
246 range: identifier_node.get_range(self),
247 },
248 arguments,
249 range: function_call_node.get_range(self),
250 })
251 })
252 .collect()
253 })
254 }
255
256 #[must_use]
257 pub fn functions(&self) -> Vec<Function> {
258 self.tree.as_ref().map_or(Vec::new(), |tree| {
259 tree
260 .root_node()
261 .find_all("function_definition")
262 .iter()
263 .filter_map(|function_node| {
264 let name_node = function_node.child_by_field_name("name")?;
265
266 let parameters = function_node
267 .child_by_field_name("parameters")
268 .map(|params_node| {
269 params_node
270 .find_all("^identifier")
271 .iter()
272 .map(|param_node| TextNode {
273 value: self.get_node_text(param_node),
274 range: param_node.get_range(self),
275 })
276 .collect::<Vec<_>>()
277 })
278 .unwrap_or_default();
279
280 let body = function_node
281 .child_by_field_name("body")
282 .map(|body_node| self.get_node_text(&body_node))
283 .unwrap_or_default();
284
285 Some(Function {
286 name: TextNode {
287 value: self.get_node_text(&name_node),
288 range: name_node.get_range(self),
289 },
290 parameters,
291 body,
292 content: self.get_node_text(function_node).trim().to_string(),
293 range: function_node.get_range(self),
294 })
295 })
296 .collect()
297 })
298 }
299
300 #[must_use]
301 pub fn get_node_text(&self, node: &Node) -> String {
302 self
303 .content
304 .slice(
305 self.content.byte_to_char(node.start_byte())
306 ..self.content.byte_to_char(node.end_byte()),
307 )
308 .to_string()
309 }
310
311 #[must_use]
312 pub fn imports(&self) -> Vec<Import> {
313 self.tree.as_ref().map_or(Vec::new(), |tree| {
314 tree
315 .root_node()
316 .find_all("import")
317 .iter()
318 .filter_map(|import_node| {
319 let path_node = import_node.find("string")?;
320
321 let content = self.get_node_text(import_node);
322
323 Some(Import {
324 optional: content.contains('?'),
325 path: TextNode {
326 value: self.get_node_text(&path_node),
327 range: path_node.get_range(self),
328 },
329 range: import_node.get_range(self),
330 })
331 })
332 .collect()
333 })
334 }
335
336 #[must_use]
337 #[allow(dead_code)]
338 pub fn modules(&self) -> Vec<Module> {
339 self.tree.as_ref().map_or(Vec::new(), |tree| {
340 tree
341 .root_node()
342 .find_all("module")
343 .iter()
344 .filter_map(|module_node| {
345 let name_node = module_node.child_by_field_name("name")?;
346
347 let content = self.get_node_text(module_node);
348
349 let path = module_node.find("string").map(|path_node| TextNode {
350 value: self.get_node_text(&path_node),
351 range: path_node.get_range(self),
352 });
353
354 Some(Module {
355 name: TextNode {
356 value: self.get_node_text(&name_node),
357 range: name_node.get_range(self),
358 },
359 optional: content.contains('?'),
360 path,
361 range: module_node.get_range(self),
362 })
363 })
364 .collect()
365 })
366 }
367
368 pub fn new(source: &str, uri: lsp::Url) -> Result<Self> {
373 let mut document = Self {
374 content: Rope::from_str(source),
375 tree: None,
376 uri,
377 version: 0,
378 };
379
380 document.parse()?;
381
382 Ok(document)
383 }
384
385 #[must_use]
387 pub fn node_at_position(&self, position: lsp::Position) -> Option<Node<'_>> {
388 let tree = self.tree.as_ref()?;
389 let point = position.point(self);
390 tree.root_node().descendant_for_point_range(point, point)
391 }
392
393 pub fn parse(&mut self) -> Result {
400 let mut parser = Parser::new();
401
402 parser.set_language(&unsafe { tree_sitter_just() })?;
404
405 let old_tree = self.tree.take();
406
407 self.tree = parser.parse(self.content.to_string(), old_tree.as_ref());
408
409 Ok(())
410 }
411
412 #[must_use]
413 pub fn recipes(&self) -> Vec<Recipe> {
414 self.tree.as_ref().map_or(Vec::new(), |tree| {
415 tree
416 .root_node()
417 .find_all("recipe")
418 .iter()
419 .filter_map(|recipe_node| {
420 let name_node = recipe_node.find("recipe_header > identifier")?;
421
422 let recipe_name = TextNode {
423 value: self.get_node_text(&name_node),
424 range: name_node.get_range(self),
425 };
426
427 let attributes = recipe_node
428 .find_all("attribute")
429 .into_iter()
430 .flat_map(|attribute_node| {
431 attribute_node
432 .find_all("^identifier")
433 .into_iter()
434 .map(|identifier_node| {
435 let arguments = identifier_node
436 .siblings()
437 .take_while(|sibling| sibling.kind() != "identifier")
438 .filter(|sibling| {
439 sibling.start_byte() != sibling.end_byte()
440 && matches!(
441 sibling.kind(),
442 "string" | "expression" | "attribute_named_param"
443 )
444 })
445 .map(|argument_node| TextNode {
446 value: self.get_node_text(&argument_node),
447 range: argument_node.get_range(self),
448 })
449 .collect::<Vec<_>>();
450
451 Attribute {
452 name: TextNode {
453 value: self.get_node_text(&identifier_node),
454 range: identifier_node.get_range(self),
455 },
456 arguments,
457 target: Some(AttributeTarget::Recipe),
458 range: attribute_node.get_range(self),
459 }
460 })
461 .collect::<Vec<_>>()
462 })
463 .collect::<Vec<_>>();
464
465 let dependencies = recipe_node
466 .find("recipe_header > dependencies")
467 .map(|dependencies_node| {
468 dependencies_node
469 .find_all("dependency")
470 .into_iter()
471 .filter_map(|dependency_node| {
472 let dependency_name = dependency_node
473 .child_by_field_name("name")
474 .or_else(|| {
475 dependency_node
476 .find("dependency_expression")
477 .and_then(|node| node.child_by_field_name("name"))
478 })
479 .map(|node| self.get_node_text(&node))?;
480
481 let arguments = dependency_node
482 .find("dependency_expression")
483 .map(|dependency_expression_node| {
484 dependency_expression_node
485 .find_all("^expression")
486 .iter()
487 .map(|argument_node| TextNode {
488 value: self.get_node_text(argument_node),
489 range: argument_node.get_range(self),
490 })
491 .collect()
492 })
493 .unwrap_or_default();
494
495 Some(Dependency {
496 name: dependency_name,
497 arguments,
498 range: dependency_node.get_range(self),
499 })
500 })
501 .collect::<Vec<_>>()
502 })
503 .unwrap_or_default();
504
505 let parameters = recipe_node
506 .find("recipe_header > parameters")
507 .map_or_else(Vec::new, |parameters_node| {
508 parameters_node
509 .find_all("^parameter, ^variadic_parameter")
510 .iter()
511 .filter_map(|parameter_node| {
512 Parameter::from_node(parameter_node, self)
513 })
514 .collect()
515 });
516
517 let shebang =
518 recipe_node
519 .find("recipe_body > shebang")
520 .map(|shebang_node| TextNode {
521 value: self.get_node_text(&shebang_node),
522 range: shebang_node.get_range(self),
523 });
524
525 Some(Recipe {
526 name: recipe_name,
527 attributes,
528 dependencies,
529 content: self.get_node_text(recipe_node).trim().to_string(),
530 parameters,
531 range: recipe_node.get_range(self),
532 shebang,
533 })
534 })
535 .collect()
536 })
537 }
538
539 #[must_use]
540 pub fn settings(&self) -> Vec<Setting> {
541 self.tree.as_ref().map_or(Vec::new(), |tree| {
542 tree
543 .root_node()
544 .find_all("setting")
545 .iter()
546 .filter_map(|setting_node| Setting::from_node(setting_node, self))
547 .collect()
548 })
549 }
550
551 #[must_use]
552 pub fn variables(&self) -> Vec<Variable> {
553 self.tree.as_ref().map_or(Vec::new(), |tree| {
554 tree
555 .root_node()
556 .find_all("assignment")
557 .iter()
558 .filter_map(|assignment_node| {
559 let identifier_node = assignment_node.child_by_field_name("left")?;
560
561 Some(Variable {
562 name: TextNode {
563 value: self.get_node_text(&identifier_node),
564 range: identifier_node.get_range(self),
565 },
566 export: identifier_node.get_parent("export").is_some(),
567 unexport: identifier_node.get_parent("unexport").is_some(),
568 content: self.get_node_text(assignment_node).trim().to_string(),
569 range: assignment_node.get_range(self),
570 })
571 })
572 .collect()
573 })
574 }
575}
576
577#[cfg(test)]
578mod tests {
579 use {
580 super::*, indoc::indoc, parameter::VariadicType,
581 pretty_assertions::assert_eq,
582 };
583
584 #[test]
585 fn create_document() {
586 let content = indoc! {"
587 foo:
588 echo foo
589 "};
590
591 let document = Document::from(content);
592
593 assert_eq!(document.content.to_string(), content);
594
595 assert!(document.tree.is_some());
596 }
597
598 #[test]
599 fn apply_change() {
600 let mut document = Document::from(indoc! {
601 "
602 foo:
603 echo \"foo\"
604 "
605 });
606
607 let original_content = document.content.to_string();
608
609 let change = lsp::DidChangeTextDocumentParams {
610 text_document: lsp::VersionedTextDocumentIdentifier {
611 uri: lsp::Url::parse("file:///test.just").unwrap(),
612 version: 2,
613 },
614 content_changes: vec![lsp::TextDocumentContentChangeEvent {
615 range: Some(lsp::Range::at(1, 7, 1, 13)),
616 range_length: None,
617 text: "\"bar\"".to_string(),
618 }],
619 };
620
621 document.apply_change(change).unwrap();
622
623 assert_ne!(document.content.to_string(), original_content);
624 assert_eq!(document.content.to_string(), "foo:\n echo \"bar\"");
625 }
626
627 #[test]
628 fn find_nonexistent_recipe() {
629 let document = Document::from(indoc! {
630 "
631 foo:
632 echo \"foo\"
633 "
634 });
635
636 assert_eq!(document.find_recipe("nonexistent"), None);
637 }
638
639 #[test]
640 fn find_recipe() {
641 let document = Document::from(indoc! {
642 "
643 foo:
644 echo \"foo\"
645
646 bar:
647 echo \"bar\"
648 "
649 });
650
651 assert_eq!(
652 document.find_recipe("foo").unwrap(),
653 Recipe {
654 name: TextNode {
655 value: "foo".into(),
656 range: lsp::Range::at(0, 0, 0, 3)
657 },
658 attributes: vec![],
659 dependencies: vec![],
660 content: "foo:\n echo \"foo\"".into(),
661 parameters: vec![],
662 range: lsp::Range::at(0, 0, 3, 0),
663 shebang: None,
664 }
665 );
666
667 assert_eq!(
668 document.find_recipe("bar").unwrap(),
669 Recipe {
670 name: TextNode {
671 value: "bar".into(),
672 range: lsp::Range::at(3, 0, 3, 3)
673 },
674 attributes: vec![],
675 dependencies: vec![],
676 content: "bar:\n echo \"bar\"".into(),
677 parameters: vec![],
678 range: lsp::Range::at(3, 0, 5, 0),
679 shebang: None,
680 }
681 );
682
683 assert!(document.find_recipe("baz").is_none());
684 }
685
686 #[test]
687 fn get_array_setting() {
688 let document = Document::from(indoc! {
689 "
690 set shell := ['foo']
691 "
692 });
693
694 let settings = document.settings();
695
696 assert_eq!(settings.len(), 1);
697
698 assert_eq!(
699 settings,
700 vec![Setting {
701 name: TextNode {
702 value: "shell".into(),
703 range: lsp::Range::at(0, 4, 0, 9),
704 },
705 kind: SettingKind::Array,
706 range: lsp::Range::at(0, 0, 1, 0)
707 }]
708 );
709 }
710
711 #[test]
712 fn get_basic_alias() {
713 let document = Document::from(indoc! {
714 "
715 alias a1 := foo
716 "
717 });
718
719 let aliases = document.aliases();
720
721 assert_eq!(aliases.len(), 1);
722
723 assert_eq!(
724 aliases,
725 vec![Alias {
726 name: TextNode {
727 value: "a1".into(),
728 range: lsp::Range::at(0, 6, 0, 8)
729 },
730 value: TextNode {
731 value: "foo".into(),
732 range: lsp::Range::at(0, 12, 0, 15)
733 },
734 range: lsp::Range::at(0, 0, 0, 15)
735 }]
736 );
737 }
738
739 #[test]
740 fn get_alias_with_module_path() {
741 let document = Document::from(indoc! {
742 "
743 alias a1 := tools::build
744 "
745 });
746
747 let aliases = document.aliases();
748
749 assert_eq!(aliases.len(), 1);
750
751 assert_eq!(
752 aliases,
753 vec![Alias {
754 name: TextNode {
755 value: "a1".into(),
756 range: lsp::Range::at(0, 6, 0, 8)
757 },
758 value: TextNode {
759 value: "tools::build".into(),
760 range: lsp::Range::at(0, 12, 0, 24)
761 },
762 range: lsp::Range::at(0, 0, 0, 24)
763 }]
764 );
765 }
766
767 #[test]
768 fn get_boolean_flag_setting() {
769 let document = Document::from(indoc! {
770 "
771 set export
772 "
773 });
774
775 let settings = document.settings();
776
777 assert_eq!(settings.len(), 1);
778
779 assert_eq!(
780 settings,
781 vec![Setting {
782 name: TextNode {
783 value: "export".into(),
784 range: lsp::Range::at(0, 4, 0, 10),
785 },
786 kind: SettingKind::Boolean(true),
787 range: lsp::Range::at(0, 0, 1, 0)
788 }]
789 );
790 }
791
792 #[test]
793 fn get_boolean_setting() {
794 let document = Document::from(indoc! {
795 "
796 set export := true
797 "
798 });
799
800 let settings = document.settings();
801
802 assert_eq!(settings.len(), 1);
803
804 assert_eq!(
805 settings,
806 vec![Setting {
807 name: TextNode {
808 value: "export".into(),
809 range: lsp::Range::at(0, 4, 0, 10),
810 },
811 kind: SettingKind::Boolean(true),
812 range: lsp::Range::at(0, 0, 1, 0)
813 }]
814 );
815 }
816
817 #[test]
818 fn get_duplicate_aliases() {
819 let document = Document::from(indoc! {
820 "
821 alias duplicate := foo
822 alias duplicate := bar
823 "
824 });
825
826 let aliases = document.aliases();
827
828 assert_eq!(aliases.len(), 2);
829
830 assert_eq!(
831 aliases,
832 vec![
833 Alias {
834 name: TextNode {
835 value: "duplicate".into(),
836 range: lsp::Range::at(0, 6, 0, 15)
837 },
838 value: TextNode {
839 value: "foo".into(),
840 range: lsp::Range::at(0, 19, 0, 22)
841 },
842 range: lsp::Range::at(0, 0, 0, 22)
843 },
844 Alias {
845 name: TextNode {
846 value: "duplicate".into(),
847 range: lsp::Range::at(1, 6, 1, 15)
848 },
849 value: TextNode {
850 value: "bar".into(),
851 range: lsp::Range::at(1, 19, 1, 22)
852 },
853 range: lsp::Range::at(1, 0, 1, 22)
854 }
855 ]
856 );
857 }
858
859 #[test]
860 fn get_multiple_aliases() {
861 let document = Document::from(indoc! {
862 "
863 alias a1 := foo
864 alias a2 := bar
865 "
866 });
867
868 let aliases = document.aliases();
869
870 assert_eq!(aliases.len(), 2);
871
872 assert_eq!(
873 aliases,
874 vec![
875 Alias {
876 name: TextNode {
877 value: "a1".into(),
878 range: lsp::Range::at(0, 6, 0, 8),
879 },
880 value: TextNode {
881 value: "foo".into(),
882 range: lsp::Range::at(0, 12, 0, 15),
883 },
884 range: lsp::Range::at(0, 0, 0, 15),
885 },
886 Alias {
887 name: TextNode {
888 value: "a2".into(),
889 range: lsp::Range::at(1, 6, 1, 8),
890 },
891 value: TextNode {
892 value: "bar".into(),
893 range: lsp::Range::at(1, 12, 1, 15),
894 },
895 range: lsp::Range::at(1, 0, 1, 15),
896 }
897 ]
898 );
899 }
900
901 #[test]
902 fn get_multiple_settings() {
903 let document = Document::from(indoc! {
904 "
905 set export := true
906 set shell := ['foo']
907 set bar := 'wow!'
908 "
909 });
910
911 let settings = document.settings();
912
913 assert_eq!(settings.len(), 3);
914
915 assert_eq!(
916 settings,
917 vec![
918 Setting {
919 name: TextNode {
920 value: "export".into(),
921 range: lsp::Range::at(0, 4, 0, 10),
922 },
923 kind: SettingKind::Boolean(true),
924 range: lsp::Range::at(0, 0, 1, 0),
925 },
926 Setting {
927 name: TextNode {
928 value: "shell".into(),
929 range: lsp::Range::at(1, 4, 1, 9),
930 },
931 kind: SettingKind::Array,
932 range: lsp::Range::at(1, 0, 2, 0),
933 },
934 Setting {
935 name: TextNode {
936 value: "bar".into(),
937 range: lsp::Range::at(2, 4, 2, 7),
938 },
939 kind: SettingKind::String,
940 range: lsp::Range::at(2, 0, 3, 0),
941 }
942 ]
943 );
944 }
945
946 #[test]
947 fn get_string_setting() {
948 let document = Document::from(indoc! {
949 "
950 set bar := 'wow!'
951 "
952 });
953
954 let settings = document.settings();
955
956 assert_eq!(settings.len(), 1);
957
958 assert_eq!(
959 settings,
960 vec![Setting {
961 name: TextNode {
962 value: "bar".into(),
963 range: lsp::Range::at(0, 4, 0, 7),
964 },
965 kind: SettingKind::String,
966 range: lsp::Range::at(0, 0, 1, 0),
967 }]
968 );
969 }
970
971 #[test]
972 fn get_variables() {
973 let document = Document::from(indoc! {
974 "
975 tmpdir := `mktemp -d`
976 version := \"0.2.7\"
977 tardir := tmpdir / \"awesomesauce-\" + version
978 tarball := tardir + \".tar.gz\"
979 config := quote(config_dir() / \".project-config\")
980 export EDITOR := 'nvim'
981 "
982 });
983
984 assert_eq!(
985 document.variables(),
986 vec![
987 Variable {
988 name: TextNode {
989 value: "tmpdir".into(),
990 range: lsp::Range::at(0, 0, 0, 6),
991 },
992 export: false,
993 unexport: false,
994 content: "tmpdir := `mktemp -d`".into(),
995 range: lsp::Range::at(0, 0, 1, 0),
996 },
997 Variable {
998 name: TextNode {
999 value: "version".into(),
1000 range: lsp::Range::at(1, 0, 1, 7),
1001 },
1002 export: false,
1003 unexport: false,
1004 content: "version := \"0.2.7\"".into(),
1005 range: lsp::Range::at(1, 0, 2, 0),
1006 },
1007 Variable {
1008 name: TextNode {
1009 value: "tardir".into(),
1010 range: lsp::Range::at(2, 0, 2, 6),
1011 },
1012 export: false,
1013 unexport: false,
1014 content: "tardir := tmpdir / \"awesomesauce-\" + version".into(),
1015 range: lsp::Range::at(2, 0, 3, 0),
1016 },
1017 Variable {
1018 name: TextNode {
1019 value: "tarball".into(),
1020 range: lsp::Range::at(3, 0, 3, 7),
1021 },
1022 export: false,
1023 unexport: false,
1024 content: "tarball := tardir + \".tar.gz\"".into(),
1025 range: lsp::Range::at(3, 0, 4, 0),
1026 },
1027 Variable {
1028 name: TextNode {
1029 value: "config".into(),
1030 range: lsp::Range::at(4, 0, 4, 6),
1031 },
1032 export: false,
1033 unexport: false,
1034 content: "config := quote(config_dir() / \".project-config\")"
1035 .into(),
1036 range: lsp::Range::at(4, 0, 5, 0),
1037 },
1038 Variable {
1039 name: TextNode {
1040 value: "EDITOR".into(),
1041 range: lsp::Range::at(5, 7, 5, 13),
1042 },
1043 export: true,
1044 unexport: false,
1045 content: "EDITOR := 'nvim'".into(),
1046 range: lsp::Range::at(5, 7, 6, 0),
1047 },
1048 ]
1049 );
1050 }
1051
1052 #[test]
1053 fn private_exported_variable_is_marked_exported() {
1054 let document = Document::from(indoc! {
1055 "
1056 [private]
1057 export PATH := '/usr/local/bin'
1058 "
1059 });
1060
1061 let variables = document.variables();
1062
1063 assert_eq!(variables.len(), 1);
1064
1065 assert_eq!(
1066 variables,
1067 vec![Variable {
1068 name: TextNode {
1069 value: "PATH".into(),
1070 range: lsp::Range::at(1, 7, 1, 11),
1071 },
1072 export: true,
1073 unexport: false,
1074 content: "PATH := '/usr/local/bin'".into(),
1075 range: lsp::Range::at(1, 7, 2, 0),
1076 }]
1077 );
1078 }
1079
1080 #[test]
1081 fn unexport_variable_is_marked_unexported() {
1082 let document = Document::from(indoc! {
1083 "
1084 unexport FOO := 'bar'
1085 "
1086 });
1087
1088 let variables = document.variables();
1089
1090 assert_eq!(variables.len(), 1);
1091
1092 assert_eq!(
1093 variables,
1094 vec![Variable {
1095 name: TextNode {
1096 value: "FOO".into(),
1097 range: lsp::Range::at(0, 9, 0, 12),
1098 },
1099 export: false,
1100 unexport: true,
1101 content: "FOO := 'bar'".into(),
1102 range: lsp::Range::at(0, 9, 1, 0),
1103 }]
1104 );
1105 }
1106
1107 #[test]
1108 fn eager_variable_is_parsed() {
1109 let document = Document::from(indoc! {
1110 "
1111 eager foo := 'bar'
1112 "
1113 });
1114
1115 assert_eq!(document.variables().len(), 1);
1116 }
1117
1118 #[test]
1119 fn multiple_recipes() {
1120 let document = Document::from(indoc! {
1121 "
1122 foo:
1123 echo \"foo\"
1124
1125 bar:
1126 echo \"bar\"
1127 "
1128 });
1129
1130 assert_eq!(
1131 document.find_recipe("foo"),
1132 Some(Recipe {
1133 name: TextNode {
1134 value: "foo".into(),
1135 range: lsp::Range::at(0, 0, 0, 3)
1136 },
1137 attributes: vec![],
1138 dependencies: vec![],
1139 parameters: vec![],
1140 content: "foo:\n echo \"foo\"".into(),
1141 range: lsp::Range::at(0, 0, 3, 0),
1142 shebang: None,
1143 })
1144 );
1145
1146 assert_eq!(
1147 document.find_recipe("bar"),
1148 Some(Recipe {
1149 name: TextNode {
1150 value: "bar".into(),
1151 range: lsp::Range::at(3, 0, 3, 3)
1152 },
1153 attributes: vec![],
1154 dependencies: vec![],
1155 parameters: vec![],
1156 content: "bar:\n echo \"bar\"".into(),
1157 range: lsp::Range::at(3, 0, 5, 0),
1158 shebang: None,
1159 })
1160 );
1161 }
1162
1163 #[test]
1164 fn node_at_position() {
1165 let document = Document::from(indoc! {"
1166 foo:
1167 echo \"foo\"
1168
1169 bar: foo
1170 echo \"bar\"
1171 "});
1172
1173 let node = document
1174 .node_at_position(lsp::Position {
1175 line: 1,
1176 character: 1,
1177 })
1178 .unwrap();
1179
1180 assert_eq!(node.kind(), "recipe");
1181 assert_eq!(document.get_node_text(&node), "foo:\n echo \"foo\"\n\n");
1182
1183 let node = document
1184 .node_at_position(lsp::Position {
1185 line: 4,
1186 character: 6,
1187 })
1188 .unwrap();
1189
1190 assert_eq!(node.kind(), "text");
1191 assert_eq!(document.get_node_text(&node), "echo \"bar\"");
1192 }
1193
1194 #[test]
1195 fn node_at_position_handles_utf16_columns() {
1196 let document = Document::from(indoc! {
1197 "
1198 foo:
1199 echo \"a🧪b\"
1200 "
1201 });
1202
1203 let node = document
1204 .node_at_position(lsp::Position {
1205 line: 1,
1206 character: 11,
1207 })
1208 .unwrap();
1209
1210 assert_eq!(node.kind(), "text");
1211 assert_eq!(document.get_node_text(&node), "echo \"a🧪b\"");
1212 }
1213
1214 #[test]
1215 fn recipe_with_default_parameter() {
1216 let document = Document::from(indoc! {
1217 "
1218 baz first second=\"default\":
1219 echo \"{{first}} {{second}}\"
1220 "
1221 });
1222
1223 assert_eq!(
1224 document.find_recipe("baz"),
1225 Some(Recipe {
1226 name: TextNode {
1227 value: "baz".into(),
1228 range: lsp::Range::at(0, 0, 0, 3)
1229 },
1230 attributes: vec![],
1231 dependencies: vec![],
1232 parameters: vec![
1233 Parameter {
1234 name: "first".into(),
1235 kind: ParameterKind::Normal,
1236 default_value: None,
1237 content: "first".into(),
1238 range: lsp::Range::at(0, 4, 0, 9),
1239 },
1240 Parameter {
1241 name: "second".into(),
1242 kind: ParameterKind::Normal,
1243 default_value: Some("\"default\"".into()),
1244 content: "second=\"default\"".into(),
1245 range: lsp::Range::at(0, 10, 0, 26),
1246 }
1247 ],
1248 content:
1249 "baz first second=\"default\":\n echo \"{{first}} {{second}}\""
1250 .into(),
1251 range: lsp::Range::at(0, 0, 2, 0),
1252 shebang: None,
1253 })
1254 );
1255 }
1256
1257 #[test]
1258 fn recipe_with_dependency() {
1259 let document = Document::from(indoc! {
1260 "
1261 foo:
1262 echo \"foo\"
1263
1264 bar: foo
1265 echo \"bar\"
1266 "
1267 });
1268
1269 assert_eq!(
1270 document.find_recipe("bar"),
1271 Some(Recipe {
1272 name: TextNode {
1273 value: "bar".into(),
1274 range: lsp::Range::at(3, 0, 3, 3)
1275 },
1276 attributes: vec![],
1277 dependencies: vec![Dependency {
1278 name: "foo".into(),
1279 arguments: vec![],
1280 range: lsp::Range::at(3, 5, 3, 8),
1281 }],
1282 parameters: vec![],
1283 content: "bar: foo\n echo \"bar\"".into(),
1284 range: lsp::Range::at(3, 0, 5, 0),
1285 shebang: None,
1286 })
1287 );
1288 }
1289
1290 #[test]
1291 fn recipe_with_module_path_dependency() {
1292 let document = Document::from(indoc! {
1293 "
1294 foo:
1295 echo \"foo\"
1296
1297 bar:
1298 echo \"bar\"
1299
1300 baz: tools::foo
1301 echo \"baz\"
1302 "
1303 });
1304
1305 assert_eq!(
1306 document.find_recipe("baz"),
1307 Some(Recipe {
1308 name: TextNode {
1309 value: "baz".into(),
1310 range: lsp::Range::at(6, 0, 6, 3)
1311 },
1312 attributes: vec![],
1313 dependencies: vec![Dependency {
1314 name: "tools::foo".into(),
1315 arguments: vec![],
1316 range: lsp::Range::at(6, 5, 6, 15),
1317 }],
1318 parameters: vec![],
1319 content: "baz: tools::foo\n echo \"baz\"".into(),
1320 range: lsp::Range::at(6, 0, 8, 0),
1321 shebang: None,
1322 })
1323 );
1324 }
1325
1326 #[test]
1327 fn recipe_with_dependency_arguments() {
1328 let document = Document::from(indoc! {
1329 "
1330 foo arg1 arg2:
1331 echo \"{{arg1}} {{arg2}}\"
1332
1333 bar: (foo 'value1' 'value2')
1334 echo \"bar\"
1335 "
1336 });
1337
1338 assert_eq!(
1339 document.find_recipe("bar"),
1340 Some(Recipe {
1341 name: TextNode {
1342 value: "bar".into(),
1343 range: lsp::Range::at(3, 0, 3, 3)
1344 },
1345 attributes: vec![],
1346 dependencies: vec![Dependency {
1347 name: "foo".into(),
1348 arguments: vec![
1349 TextNode {
1350 value: "'value1'".into(),
1351 range: lsp::Range::at(3, 10, 3, 18),
1352 },
1353 TextNode {
1354 value: "'value2'".into(),
1355 range: lsp::Range::at(3, 19, 3, 27),
1356 }
1357 ],
1358 range: lsp::Range::at(3, 5, 3, 28),
1359 }],
1360 parameters: vec![],
1361 content: "bar: (foo 'value1' 'value2')\n echo \"bar\"".into(),
1362 range: lsp::Range::at(3, 0, 5, 0),
1363 shebang: None,
1364 })
1365 );
1366 }
1367
1368 #[test]
1369 fn recipe_with_shebang() {
1370 let document = Document::from(indoc! {
1371 "
1372 foo:
1373 #!/usr/bin/env bash
1374 echo \"foo\"
1375 "
1376 });
1377
1378 let recipe = document.find_recipe("foo").unwrap();
1379
1380 assert_eq!(
1381 recipe.shebang,
1382 Some(TextNode {
1383 value: "#!/usr/bin/env bash".into(),
1384 range: lsp::Range::at(1, 2, 1, 21),
1385 })
1386 );
1387 }
1388
1389 #[test]
1390 fn recipe_with_multiple_dependencies() {
1391 let document = Document::from(indoc! {
1392 "
1393 foo:
1394 echo \"foo\"
1395
1396 bar:
1397 echo \"bar\"
1398
1399 baz: foo bar
1400 echo \"baz\"
1401 "
1402 });
1403
1404 assert_eq!(
1405 document.find_recipe("baz"),
1406 Some(Recipe {
1407 name: TextNode {
1408 value: "baz".into(),
1409 range: lsp::Range::at(6, 0, 6, 3)
1410 },
1411 attributes: vec![],
1412 dependencies: vec![
1413 Dependency {
1414 name: "foo".into(),
1415 arguments: vec![],
1416 range: lsp::Range::at(6, 5, 6, 8),
1417 },
1418 Dependency {
1419 name: "bar".into(),
1420 arguments: vec![],
1421 range: lsp::Range::at(6, 9, 6, 12),
1422 }
1423 ],
1424 parameters: vec![],
1425 content: "baz: foo bar\n echo \"baz\"".into(),
1426 range: lsp::Range::at(6, 0, 8, 0),
1427 shebang: None,
1428 })
1429 );
1430 }
1431
1432 #[test]
1433 fn recipe_with_parameters() {
1434 let document = Document::from(indoc! {
1435 "
1436 bar target $lol:
1437 echo \"Building {{target}}\"
1438 "
1439 });
1440
1441 assert_eq!(
1442 document.find_recipe("bar"),
1443 Some(Recipe {
1444 name: TextNode {
1445 value: "bar".into(),
1446 range: lsp::Range::at(0, 0, 0, 3)
1447 },
1448 attributes: vec![],
1449 dependencies: vec![],
1450 parameters: vec![
1451 Parameter {
1452 name: "target".into(),
1453 kind: ParameterKind::Normal,
1454 default_value: None,
1455 content: "target".into(),
1456 range: lsp::Range::at(0, 4, 0, 10),
1457 },
1458 Parameter {
1459 name: "lol".into(),
1460 kind: ParameterKind::Export,
1461 default_value: None,
1462 content: "$lol".into(),
1463 range: lsp::Range::at(0, 11, 0, 15),
1464 }
1465 ],
1466 content: "bar target $lol:\n echo \"Building {{target}}\"".into(),
1467 range: lsp::Range::at(0, 0, 2, 0),
1468 shebang: None,
1469 })
1470 );
1471 }
1472
1473 #[test]
1474 fn recipe_with_variadic_parameter() {
1475 let document = Document::from(indoc! {
1476 "
1477 baz first +second=\"default\":
1478 echo \"{{first}} {{second}}\"
1479 "
1480 });
1481
1482 assert_eq!(
1483 document.find_recipe("baz"),
1484 Some(Recipe {
1485 name: TextNode {
1486 value: "baz".into(),
1487 range: lsp::Range::at(0, 0, 0, 3)
1488 },
1489 attributes: vec![],
1490 dependencies: vec![],
1491 parameters: vec![
1492 Parameter {
1493 name: "first".into(),
1494 kind: ParameterKind::Normal,
1495 default_value: None,
1496 content: "first".into(),
1497 range: lsp::Range::at(0, 4, 0, 9),
1498 },
1499 Parameter {
1500 name: "second".into(),
1501 kind: ParameterKind::Variadic(VariadicType::OneOrMore),
1502 default_value: Some("\"default\"".into()),
1503 content: "+second=\"default\"".into(),
1504 range: lsp::Range::at(0, 10, 0, 27),
1505 }
1506 ],
1507 content:
1508 "baz first +second=\"default\":\n echo \"{{first}} {{second}}\""
1509 .into(),
1510 range: lsp::Range::at(0, 0, 2, 0),
1511 shebang: None,
1512 })
1513 );
1514 }
1515
1516 #[test]
1517 fn recipe_without_parameters_or_dependencies() {
1518 let document = Document::from(indoc! {
1519 "
1520 foo:
1521 echo \"foo\"
1522 "
1523 });
1524
1525 assert_eq!(
1526 document.find_recipe("foo"),
1527 Some(Recipe {
1528 name: TextNode {
1529 value: "foo".into(),
1530 range: lsp::Range::at(0, 0, 0, 3)
1531 },
1532 attributes: vec![],
1533 dependencies: vec![],
1534 parameters: vec![],
1535 content: "foo:\n echo \"foo\"".into(),
1536 range: lsp::Range::at(0, 0, 2, 0),
1537 shebang: None,
1538 })
1539 );
1540 }
1541
1542 #[test]
1543 fn recipe_with_attributes() {
1544 let document = Document::from(indoc! {
1545 "
1546 [private]
1547 [description: \"This is a test recipe\"]
1548 [tags(\"test\", \"example\")]
1549 foo:
1550 echo \"foo\"
1551 "
1552 });
1553
1554 let recipe = document.find_recipe("foo").unwrap();
1555
1556 assert_eq!(recipe.attributes.len(), 3);
1557
1558 assert_eq!(
1559 recipe.attributes,
1560 vec![
1561 Attribute {
1562 name: TextNode {
1563 value: "private".into(),
1564 range: lsp::Range::at(0, 1, 0, 8),
1565 },
1566 arguments: vec![],
1567 target: Some(AttributeTarget::Recipe),
1568 range: lsp::Range::at(0, 0, 1, 0),
1569 },
1570 Attribute {
1571 name: TextNode {
1572 value: "description".into(),
1573 range: lsp::Range::at(1, 1, 1, 12),
1574 },
1575 arguments: vec![TextNode {
1576 value: "\"This is a test recipe\"".into(),
1577 range: lsp::Range::at(1, 14, 1, 37),
1578 }],
1579 target: Some(AttributeTarget::Recipe),
1580 range: lsp::Range::at(1, 0, 2, 0),
1581 },
1582 Attribute {
1583 name: TextNode {
1584 value: "tags".into(),
1585 range: lsp::Range::at(2, 1, 2, 5),
1586 },
1587 arguments: vec![
1588 TextNode {
1589 value: "\"test\"".into(),
1590 range: lsp::Range::at(2, 6, 2, 12),
1591 },
1592 TextNode {
1593 value: "\"example\"".into(),
1594 range: lsp::Range::at(2, 14, 2, 23),
1595 }
1596 ],
1597 target: Some(AttributeTarget::Recipe),
1598 range: lsp::Range::at(2, 0, 3, 0),
1599 }
1600 ]
1601 );
1602 }
1603
1604 #[test]
1605 fn list_document_attributes() {
1606 let document = Document::from(indoc! {
1607 "
1608 [private, description: \"desc\"]
1609 foo:
1610 echo \"foo\"
1611
1612 [alias_attr]
1613 alias build := foo
1614
1615 [var_attr(\"value\")]
1616 bar := \"bar\"
1617
1618 [export_attr]
1619 export baz := \"baz\"
1620
1621 [module_attr]
1622 mod utils \"./utils.just\"
1623 "
1624 });
1625
1626 let attributes = document.attributes();
1627
1628 assert_eq!(
1629 attributes,
1630 vec![
1631 Attribute {
1632 arguments: vec![],
1633 name: TextNode {
1634 value: "private".into(),
1635 range: lsp::Range::at(0, 1, 0, 8),
1636 },
1637 range: lsp::Range::at(0, 0, 1, 0),
1638 target: Some(AttributeTarget::Recipe),
1639 },
1640 Attribute {
1641 arguments: vec![TextNode {
1642 value: "\"desc\"".into(),
1643 range: lsp::Range::at(0, 23, 0, 29),
1644 }],
1645 name: TextNode {
1646 value: "description".into(),
1647 range: lsp::Range::at(0, 10, 0, 21),
1648 },
1649 range: lsp::Range::at(0, 0, 1, 0),
1650 target: Some(AttributeTarget::Recipe),
1651 },
1652 Attribute {
1653 arguments: vec![],
1654 name: TextNode {
1655 value: "alias_attr".into(),
1656 range: lsp::Range::at(4, 1, 4, 11),
1657 },
1658 range: lsp::Range::at(4, 0, 5, 0),
1659 target: Some(AttributeTarget::Alias),
1660 },
1661 Attribute {
1662 arguments: vec![TextNode {
1663 value: "\"value\"".into(),
1664 range: lsp::Range::at(7, 10, 7, 17),
1665 }],
1666 name: TextNode {
1667 value: "var_attr".into(),
1668 range: lsp::Range::at(7, 1, 7, 9),
1669 },
1670 range: lsp::Range::at(7, 0, 8, 0),
1671 target: Some(AttributeTarget::Assignment),
1672 },
1673 Attribute {
1674 arguments: vec![],
1675 name: TextNode {
1676 value: "export_attr".into(),
1677 range: lsp::Range::at(10, 1, 10, 12),
1678 },
1679 range: lsp::Range::at(10, 0, 11, 0),
1680 target: Some(AttributeTarget::Assignment),
1681 },
1682 Attribute {
1683 arguments: vec![],
1684 name: TextNode {
1685 value: "module_attr".into(),
1686 range: lsp::Range::at(13, 1, 13, 12),
1687 },
1688 range: lsp::Range::at(13, 0, 14, 0),
1689 target: Some(AttributeTarget::Module),
1690 },
1691 ],
1692 );
1693 }
1694
1695 #[test]
1696 fn imports() {
1697 let document = Document::from(indoc! {
1698 "
1699 import 'foo/bar.just'
1700
1701 a: b
1702 @echo A
1703 "
1704 });
1705
1706 assert_eq!(
1707 document.imports(),
1708 vec![Import {
1709 optional: false,
1710 path: TextNode {
1711 value: "'foo/bar.just'".into(),
1712 range: lsp::Range::at(0, 7, 0, 21),
1713 },
1714 range: lsp::Range::at(0, 0, 0, 21),
1715 }]
1716 );
1717 }
1718
1719 #[test]
1720 fn optional_import() {
1721 let document = Document::from(indoc! {
1722 "
1723 import? 'foo/bar.just'
1724 "
1725 });
1726
1727 assert_eq!(
1728 document.imports(),
1729 vec![Import {
1730 optional: true,
1731 path: TextNode {
1732 value: "'foo/bar.just'".into(),
1733 range: lsp::Range::at(0, 8, 0, 22),
1734 },
1735 range: lsp::Range::at(0, 0, 0, 22),
1736 }]
1737 );
1738 }
1739
1740 #[test]
1741 fn multiple_imports() {
1742 let document = Document::from(indoc! {
1743 "
1744 import 'foo.just'
1745 import? 'bar.just'
1746 "
1747 });
1748
1749 assert_eq!(
1750 document.imports(),
1751 vec![
1752 Import {
1753 optional: false,
1754 path: TextNode {
1755 value: "'foo.just'".into(),
1756 range: lsp::Range::at(0, 7, 0, 17),
1757 },
1758 range: lsp::Range::at(0, 0, 0, 17),
1759 },
1760 Import {
1761 optional: true,
1762 path: TextNode {
1763 value: "'bar.just'".into(),
1764 range: lsp::Range::at(1, 8, 1, 18),
1765 },
1766 range: lsp::Range::at(1, 0, 1, 18),
1767 },
1768 ]
1769 );
1770 }
1771
1772 #[test]
1773 fn module_without_path() {
1774 let document = Document::from(indoc! {
1775 "
1776 mod foo
1777 "
1778 });
1779
1780 assert_eq!(
1781 document.modules(),
1782 vec![Module {
1783 name: TextNode {
1784 value: "foo".into(),
1785 range: lsp::Range::at(0, 4, 0, 7),
1786 },
1787 optional: false,
1788 path: None,
1789 range: lsp::Range::at(0, 0, 0, 7),
1790 }]
1791 );
1792 }
1793
1794 #[test]
1795 fn module_with_path() {
1796 let document = Document::from(indoc! {
1797 r#"
1798 mod foo "./utils.just"
1799 "#
1800 });
1801
1802 assert_eq!(
1803 document.modules(),
1804 vec![Module {
1805 name: TextNode {
1806 value: "foo".into(),
1807 range: lsp::Range::at(0, 4, 0, 7),
1808 },
1809 optional: false,
1810 path: Some(TextNode {
1811 value: "\"./utils.just\"".into(),
1812 range: lsp::Range::at(0, 8, 0, 22),
1813 }),
1814 range: lsp::Range::at(0, 0, 0, 22),
1815 }]
1816 );
1817 }
1818
1819 #[test]
1820 fn optional_module() {
1821 let document = Document::from(indoc! {
1822 "
1823 mod? foo
1824 "
1825 });
1826
1827 assert_eq!(
1828 document.modules(),
1829 vec![Module {
1830 name: TextNode {
1831 value: "foo".into(),
1832 range: lsp::Range::at(0, 5, 0, 8),
1833 },
1834 optional: true,
1835 path: None,
1836 range: lsp::Range::at(0, 0, 0, 8),
1837 }]
1838 );
1839 }
1840
1841 #[test]
1842 fn multiple_modules() {
1843 let document = Document::from(indoc! {
1844 r#"
1845 mod foo
1846 mod? bar "bar.just"
1847 "#
1848 });
1849
1850 assert_eq!(
1851 document.modules(),
1852 vec![
1853 Module {
1854 name: TextNode {
1855 value: "foo".into(),
1856 range: lsp::Range::at(0, 4, 0, 7),
1857 },
1858 optional: false,
1859 path: None,
1860 range: lsp::Range::at(0, 0, 0, 7),
1861 },
1862 Module {
1863 name: TextNode {
1864 value: "bar".into(),
1865 range: lsp::Range::at(1, 5, 1, 8),
1866 },
1867 optional: true,
1868 path: Some(TextNode {
1869 value: "\"bar.just\"".into(),
1870 range: lsp::Range::at(1, 9, 1, 19),
1871 }),
1872 range: lsp::Range::at(1, 0, 1, 19),
1873 },
1874 ]
1875 );
1876 }
1877
1878 #[test]
1879 fn list_function_calls() {
1880 let document = Document::from(indoc! {
1881 "
1882 foo:
1883 echo {{arch()}}
1884 echo {{env_var(\"HOME\", \"fallback\")}}
1885 "
1886 });
1887
1888 let calls = document.function_calls();
1889
1890 assert_eq!(
1891 calls,
1892 vec![
1893 FunctionCall {
1894 arguments: vec![],
1895 name: TextNode {
1896 value: "arch".into(),
1897 range: lsp::Range::at(1, 9, 1, 13),
1898 },
1899 range: lsp::Range::at(1, 9, 1, 15),
1900 },
1901 FunctionCall {
1902 arguments: vec![
1903 TextNode {
1904 value: "\"HOME\"".into(),
1905 range: lsp::Range::at(2, 17, 2, 23),
1906 },
1907 TextNode {
1908 value: "\"fallback\"".into(),
1909 range: lsp::Range::at(2, 25, 2, 35),
1910 },
1911 ],
1912 name: TextNode {
1913 value: "env_var".into(),
1914 range: lsp::Range::at(2, 9, 2, 16),
1915 },
1916 range: lsp::Range::at(2, 9, 2, 36),
1917 },
1918 ],
1919 );
1920 }
1921
1922 #[test]
1923 fn list_functions() {
1924 let document = Document::from(indoc! {
1925 "
1926 hello(name) := f\"Hello, \" + name
1927
1928 greet(a, b) := hello(a) + \" and \" + hello(b)
1929 "
1930 });
1931
1932 assert_eq!(
1933 document.functions(),
1934 vec![
1935 Function {
1936 name: TextNode {
1937 value: "hello".into(),
1938 range: lsp::Range::at(0, 0, 0, 5),
1939 },
1940 parameters: vec![TextNode {
1941 value: "name".into(),
1942 range: lsp::Range::at(0, 6, 0, 10),
1943 }],
1944 body: "f\"Hello, \" + name".into(),
1945 content: "hello(name) := f\"Hello, \" + name".into(),
1946 range: lsp::Range::at(0, 0, 1, 0),
1947 },
1948 Function {
1949 name: TextNode {
1950 value: "greet".into(),
1951 range: lsp::Range::at(2, 0, 2, 5),
1952 },
1953 parameters: vec![
1954 TextNode {
1955 value: "a".into(),
1956 range: lsp::Range::at(2, 6, 2, 7),
1957 },
1958 TextNode {
1959 value: "b".into(),
1960 range: lsp::Range::at(2, 9, 2, 10),
1961 },
1962 ],
1963 body: "hello(a) + \" and \" + hello(b)".into(),
1964 content: "greet(a, b) := hello(a) + \" and \" + hello(b)".into(),
1965 range: lsp::Range::at(2, 0, 3, 0),
1966 },
1967 ],
1968 );
1969 }
1970
1971 #[test]
1972 fn find_function() {
1973 let document = Document::from(indoc! {
1974 "
1975 foo(x) := x + \"!\"
1976 "
1977 });
1978
1979 assert!(document.find_function("foo").is_some());
1980 assert!(document.find_function("bar").is_none());
1981 }
1982
1983 #[test]
1984 fn function_no_parameters() {
1985 let document = Document::from(indoc! {
1986 "
1987 foo() := \"bar\"
1988 "
1989 });
1990
1991 assert_eq!(
1992 document.functions(),
1993 vec![Function {
1994 name: TextNode {
1995 value: "foo".into(),
1996 range: lsp::Range::at(0, 0, 0, 3),
1997 },
1998 parameters: vec![],
1999 body: "\"bar\"".into(),
2000 content: "foo() := \"bar\"".into(),
2001 range: lsp::Range::at(0, 0, 1, 0),
2002 }],
2003 );
2004 }
2005}