1use smol_str::SmolStr;
2use text_size::{TextRange, TextSize};
3
4use crate::{
5 DirectiveKind, GuardKind, ShellKind, ShellOperator, ShellSelection, SyntaxKind, SyntaxNode,
6 TaskShellRef,
7};
8
9#[derive(Debug, Clone)]
17pub struct DocumentNode {
18 syntax: SyntaxNode,
19}
20
21#[derive(Debug, Clone)]
29pub struct DirectiveNode {
30 syntax: SyntaxNode,
31}
32
33#[derive(Debug, Clone)]
41pub struct MetadataNode {
42 syntax: SyntaxNode,
43}
44
45#[derive(Debug, Clone)]
53pub struct NamespaceNode {
54 syntax: SyntaxNode,
55}
56
57#[derive(Debug, Clone)]
65pub struct TaskNode {
66 syntax: SyntaxNode,
67}
68
69#[derive(Debug, Clone)]
70pub struct TaskHeaderNode {
71 syntax: SyntaxNode,
72}
73
74#[derive(Debug, Clone)]
75pub struct ParameterListNode {
76 syntax: SyntaxNode,
77}
78
79#[derive(Debug, Clone)]
80pub struct ParameterNode {
81 syntax: SyntaxNode,
82}
83
84#[derive(Debug, Clone)]
85pub struct ConditionClauseNode {
86 syntax: SyntaxNode,
87}
88
89#[derive(Debug, Clone)]
90pub struct DependencyClauseNode {
91 syntax: SyntaxNode,
92}
93
94#[derive(Debug, Clone)]
95pub struct ShellClauseNode {
96 syntax: SyntaxNode,
97}
98
99#[derive(Debug, Clone)]
100pub struct HeaderTerminatorNode {
101 syntax: SyntaxNode,
102}
103
104#[derive(Debug, Clone, PartialEq, Eq)]
106pub enum TaskStepNode {
107 Command(TaskCommandNode),
108 CommandBlock(TaskCommandBlockNode),
109}
110
111#[derive(Debug, Clone, PartialEq, Eq)]
113pub struct TaskCommandNode {
114 pub text: SmolStr,
115 pub range: TextRange,
116}
117
118#[derive(Debug, Clone, PartialEq, Eq)]
120pub struct TaskCommandBlockNode {
121 pub source: SmolStr,
122 pub range: TextRange,
123 pub line_ranges: Vec<TextRange>,
124 pub marker_ranges: Vec<TextRange>,
125}
126
127#[derive(Debug, Clone, PartialEq, Eq)]
135pub struct TaskDependencyRef {
136 pub name: SmolStr,
137 pub range: TextRange,
138 pub arguments: Vec<TaskDependencyArgRef>,
139 pub invocation_range: TextRange,
140 pub stage: usize,
141}
142
143#[derive(Debug, Clone, PartialEq, Eq)]
145pub struct TaskDependencyArgRef {
146 pub value: SmolStr,
147 pub range: TextRange,
148}
149
150#[derive(Debug, Clone, PartialEq, Eq)]
152pub struct TaskParamRef {
153 pub name: SmolStr,
154 pub range: TextRange,
155 pub default_value: Option<SmolStr>,
156 pub is_slice: bool,
157}
158
159#[derive(Debug, Clone, PartialEq, Eq)]
160pub struct TaskGuardRef {
161 pub kind: GuardKind,
162 pub argument: SmolStr,
163 pub range: TextRange,
164 pub name_range: TextRange,
165}
166
167#[derive(Debug, Clone, Default, PartialEq, Eq)]
175pub struct TaskHeaderInfo {
176 pub params: Option<SmolStr>,
177 pub param_refs: Vec<TaskParamRef>,
178 pub guard: Option<SmolStr>,
179 pub guards: Vec<TaskGuardRef>,
180 pub dependencies: Option<SmolStr>,
181 pub shell: Option<TaskShellRef>,
182 pub dependency_refs: Vec<TaskDependencyRef>,
183}
184
185impl DocumentNode {
186 pub fn cast(syntax: SyntaxNode) -> Option<Self> {
194 (syntax.kind() == SyntaxKind::Document).then_some(Self { syntax })
195 }
196
197 pub fn syntax(&self) -> &SyntaxNode {
205 &self.syntax
206 }
207
208 pub fn range(&self) -> TextRange {
216 self.syntax.text_range()
217 }
218
219 pub fn directives(&self) -> impl Iterator<Item = DirectiveNode> + '_ {
227 self.syntax.children().filter_map(DirectiveNode::cast)
228 }
229
230 pub fn metadata(&self) -> impl Iterator<Item = MetadataNode> + '_ {
238 self.syntax.children().filter_map(MetadataNode::cast)
239 }
240
241 pub fn namespaces(&self) -> impl Iterator<Item = NamespaceNode> + '_ {
249 self.syntax.children().filter_map(NamespaceNode::cast)
250 }
251
252 pub fn tasks(&self) -> impl Iterator<Item = TaskNode> + '_ {
260 self.syntax.children().filter_map(TaskNode::cast)
261 }
262}
263
264impl DirectiveNode {
265 pub fn cast(syntax: SyntaxNode) -> Option<Self> {
273 (syntax.kind() == SyntaxKind::Directive).then_some(Self { syntax })
274 }
275
276 pub fn range(&self) -> TextRange {
284 self.syntax.text_range()
285 }
286
287 pub fn keyword_range(&self) -> Option<TextRange> {
295 let mut tokens = self
296 .syntax
297 .children_with_tokens()
298 .filter_map(|element| element.into_token())
299 .filter(|token| {
300 !matches!(
301 token.kind(),
302 SyntaxKind::Whitespace | SyntaxKind::Indent | SyntaxKind::Newline
303 )
304 });
305 let bang = tokens.find(|token| token.kind() == SyntaxKind::Bang)?;
306 let keyword = tokens.next()?;
307 Some(TextRange::new(
308 bang.text_range().start(),
309 keyword.text_range().end(),
310 ))
311 }
312
313 pub fn name(&self) -> Option<SmolStr> {
321 non_trivia_token_texts(&self.syntax).nth(1)
322 }
323
324 pub fn directive_kind(&self) -> Option<DirectiveKind> {
326 self.name().map(|name| DirectiveKind::parse(&name))
327 }
328
329 pub fn value(&self) -> Option<SmolStr> {
337 let value = non_trivia_token_texts(&self.syntax)
338 .skip(2)
339 .collect::<Vec<_>>()
340 .join(" ");
341 (!value.is_empty()).then(|| SmolStr::new(value))
342 }
343
344 pub fn raw_value(&self) -> Option<SmolStr> {
346 let mut non_trivia = 0usize;
347 let mut value = String::new();
348
349 for token in self
350 .syntax
351 .children_with_tokens()
352 .filter_map(|element| element.into_token())
353 {
354 if token.kind() == SyntaxKind::Newline {
355 break;
356 }
357 if !matches!(
358 token.kind(),
359 SyntaxKind::Whitespace | SyntaxKind::Indent | SyntaxKind::Comment
360 ) {
361 non_trivia += 1;
362 }
363 if non_trivia >= 2 && !(non_trivia == 2 && token.kind() == SyntaxKind::Ident) {
364 value.push_str(token.text());
365 }
366 }
367
368 let value = value.trim();
369 (!value.is_empty()).then(|| SmolStr::new(value))
370 }
371
372 pub fn argument_name_range(&self) -> Option<TextRange> {
374 self.syntax
375 .children_with_tokens()
376 .filter_map(|element| element.into_token())
377 .filter(|token| matches!(token.kind(), SyntaxKind::Ident | SyntaxKind::ShellKw))
378 .nth(1)
379 .map(|token| token.text_range())
380 }
381}
382
383impl MetadataNode {
384 pub fn cast(syntax: SyntaxNode) -> Option<Self> {
392 (syntax.kind() == SyntaxKind::MetadataComment).then_some(Self { syntax })
393 }
394
395 pub fn range(&self) -> TextRange {
403 self.syntax.text_range()
404 }
405
406 pub fn text(&self) -> Option<SmolStr> {
414 let text = self.syntax.text().to_string();
415 let text = text.trim();
416 let text = if self.syntax.kind() == SyntaxKind::MetadataComment {
417 let close = text.find(']')?;
418 text.get(close + 1..)?.trim()
419 } else {
420 text.strip_prefix('#')?.trim()
421 };
422 (!text.is_empty()).then(|| SmolStr::new(text))
423 }
424
425 pub fn field(&self) -> Option<(SmolStr, SmolStr)> {
427 if self.syntax.kind() != SyntaxKind::MetadataComment {
428 return None;
429 }
430 let text = self.syntax.text().to_string();
431 let text = text.trim().strip_prefix('[')?;
432 let close = text.find(']')?;
433 let name = &text[..close];
434 if name.is_empty()
435 || !name.chars().all(|character| {
436 character.is_ascii_alphanumeric() || matches!(character, '_' | '-')
437 })
438 {
439 return None;
440 }
441
442 Some((SmolStr::new(name), SmolStr::new(text[close + 1..].trim())))
443 }
444
445 pub fn field_range(&self) -> Option<TextRange> {
447 if self.syntax.kind() != SyntaxKind::MetadataComment {
448 return None;
449 }
450 let text = self.syntax.text().to_string();
451 let text = text.trim().strip_prefix('[')?;
452 let close = text.find(']')?;
453 let name = &text[..close];
454 if name.is_empty()
455 || !name.chars().all(|character| {
456 character.is_ascii_alphanumeric() || matches!(character, '_' | '-')
457 })
458 {
459 return None;
460 }
461
462 let start = self.syntax.text_range().start() + TextSize::from(1);
463 Some(TextRange::new(
464 start,
465 start + TextSize::from(name.len() as u32),
466 ))
467 }
468
469 pub fn tag_range(&self) -> Option<TextRange> {
471 let field = self.field_range()?;
472 let delimiter = TextSize::from(1);
473 Some(TextRange::new(
474 field.start() - delimiter,
475 field.end() + delimiter,
476 ))
477 }
478}
479
480impl NamespaceNode {
481 pub fn cast(syntax: SyntaxNode) -> Option<Self> {
489 (syntax.kind() == SyntaxKind::NamespaceBlock).then_some(Self { syntax })
490 }
491
492 pub fn range(&self) -> TextRange {
500 self.syntax.text_range()
501 }
502
503 pub fn name(&self) -> Option<SmolStr> {
511 self.syntax
512 .children_with_tokens()
513 .filter_map(|element| element.into_token())
514 .find(|token| token.kind() == SyntaxKind::Ident)
515 .map(|token| SmolStr::new(token.text()))
516 }
517
518 pub fn name_range(&self) -> Option<TextRange> {
526 self.syntax
527 .children_with_tokens()
528 .filter_map(|element| element.into_token())
529 .find(|token| token.kind() == SyntaxKind::Ident)
530 .map(|token| token.text_range())
531 }
532
533 pub fn is_group(&self) -> bool {
535 self.syntax
536 .children_with_tokens()
537 .filter_map(|element| element.into_token())
538 .any(|token| token.kind() == SyntaxKind::GroupKw)
539 }
540
541 pub fn is_close(&self) -> bool {
543 self.syntax.text().to_string().trim() == "}"
544 }
545
546 pub fn has_open_brace(&self) -> bool {
548 self.syntax
549 .descendants_with_tokens()
550 .filter_map(|element| element.into_token())
551 .any(|token| token.kind() == SyntaxKind::LBrace)
552 }
553
554 pub fn is_empty(&self) -> bool {
556 if self.is_close() {
557 return false;
558 }
559 self.name().is_none()
560 }
561}
562
563impl TaskNode {
564 pub fn cast(syntax: SyntaxNode) -> Option<Self> {
572 (syntax.kind() == SyntaxKind::TaskDecl).then_some(Self { syntax })
573 }
574
575 pub fn range(&self) -> TextRange {
583 self.syntax.text_range()
584 }
585
586 pub fn name_range(&self) -> Option<TextRange> {
594 self.header()?.name_range()
595 }
596
597 pub fn name(&self) -> Option<SmolStr> {
605 self.header()?.name()
606 }
607
608 pub fn header_text(&self) -> Option<SmolStr> {
616 let header = self.header()?.syntax.text().to_string();
617 let header = header.trim().trim_end_matches(':').trim_end();
618 (!header.is_empty()).then(|| SmolStr::new(header))
619 }
620
621 pub fn header(&self) -> Option<TaskHeaderNode> {
622 self.syntax.children().find_map(TaskHeaderNode::cast)
623 }
624
625 pub fn uses_multiline_header(&self) -> bool {
626 self.header()
627 .is_some_and(|header| header.syntax.text().to_string().contains(['\n', '\r']))
628 }
629
630 pub fn header_info(&self) -> TaskHeaderInfo {
638 self.header()
639 .map_or_else(TaskHeaderInfo::default, |header| header.info())
640 }
641
642 pub fn commands(&self) -> std::vec::IntoIter<SmolStr> {
650 self.steps()
651 .map(|step| match step {
652 TaskStepNode::Command(command) => command.text,
653 TaskStepNode::CommandBlock(block) => block.source,
654 })
655 .collect::<Vec<_>>()
656 .into_iter()
657 }
658
659 pub fn steps(&self) -> std::vec::IntoIter<TaskStepNode> {
661 task_body_steps(&self.syntax)
662 .collect::<Vec<_>>()
663 .into_iter()
664 }
665}
666
667#[derive(Debug, Clone, Copy)]
668struct BodyLine<'a> {
669 text: &'a str,
670 start: usize,
671 end_with_newline: usize,
672}
673
674fn task_body_steps(node: &SyntaxNode) -> impl Iterator<Item = TaskStepNode> + '_ {
675 let source = node.text().to_string();
676 let body_start = node
677 .children()
678 .find(|child| child.kind() == SyntaxKind::TaskHeader)
679 .map(|header| usize::from(header.text_range().end() - node.text_range().start()))
680 .unwrap_or_else(|| first_line_end(&source).unwrap_or(source.len()));
681 let base = usize::from(node.text_range().start());
682 let lines = body_lines(&source, body_start).collect::<Vec<_>>();
683 let mut steps = Vec::new();
684 let mut index = 0usize;
685
686 while index < lines.len() {
687 let line = lines[index];
688 let trimmed = line.text.trim_start_matches([' ', '\t']);
689 if block_line_content(trimmed).is_none() {
690 if !trimmed.is_empty() && !trimmed.starts_with("//") {
691 let indent = line.text.len() - trimmed.len();
692 steps.push(TaskStepNode::Command(TaskCommandNode {
693 text: SmolStr::new(trimmed),
694 range: text_range(
695 base + line.start + indent,
696 base + line.start + line.text.len(),
697 ),
698 }));
699 }
700 index += 1;
701 continue;
702 }
703
704 let block_start = line.start;
705 let mut block_end = line.end_with_newline;
706 let mut block_source = String::new();
707 let mut line_ranges = Vec::new();
708 let mut marker_ranges = Vec::new();
709
710 while index < lines.len() {
711 let block_line = lines[index];
712 let trimmed = block_line.text.trim_start_matches([' ', '\t']);
713 let Some(content) = block_line_content(trimmed) else {
714 break;
715 };
716 let indent = block_line.text.len() - trimmed.len();
717 let marker_start = base + block_line.start + indent;
718 block_source.push_str(content);
719 block_source.push('\n');
720 line_ranges.push(text_range(
721 base + block_line.start,
722 base + block_line.start + block_line.text.len(),
723 ));
724 marker_ranges.push(text_range(marker_start, marker_start + 1));
725 block_end = block_line.end_with_newline;
726 index += 1;
727 }
728
729 steps.push(TaskStepNode::CommandBlock(TaskCommandBlockNode {
730 source: SmolStr::new(block_source),
731 range: text_range(base + block_start, base + block_end),
732 line_ranges,
733 marker_ranges,
734 }));
735 }
736
737 steps.into_iter()
738}
739
740fn first_line_end(source: &str) -> Option<usize> {
741 let (index, newline) = source
742 .char_indices()
743 .find(|(_, character)| matches!(character, '\n' | '\r'))?;
744 let newline_len = if newline == '\r' && source.as_bytes().get(index + 1) == Some(&b'\n') {
745 2
746 } else {
747 1
748 };
749 Some(index + newline_len)
750}
751
752fn body_lines(source: &str, start: usize) -> impl Iterator<Item = BodyLine<'_>> {
753 let mut cursor = start;
754 std::iter::from_fn(move || {
755 if cursor >= source.len() {
756 return None;
757 }
758 let line_start = cursor;
759 let rest = &source[cursor..];
760 let newline = rest
761 .char_indices()
762 .find(|(_, character)| matches!(character, '\n' | '\r'));
763 let (line_end, newline_len) = match newline {
764 Some((offset, '\r')) if rest.as_bytes().get(offset + 1) == Some(&b'\n') => {
765 (cursor + offset, 2)
766 }
767 Some((offset, _)) => (cursor + offset, 1),
768 None => (source.len(), 0),
769 };
770 cursor = line_end + newline_len;
771 Some(BodyLine {
772 text: &source[line_start..line_end],
773 start: line_start,
774 end_with_newline: cursor,
775 })
776 })
777}
778
779fn block_line_content(line: &str) -> Option<&str> {
780 let rest = line.strip_prefix('|')?;
781 match rest.as_bytes().first() {
782 None => Some(rest),
783 Some(b' ' | b'\t') => Some(&rest[1..]),
784 Some(_) => None,
785 }
786}
787
788fn text_range(start: usize, end: usize) -> TextRange {
789 TextRange::new(TextSize::from(start as u32), TextSize::from(end as u32))
790}
791
792impl TaskHeaderNode {
793 pub fn cast(syntax: SyntaxNode) -> Option<Self> {
794 (syntax.kind() == SyntaxKind::TaskHeader).then_some(Self { syntax })
795 }
796
797 pub fn range(&self) -> TextRange {
798 self.syntax.text_range()
799 }
800
801 pub fn name(&self) -> Option<SmolStr> {
802 self.name_node()?
803 .first_token()
804 .map(|token| SmolStr::new(token.text()))
805 }
806
807 pub fn name_range(&self) -> Option<TextRange> {
808 self.name_node()?
809 .first_token()
810 .map(|token| token.text_range())
811 }
812
813 pub fn parameter_list(&self) -> Option<ParameterListNode> {
814 self.syntax.children().find_map(ParameterListNode::cast)
815 }
816
817 pub fn conditions(&self) -> impl Iterator<Item = ConditionClauseNode> + '_ {
818 self.syntax.children().filter_map(ConditionClauseNode::cast)
819 }
820
821 pub fn dependencies(&self) -> impl Iterator<Item = DependencyClauseNode> + '_ {
822 self.syntax
823 .children()
824 .filter_map(DependencyClauseNode::cast)
825 }
826
827 pub fn shell(&self) -> Option<ShellClauseNode> {
828 self.syntax.children().find_map(ShellClauseNode::cast)
829 }
830
831 pub fn terminator(&self) -> Option<HeaderTerminatorNode> {
832 self.syntax.children().find_map(HeaderTerminatorNode::cast)
833 }
834
835 pub fn info(&self) -> TaskHeaderInfo {
836 parse_task_header(self)
837 }
838
839 fn name_node(&self) -> Option<SyntaxNode> {
840 self.syntax
841 .children()
842 .find(|node| node.kind() == SyntaxKind::TaskName)
843 }
844}
845
846impl ParameterListNode {
847 pub fn cast(syntax: SyntaxNode) -> Option<Self> {
848 (syntax.kind() == SyntaxKind::ParameterList).then_some(Self { syntax })
849 }
850
851 pub fn range(&self) -> TextRange {
852 self.syntax.text_range()
853 }
854
855 pub fn parameters(&self) -> impl Iterator<Item = ParameterNode> + '_ {
856 self.syntax.children().filter_map(ParameterNode::cast)
857 }
858}
859
860impl ParameterNode {
861 pub fn cast(syntax: SyntaxNode) -> Option<Self> {
862 (syntax.kind() == SyntaxKind::Parameter).then_some(Self { syntax })
863 }
864
865 pub fn range(&self) -> TextRange {
866 self.syntax.text_range()
867 }
868
869 pub fn name(&self) -> Option<SmolStr> {
870 self.name_token().map(|token| SmolStr::new(token.text()))
871 }
872
873 pub fn name_range(&self) -> Option<TextRange> {
874 self.name_token().map(|token| token.text_range())
875 }
876
877 pub fn default_value(&self) -> Option<SmolStr> {
878 node_tokens(&self.syntax)
879 .find(|token| token.kind() == SyntaxKind::String)
880 .and_then(|token| {
881 token
882 .text()
883 .strip_prefix('"')?
884 .strip_suffix('"')
885 .map(SmolStr::new)
886 })
887 }
888
889 pub fn is_slice(&self) -> bool {
890 self.syntax
891 .text()
892 .to_string()
893 .split('=')
894 .next()
895 .is_some_and(|name| name.trim_end().ends_with(".."))
896 }
897
898 fn name_token(&self) -> Option<crate::cst::SyntaxToken> {
899 node_tokens(&self.syntax)
900 .find(|token| matches!(token.kind(), SyntaxKind::Ident | SyntaxKind::ShellKw))
901 }
902}
903
904macro_rules! clause_node {
905 ($type:ident, $kind:ident) => {
906 impl $type {
907 pub fn cast(syntax: SyntaxNode) -> Option<Self> {
908 (syntax.kind() == SyntaxKind::$kind).then_some(Self { syntax })
909 }
910
911 pub fn range(&self) -> TextRange {
912 self.syntax.text_range()
913 }
914
915 pub fn text(&self) -> SmolStr {
916 SmolStr::new(self.syntax.text().to_string().trim())
917 }
918 }
919 };
920}
921
922clause_node!(ConditionClauseNode, ConditionClause);
923clause_node!(DependencyClauseNode, DependencyClause);
924clause_node!(ShellClauseNode, ShellClause);
925clause_node!(HeaderTerminatorNode, HeaderTerminator);
926
927impl ConditionClauseNode {
928 pub fn operator_range(&self) -> Option<TextRange> {
930 node_tokens(&self.syntax)
931 .find(|token| token.kind() == SyntaxKind::Question)
932 .map(|token| token.text_range())
933 }
934}
935
936impl DependencyClauseNode {
937 pub fn operator_range(&self) -> Option<TextRange> {
939 node_tokens(&self.syntax)
940 .find(|token| token.kind() == SyntaxKind::Amp)
941 .map(|token| token.text_range())
942 }
943
944 pub fn parallel_group_delimiter_ranges(&self) -> Vec<TextRange> {
946 let tokens = node_tokens(&self.syntax).collect::<Vec<_>>();
947 if !tokens
948 .iter()
949 .any(|token| token.kind() == SyntaxKind::LParen)
950 || !tokens
951 .iter()
952 .any(|token| token.kind() == SyntaxKind::RParen)
953 {
954 return Vec::new();
955 }
956
957 tokens
958 .into_iter()
959 .filter(|token| {
960 matches!(
961 token.kind(),
962 SyntaxKind::LParen | SyntaxKind::Comma | SyntaxKind::RParen
963 )
964 })
965 .map(|token| token.text_range())
966 .collect()
967 }
968}
969
970impl ShellClauseNode {
971 pub fn operator(&self) -> Option<ShellOperator> {
973 node_tokens(&self.syntax).find_map(|token| match token.kind() {
974 SyntaxKind::ShellKw => Some(ShellOperator::Required),
975 SyntaxKind::ShellFallbackKw => Some(ShellOperator::Fallback),
976 _ => None,
977 })
978 }
979
980 pub fn shell_name(&self) -> Option<SmolStr> {
982 node_tokens(&self.syntax)
983 .find(|token| token.kind() == SyntaxKind::Ident)
984 .map(|token| SmolStr::new(token.text()))
985 }
986
987 pub fn content_range(&self) -> Option<TextRange> {
989 let mut tokens = node_tokens(&self.syntax).filter(|token| {
990 !matches!(
991 token.kind(),
992 SyntaxKind::Whitespace | SyntaxKind::Indent | SyntaxKind::Newline
993 )
994 });
995 let first = tokens.next()?;
996 let end = tokens.last().unwrap_or_else(|| first.clone());
997 Some(TextRange::new(
998 first.text_range().start(),
999 end.text_range().end(),
1000 ))
1001 }
1002}
1003
1004fn parse_task_header(node: &TaskHeaderNode) -> TaskHeaderInfo {
1005 let mut info = TaskHeaderInfo::default();
1006
1007 if let Some(parameters) = node.parameter_list() {
1008 let refs = parameters
1009 .parameters()
1010 .filter_map(|parameter| {
1011 Some(TaskParamRef {
1012 name: parameter.name()?,
1013 range: parameter.name_range()?,
1014 default_value: parameter.default_value(),
1015 is_slice: parameter.is_slice(),
1016 })
1017 })
1018 .collect::<Vec<_>>();
1019 if !refs.is_empty() {
1020 info.params = Some(SmolStr::new(
1021 refs.iter()
1022 .map(render_param_ref)
1023 .collect::<Vec<_>>()
1024 .join(", "),
1025 ));
1026 }
1027 info.param_refs = refs;
1028 }
1029
1030 info.guards = node.conditions().filter_map(parse_guard_ref).collect();
1031 info.guard = info
1032 .guards
1033 .first()
1034 .map(|guard| SmolStr::new(format!("@{}(\"{}\")", guard.kind, guard.argument)));
1035
1036 let mut dependency_text = Vec::new();
1037 for (stage, clause) in node.dependencies().enumerate() {
1038 dependency_text.push(clause.text().trim_start_matches('&').trim().to_string());
1039 parse_dependency_clause(&clause.syntax, stage, &mut info.dependency_refs);
1040 }
1041 if !dependency_text.is_empty() {
1042 info.dependencies = Some(SmolStr::new(dependency_text.join(" & ")));
1043 }
1044
1045 if let Some(shell) = node.shell() {
1046 let tokens = node_tokens(&shell.syntax)
1047 .filter(|token| {
1048 !matches!(
1049 token.kind(),
1050 SyntaxKind::Whitespace | SyntaxKind::Indent | SyntaxKind::Newline
1051 )
1052 })
1053 .collect::<Vec<_>>();
1054 let operator = tokens.first().and_then(|token| match token.kind() {
1055 SyntaxKind::ShellKw => Some(ShellOperator::Required),
1056 SyntaxKind::ShellFallbackKw => Some(ShellOperator::Fallback),
1057 _ => None,
1058 });
1059 let kind = tokens
1060 .iter()
1061 .rev()
1062 .find(|token| token.kind() == SyntaxKind::Ident)
1063 .map(|token| ShellKind::parse(token.text()));
1064 info.shell = operator.zip(kind).map(|(operator, kind)| TaskShellRef {
1065 selection: ShellSelection { kind, operator },
1066 range: shell.content_range().unwrap_or_else(|| shell.range()),
1067 });
1068 }
1069
1070 info
1071}
1072
1073fn render_param_ref(parameter: &TaskParamRef) -> String {
1074 let suffix = if parameter.is_slice { ".." } else { "" };
1075 match ¶meter.default_value {
1076 Some(value) => format!("{}{suffix}=\"{value}\"", parameter.name),
1077 None => format!("{}{suffix}", parameter.name),
1078 }
1079}
1080
1081fn parse_guard_ref(clause: ConditionClauseNode) -> Option<TaskGuardRef> {
1082 let tokens = node_tokens(&clause.syntax)
1083 .filter(|token| {
1084 !matches!(
1085 token.kind(),
1086 SyntaxKind::Whitespace | SyntaxKind::Indent | SyntaxKind::Newline
1087 )
1088 })
1089 .collect::<Vec<_>>();
1090 let name_token = tokens
1091 .iter()
1092 .find(|token| token.kind() == SyntaxKind::Ident)?;
1093 let name = name_token.text();
1094 let name_start = tokens
1095 .iter()
1096 .find(|token| token.kind() == SyntaxKind::At)
1097 .map_or_else(
1098 || name_token.text_range().start(),
1099 |token| token.text_range().start(),
1100 );
1101 let argument = tokens
1102 .iter()
1103 .find(|token| token.kind() == SyntaxKind::String)?
1104 .text()
1105 .strip_prefix('"')?
1106 .strip_suffix('"')?;
1107
1108 Some(TaskGuardRef {
1109 kind: GuardKind::parse(name),
1110 argument: SmolStr::new(argument),
1111 range: clause.range(),
1112 name_range: TextRange::new(name_start, name_token.text_range().end()),
1113 })
1114}
1115
1116fn parse_dependency_clause(node: &SyntaxNode, stage: usize, refs: &mut Vec<TaskDependencyRef>) {
1117 let mut tokens = node_tokens(node)
1118 .filter(|token| {
1119 !matches!(
1120 token.kind(),
1121 SyntaxKind::Whitespace | SyntaxKind::Indent | SyntaxKind::Newline
1122 )
1123 })
1124 .collect::<Vec<_>>();
1125 if tokens
1126 .first()
1127 .is_some_and(|token| token.kind() == SyntaxKind::Amp)
1128 {
1129 tokens.remove(0);
1130 }
1131 if tokens
1132 .first()
1133 .is_some_and(|token| token.kind() == SyntaxKind::LParen)
1134 && tokens
1135 .last()
1136 .is_some_and(|token| token.kind() == SyntaxKind::RParen)
1137 {
1138 tokens.remove(0);
1139 tokens.pop();
1140 }
1141
1142 let mut invocation_start = 0usize;
1143 let mut depth = 0usize;
1144 for index in 0..=tokens.len() {
1145 let at_separator =
1146 index == tokens.len() || (tokens[index].kind() == SyntaxKind::Comma && depth == 0);
1147 if at_separator {
1148 if let Some(reference) =
1149 parse_dependency_invocation(&tokens[invocation_start..index], stage)
1150 {
1151 refs.push(reference);
1152 }
1153 invocation_start = index + 1;
1154 continue;
1155 }
1156
1157 match tokens[index].kind() {
1158 SyntaxKind::LParen => depth += 1,
1159 SyntaxKind::RParen => depth = depth.saturating_sub(1),
1160 _ => {}
1161 }
1162 }
1163}
1164
1165fn parse_dependency_invocation(
1166 tokens: &[crate::cst::SyntaxToken],
1167 stage: usize,
1168) -> Option<TaskDependencyRef> {
1169 let first = tokens.first()?;
1170 let invocation_end = tokens.last()?.text_range().end();
1171 let argument_start = tokens
1172 .iter()
1173 .position(|token| token.kind() == SyntaxKind::LParen)
1174 .unwrap_or(tokens.len());
1175 let name_tokens = &tokens[..argument_start];
1176 let name_start = name_tokens.first()?.text_range().start();
1177 let name_end = name_tokens.last()?.text_range().end();
1178 let name = name_tokens
1179 .iter()
1180 .map(|token| token.text())
1181 .collect::<String>();
1182 let arguments = tokens
1183 .iter()
1184 .skip(argument_start.saturating_add(1))
1185 .filter(|token| token.kind() == SyntaxKind::String)
1186 .filter_map(|token| {
1187 let value = token.text().strip_prefix('"')?.strip_suffix('"')?;
1188 Some(TaskDependencyArgRef {
1189 value: SmolStr::new(value),
1190 range: token.text_range(),
1191 })
1192 })
1193 .collect();
1194
1195 Some(TaskDependencyRef {
1196 name: SmolStr::new(name),
1197 range: TextRange::new(name_start, name_end),
1198 arguments,
1199 invocation_range: TextRange::new(first.text_range().start(), invocation_end),
1200 stage,
1201 })
1202}
1203
1204fn node_tokens(node: &SyntaxNode) -> impl Iterator<Item = crate::cst::SyntaxToken> + '_ {
1205 node.descendants_with_tokens()
1206 .filter_map(|element| element.into_token())
1207}
1208
1209fn non_trivia_token_texts(node: &SyntaxNode) -> impl Iterator<Item = SmolStr> + '_ {
1210 node.children_with_tokens()
1211 .filter_map(|element| element.into_token())
1212 .filter(|token| {
1213 !matches!(
1214 token.kind(),
1215 SyntaxKind::Whitespace | SyntaxKind::Indent | SyntaxKind::Newline
1216 )
1217 })
1218 .map(|token| SmolStr::new(token.text()))
1219}