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_close(&self) -> bool {
535 self.syntax.text().to_string().trim() == "}"
536 }
537
538 pub fn has_open_brace(&self) -> bool {
540 self.syntax
541 .descendants_with_tokens()
542 .filter_map(|element| element.into_token())
543 .any(|token| token.kind() == SyntaxKind::LBrace)
544 }
545
546 pub fn is_empty(&self) -> bool {
548 if self.is_close() {
549 return false;
550 }
551 self.name().is_none()
552 }
553}
554
555impl TaskNode {
556 pub fn cast(syntax: SyntaxNode) -> Option<Self> {
564 (syntax.kind() == SyntaxKind::TaskDecl).then_some(Self { syntax })
565 }
566
567 pub fn range(&self) -> TextRange {
575 self.syntax.text_range()
576 }
577
578 pub fn name_range(&self) -> Option<TextRange> {
586 self.header()?.name_range()
587 }
588
589 pub fn name(&self) -> Option<SmolStr> {
597 self.header()?.name()
598 }
599
600 pub fn header_text(&self) -> Option<SmolStr> {
608 let header = self.header()?.syntax.text().to_string();
609 let header = header.trim().trim_end_matches(':').trim_end();
610 (!header.is_empty()).then(|| SmolStr::new(header))
611 }
612
613 pub fn header(&self) -> Option<TaskHeaderNode> {
614 self.syntax.children().find_map(TaskHeaderNode::cast)
615 }
616
617 pub fn uses_multiline_header(&self) -> bool {
618 self.header()
619 .is_some_and(|header| header.syntax.text().to_string().contains(['\n', '\r']))
620 }
621
622 pub fn header_info(&self) -> TaskHeaderInfo {
630 self.header()
631 .map_or_else(TaskHeaderInfo::default, |header| header.info())
632 }
633
634 pub fn commands(&self) -> std::vec::IntoIter<SmolStr> {
642 self.steps()
643 .map(|step| match step {
644 TaskStepNode::Command(command) => command.text,
645 TaskStepNode::CommandBlock(block) => block.source,
646 })
647 .collect::<Vec<_>>()
648 .into_iter()
649 }
650
651 pub fn steps(&self) -> std::vec::IntoIter<TaskStepNode> {
653 task_body_steps(&self.syntax)
654 .collect::<Vec<_>>()
655 .into_iter()
656 }
657}
658
659#[derive(Debug, Clone, Copy)]
660struct BodyLine<'a> {
661 text: &'a str,
662 start: usize,
663 end_with_newline: usize,
664}
665
666fn task_body_steps(node: &SyntaxNode) -> impl Iterator<Item = TaskStepNode> + '_ {
667 let source = node.text().to_string();
668 let body_start = node
669 .children()
670 .find(|child| child.kind() == SyntaxKind::TaskHeader)
671 .map(|header| usize::from(header.text_range().end() - node.text_range().start()))
672 .unwrap_or_else(|| first_line_end(&source).unwrap_or(source.len()));
673 let base = usize::from(node.text_range().start());
674 let lines = body_lines(&source, body_start).collect::<Vec<_>>();
675 let mut steps = Vec::new();
676 let mut index = 0usize;
677
678 while index < lines.len() {
679 let line = lines[index];
680 let trimmed = line.text.trim_start_matches([' ', '\t']);
681 if block_line_content(trimmed).is_none() {
682 if !trimmed.is_empty() && !trimmed.starts_with("//") {
683 let indent = line.text.len() - trimmed.len();
684 steps.push(TaskStepNode::Command(TaskCommandNode {
685 text: SmolStr::new(trimmed),
686 range: text_range(
687 base + line.start + indent,
688 base + line.start + line.text.len(),
689 ),
690 }));
691 }
692 index += 1;
693 continue;
694 }
695
696 let block_start = line.start;
697 let mut block_end = line.end_with_newline;
698 let mut block_source = String::new();
699 let mut line_ranges = Vec::new();
700 let mut marker_ranges = Vec::new();
701
702 while index < lines.len() {
703 let block_line = lines[index];
704 let trimmed = block_line.text.trim_start_matches([' ', '\t']);
705 let Some(content) = block_line_content(trimmed) else {
706 break;
707 };
708 let indent = block_line.text.len() - trimmed.len();
709 let marker_start = base + block_line.start + indent;
710 block_source.push_str(content);
711 block_source.push('\n');
712 line_ranges.push(text_range(
713 base + block_line.start,
714 base + block_line.start + block_line.text.len(),
715 ));
716 marker_ranges.push(text_range(marker_start, marker_start + 1));
717 block_end = block_line.end_with_newline;
718 index += 1;
719 }
720
721 steps.push(TaskStepNode::CommandBlock(TaskCommandBlockNode {
722 source: SmolStr::new(block_source),
723 range: text_range(base + block_start, base + block_end),
724 line_ranges,
725 marker_ranges,
726 }));
727 }
728
729 steps.into_iter()
730}
731
732fn first_line_end(source: &str) -> Option<usize> {
733 let (index, newline) = source
734 .char_indices()
735 .find(|(_, character)| matches!(character, '\n' | '\r'))?;
736 let newline_len = if newline == '\r' && source.as_bytes().get(index + 1) == Some(&b'\n') {
737 2
738 } else {
739 1
740 };
741 Some(index + newline_len)
742}
743
744fn body_lines(source: &str, start: usize) -> impl Iterator<Item = BodyLine<'_>> {
745 let mut cursor = start;
746 std::iter::from_fn(move || {
747 if cursor >= source.len() {
748 return None;
749 }
750 let line_start = cursor;
751 let rest = &source[cursor..];
752 let newline = rest
753 .char_indices()
754 .find(|(_, character)| matches!(character, '\n' | '\r'));
755 let (line_end, newline_len) = match newline {
756 Some((offset, '\r')) if rest.as_bytes().get(offset + 1) == Some(&b'\n') => {
757 (cursor + offset, 2)
758 }
759 Some((offset, _)) => (cursor + offset, 1),
760 None => (source.len(), 0),
761 };
762 cursor = line_end + newline_len;
763 Some(BodyLine {
764 text: &source[line_start..line_end],
765 start: line_start,
766 end_with_newline: cursor,
767 })
768 })
769}
770
771fn block_line_content(line: &str) -> Option<&str> {
772 let rest = line.strip_prefix('|')?;
773 match rest.as_bytes().first() {
774 None => Some(rest),
775 Some(b' ' | b'\t') => Some(&rest[1..]),
776 Some(_) => None,
777 }
778}
779
780fn text_range(start: usize, end: usize) -> TextRange {
781 TextRange::new(TextSize::from(start as u32), TextSize::from(end as u32))
782}
783
784impl TaskHeaderNode {
785 pub fn cast(syntax: SyntaxNode) -> Option<Self> {
786 (syntax.kind() == SyntaxKind::TaskHeader).then_some(Self { syntax })
787 }
788
789 pub fn range(&self) -> TextRange {
790 self.syntax.text_range()
791 }
792
793 pub fn name(&self) -> Option<SmolStr> {
794 self.name_node()?
795 .first_token()
796 .map(|token| SmolStr::new(token.text()))
797 }
798
799 pub fn name_range(&self) -> Option<TextRange> {
800 self.name_node()?
801 .first_token()
802 .map(|token| token.text_range())
803 }
804
805 pub fn parameter_list(&self) -> Option<ParameterListNode> {
806 self.syntax.children().find_map(ParameterListNode::cast)
807 }
808
809 pub fn conditions(&self) -> impl Iterator<Item = ConditionClauseNode> + '_ {
810 self.syntax.children().filter_map(ConditionClauseNode::cast)
811 }
812
813 pub fn dependencies(&self) -> impl Iterator<Item = DependencyClauseNode> + '_ {
814 self.syntax
815 .children()
816 .filter_map(DependencyClauseNode::cast)
817 }
818
819 pub fn shell(&self) -> Option<ShellClauseNode> {
820 self.syntax.children().find_map(ShellClauseNode::cast)
821 }
822
823 pub fn terminator(&self) -> Option<HeaderTerminatorNode> {
824 self.syntax.children().find_map(HeaderTerminatorNode::cast)
825 }
826
827 pub fn info(&self) -> TaskHeaderInfo {
828 parse_task_header(self)
829 }
830
831 fn name_node(&self) -> Option<SyntaxNode> {
832 self.syntax
833 .children()
834 .find(|node| node.kind() == SyntaxKind::TaskName)
835 }
836}
837
838impl ParameterListNode {
839 pub fn cast(syntax: SyntaxNode) -> Option<Self> {
840 (syntax.kind() == SyntaxKind::ParameterList).then_some(Self { syntax })
841 }
842
843 pub fn range(&self) -> TextRange {
844 self.syntax.text_range()
845 }
846
847 pub fn delimiter_ranges(&self) -> Vec<TextRange> {
849 self.syntax
850 .children_with_tokens()
851 .filter_map(|element| element.into_token())
852 .filter(|token| matches!(token.kind(), SyntaxKind::LParen | SyntaxKind::RParen))
853 .map(|token| token.text_range())
854 .collect()
855 }
856
857 pub fn parameters(&self) -> impl Iterator<Item = ParameterNode> + '_ {
858 self.syntax.children().filter_map(ParameterNode::cast)
859 }
860}
861
862impl ParameterNode {
863 pub fn cast(syntax: SyntaxNode) -> Option<Self> {
864 (syntax.kind() == SyntaxKind::Parameter).then_some(Self { syntax })
865 }
866
867 pub fn range(&self) -> TextRange {
868 self.syntax.text_range()
869 }
870
871 pub fn name(&self) -> Option<SmolStr> {
872 self.name_token().map(|token| SmolStr::new(token.text()))
873 }
874
875 pub fn name_range(&self) -> Option<TextRange> {
876 self.name_token().map(|token| token.text_range())
877 }
878
879 pub fn default_value(&self) -> Option<SmolStr> {
880 node_tokens(&self.syntax)
881 .find(|token| token.kind() == SyntaxKind::String)
882 .and_then(|token| {
883 token
884 .text()
885 .strip_prefix('"')?
886 .strip_suffix('"')
887 .map(SmolStr::new)
888 })
889 }
890
891 pub fn is_slice(&self) -> bool {
892 self.syntax
893 .text()
894 .to_string()
895 .split('=')
896 .next()
897 .is_some_and(|name| name.trim_end().ends_with(".."))
898 }
899
900 fn name_token(&self) -> Option<crate::cst::SyntaxToken> {
901 node_tokens(&self.syntax)
902 .find(|token| matches!(token.kind(), SyntaxKind::Ident | SyntaxKind::ShellKw))
903 }
904}
905
906macro_rules! clause_node {
907 ($type:ident, $kind:ident) => {
908 impl $type {
909 pub fn cast(syntax: SyntaxNode) -> Option<Self> {
910 (syntax.kind() == SyntaxKind::$kind).then_some(Self { syntax })
911 }
912
913 pub fn range(&self) -> TextRange {
914 self.syntax.text_range()
915 }
916
917 pub fn text(&self) -> SmolStr {
918 SmolStr::new(self.syntax.text().to_string().trim())
919 }
920 }
921 };
922}
923
924clause_node!(ConditionClauseNode, ConditionClause);
925clause_node!(DependencyClauseNode, DependencyClause);
926clause_node!(ShellClauseNode, ShellClause);
927clause_node!(HeaderTerminatorNode, HeaderTerminator);
928
929impl ConditionClauseNode {
930 pub fn operator_range(&self) -> Option<TextRange> {
932 node_tokens(&self.syntax)
933 .find(|token| token.kind() == SyntaxKind::Question)
934 .map(|token| token.text_range())
935 }
936}
937
938impl DependencyClauseNode {
939 pub fn operator_range(&self) -> Option<TextRange> {
941 node_tokens(&self.syntax)
942 .find(|token| token.kind() == SyntaxKind::Amp)
943 .map(|token| token.text_range())
944 }
945
946 pub fn parallel_group_delimiter_ranges(&self) -> Vec<TextRange> {
948 let tokens = node_tokens(&self.syntax).collect::<Vec<_>>();
949 if !tokens
950 .iter()
951 .any(|token| token.kind() == SyntaxKind::LParen)
952 || !tokens
953 .iter()
954 .any(|token| token.kind() == SyntaxKind::RParen)
955 {
956 return Vec::new();
957 }
958
959 tokens
960 .into_iter()
961 .filter(|token| {
962 matches!(
963 token.kind(),
964 SyntaxKind::LParen | SyntaxKind::Comma | SyntaxKind::RParen
965 )
966 })
967 .map(|token| token.text_range())
968 .collect()
969 }
970}
971
972impl ShellClauseNode {
973 pub fn operator(&self) -> Option<ShellOperator> {
975 node_tokens(&self.syntax).find_map(|token| match token.kind() {
976 SyntaxKind::ShellKw => Some(ShellOperator::Required),
977 SyntaxKind::ShellFallbackKw => Some(ShellOperator::Fallback),
978 _ => None,
979 })
980 }
981
982 pub fn shell_name(&self) -> Option<SmolStr> {
984 node_tokens(&self.syntax)
985 .find(|token| token.kind() == SyntaxKind::Ident)
986 .map(|token| SmolStr::new(token.text()))
987 }
988
989 pub fn content_range(&self) -> Option<TextRange> {
991 let mut tokens = node_tokens(&self.syntax).filter(|token| {
992 !matches!(
993 token.kind(),
994 SyntaxKind::Whitespace | SyntaxKind::Indent | SyntaxKind::Newline
995 )
996 });
997 let first = tokens.next()?;
998 let end = tokens.last().unwrap_or_else(|| first.clone());
999 Some(TextRange::new(
1000 first.text_range().start(),
1001 end.text_range().end(),
1002 ))
1003 }
1004}
1005
1006fn parse_task_header(node: &TaskHeaderNode) -> TaskHeaderInfo {
1007 let mut info = TaskHeaderInfo::default();
1008
1009 if let Some(parameters) = node.parameter_list() {
1010 let refs = parameters
1011 .parameters()
1012 .filter_map(|parameter| {
1013 Some(TaskParamRef {
1014 name: parameter.name()?,
1015 range: parameter.name_range()?,
1016 default_value: parameter.default_value(),
1017 is_slice: parameter.is_slice(),
1018 })
1019 })
1020 .collect::<Vec<_>>();
1021 if !refs.is_empty() {
1022 info.params = Some(SmolStr::new(
1023 refs.iter()
1024 .map(render_param_ref)
1025 .collect::<Vec<_>>()
1026 .join(", "),
1027 ));
1028 }
1029 info.param_refs = refs;
1030 }
1031
1032 info.guards = node.conditions().filter_map(parse_guard_ref).collect();
1033 info.guard = info
1034 .guards
1035 .first()
1036 .map(|guard| SmolStr::new(format!("@{}(\"{}\")", guard.kind, guard.argument)));
1037
1038 let mut dependency_text = Vec::new();
1039 for (stage, clause) in node.dependencies().enumerate() {
1040 dependency_text.push(clause.text().trim_start_matches('&').trim().to_string());
1041 parse_dependency_clause(&clause.syntax, stage, &mut info.dependency_refs);
1042 }
1043 if !dependency_text.is_empty() {
1044 info.dependencies = Some(SmolStr::new(dependency_text.join(" & ")));
1045 }
1046
1047 if let Some(shell) = node.shell() {
1048 let tokens = node_tokens(&shell.syntax)
1049 .filter(|token| {
1050 !matches!(
1051 token.kind(),
1052 SyntaxKind::Whitespace | SyntaxKind::Indent | SyntaxKind::Newline
1053 )
1054 })
1055 .collect::<Vec<_>>();
1056 let operator = tokens.first().and_then(|token| match token.kind() {
1057 SyntaxKind::ShellKw => Some(ShellOperator::Required),
1058 SyntaxKind::ShellFallbackKw => Some(ShellOperator::Fallback),
1059 _ => None,
1060 });
1061 let kind = tokens
1062 .iter()
1063 .rev()
1064 .find(|token| token.kind() == SyntaxKind::Ident)
1065 .map(|token| ShellKind::parse(token.text()));
1066 info.shell = operator.zip(kind).map(|(operator, kind)| TaskShellRef {
1067 selection: ShellSelection { kind, operator },
1068 range: shell.content_range().unwrap_or_else(|| shell.range()),
1069 });
1070 }
1071
1072 info
1073}
1074
1075fn render_param_ref(parameter: &TaskParamRef) -> String {
1076 let suffix = if parameter.is_slice { ".." } else { "" };
1077 match ¶meter.default_value {
1078 Some(value) => format!("{}{suffix}=\"{value}\"", parameter.name),
1079 None => format!("{}{suffix}", parameter.name),
1080 }
1081}
1082
1083fn parse_guard_ref(clause: ConditionClauseNode) -> Option<TaskGuardRef> {
1084 let tokens = node_tokens(&clause.syntax)
1085 .filter(|token| {
1086 !matches!(
1087 token.kind(),
1088 SyntaxKind::Whitespace | SyntaxKind::Indent | SyntaxKind::Newline
1089 )
1090 })
1091 .collect::<Vec<_>>();
1092 let name_token = tokens
1093 .iter()
1094 .find(|token| token.kind() == SyntaxKind::Ident)?;
1095 let name = name_token.text();
1096 let name_start = tokens
1097 .iter()
1098 .find(|token| token.kind() == SyntaxKind::At)
1099 .map_or_else(
1100 || name_token.text_range().start(),
1101 |token| token.text_range().start(),
1102 );
1103 let argument = tokens
1104 .iter()
1105 .find(|token| token.kind() == SyntaxKind::String)?
1106 .text()
1107 .strip_prefix('"')?
1108 .strip_suffix('"')?;
1109
1110 Some(TaskGuardRef {
1111 kind: GuardKind::parse(name),
1112 argument: SmolStr::new(argument),
1113 range: clause.range(),
1114 name_range: TextRange::new(name_start, name_token.text_range().end()),
1115 })
1116}
1117
1118fn parse_dependency_clause(node: &SyntaxNode, stage: usize, refs: &mut Vec<TaskDependencyRef>) {
1119 let mut tokens = node_tokens(node)
1120 .filter(|token| {
1121 !matches!(
1122 token.kind(),
1123 SyntaxKind::Whitespace | SyntaxKind::Indent | SyntaxKind::Newline
1124 )
1125 })
1126 .collect::<Vec<_>>();
1127 if tokens
1128 .first()
1129 .is_some_and(|token| token.kind() == SyntaxKind::Amp)
1130 {
1131 tokens.remove(0);
1132 }
1133 if tokens
1134 .first()
1135 .is_some_and(|token| token.kind() == SyntaxKind::LParen)
1136 && tokens
1137 .last()
1138 .is_some_and(|token| token.kind() == SyntaxKind::RParen)
1139 {
1140 tokens.remove(0);
1141 tokens.pop();
1142 }
1143
1144 let mut invocation_start = 0usize;
1145 let mut depth = 0usize;
1146 for index in 0..=tokens.len() {
1147 let at_separator =
1148 index == tokens.len() || (tokens[index].kind() == SyntaxKind::Comma && depth == 0);
1149 if at_separator {
1150 if let Some(reference) =
1151 parse_dependency_invocation(&tokens[invocation_start..index], stage)
1152 {
1153 refs.push(reference);
1154 }
1155 invocation_start = index + 1;
1156 continue;
1157 }
1158
1159 match tokens[index].kind() {
1160 SyntaxKind::LParen => depth += 1,
1161 SyntaxKind::RParen => depth = depth.saturating_sub(1),
1162 _ => {}
1163 }
1164 }
1165}
1166
1167fn parse_dependency_invocation(
1168 tokens: &[crate::cst::SyntaxToken],
1169 stage: usize,
1170) -> Option<TaskDependencyRef> {
1171 let first = tokens.first()?;
1172 let invocation_end = tokens.last()?.text_range().end();
1173 let argument_start = tokens
1174 .iter()
1175 .position(|token| token.kind() == SyntaxKind::LParen)
1176 .unwrap_or(tokens.len());
1177 let name_tokens = &tokens[..argument_start];
1178 let name_start = name_tokens.first()?.text_range().start();
1179 let name_end = name_tokens.last()?.text_range().end();
1180 let name = name_tokens
1181 .iter()
1182 .map(|token| token.text())
1183 .collect::<String>();
1184 let arguments = tokens
1185 .iter()
1186 .skip(argument_start.saturating_add(1))
1187 .filter(|token| token.kind() == SyntaxKind::String)
1188 .filter_map(|token| {
1189 let value = token.text().strip_prefix('"')?.strip_suffix('"')?;
1190 Some(TaskDependencyArgRef {
1191 value: SmolStr::new(value),
1192 range: token.text_range(),
1193 })
1194 })
1195 .collect();
1196
1197 Some(TaskDependencyRef {
1198 name: SmolStr::new(name),
1199 range: TextRange::new(name_start, name_end),
1200 arguments,
1201 invocation_range: TextRange::new(first.text_range().start(), invocation_end),
1202 stage,
1203 })
1204}
1205
1206fn node_tokens(node: &SyntaxNode) -> impl Iterator<Item = crate::cst::SyntaxToken> + '_ {
1207 node.descendants_with_tokens()
1208 .filter_map(|element| element.into_token())
1209}
1210
1211fn non_trivia_token_texts(node: &SyntaxNode) -> impl Iterator<Item = SmolStr> + '_ {
1212 node.children_with_tokens()
1213 .filter_map(|element| element.into_token())
1214 .filter(|token| {
1215 !matches!(
1216 token.kind(),
1217 SyntaxKind::Whitespace | SyntaxKind::Indent | SyntaxKind::Newline
1218 )
1219 })
1220 .map(|token| SmolStr::new(token.text()))
1221}