Skip to main content

just_lsp/
document.rs

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  /// Applies incremental edits from the client and reparses the syntax tree.
76  ///
77  /// # Errors
78  ///
79  /// Returns an [`Error`] if tree-sitter fails to parse the updated document.
80  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  /// # Errors
180  ///
181  /// Returns an [`Error`] if formatting fails.
182  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  /// # Errors
369  ///
370  /// Returns an [`Error`] if the tree-sitter parser cannot be created or the
371  /// contents fail to parse.
372  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  /// Returns the syntax tree node at the given LSP `Position`.
386  #[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  /// Parses the current document contents and updates the cached syntax tree.
394  ///
395  /// # Errors
396  ///
397  /// Returns an [`Error`] if the tree-sitter parser cannot be created or the
398  /// contents fail to parse.
399  pub fn parse(&mut self) -> Result {
400    let mut parser = Parser::new();
401
402    // SAFETY: tree_sitter_just returns a static language definition.
403    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}