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 DocCommentNode {
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 GuardClauseNode {
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 stage: usize,
139}
140
141#[derive(Debug, Clone, PartialEq, Eq)]
143pub struct TaskParamRef {
144 pub name: SmolStr,
145 pub range: TextRange,
146 pub default_value: Option<SmolStr>,
147 pub is_slice: bool,
148}
149
150#[derive(Debug, Clone, PartialEq, Eq)]
151pub struct TaskGuardRef {
152 pub kind: GuardKind,
153 pub argument: SmolStr,
154 pub range: TextRange,
155}
156
157#[derive(Debug, Clone, Default, PartialEq, Eq)]
165pub struct TaskHeaderInfo {
166 pub params: Option<SmolStr>,
167 pub param_refs: Vec<TaskParamRef>,
168 pub guard: Option<SmolStr>,
169 pub guards: Vec<TaskGuardRef>,
170 pub dependencies: Option<SmolStr>,
171 pub shell: Option<TaskShellRef>,
172 pub dependency_refs: Vec<TaskDependencyRef>,
173}
174
175impl DocumentNode {
176 pub fn cast(syntax: SyntaxNode) -> Option<Self> {
184 (syntax.kind() == SyntaxKind::Document).then_some(Self { syntax })
185 }
186
187 pub fn syntax(&self) -> &SyntaxNode {
195 &self.syntax
196 }
197
198 pub fn range(&self) -> TextRange {
206 self.syntax.text_range()
207 }
208
209 pub fn directives(&self) -> impl Iterator<Item = DirectiveNode> + '_ {
217 self.syntax.children().filter_map(DirectiveNode::cast)
218 }
219
220 pub fn doc_comments(&self) -> impl Iterator<Item = DocCommentNode> + '_ {
228 self.syntax.children().filter_map(DocCommentNode::cast)
229 }
230
231 pub fn namespaces(&self) -> impl Iterator<Item = NamespaceNode> + '_ {
239 self.syntax.children().filter_map(NamespaceNode::cast)
240 }
241
242 pub fn tasks(&self) -> impl Iterator<Item = TaskNode> + '_ {
250 self.syntax.children().filter_map(TaskNode::cast)
251 }
252}
253
254impl DirectiveNode {
255 pub fn cast(syntax: SyntaxNode) -> Option<Self> {
263 (syntax.kind() == SyntaxKind::Directive).then_some(Self { syntax })
264 }
265
266 pub fn range(&self) -> TextRange {
274 self.syntax.text_range()
275 }
276
277 pub fn keyword_range(&self) -> Option<TextRange> {
285 let mut tokens = self
286 .syntax
287 .children_with_tokens()
288 .filter_map(|element| element.into_token())
289 .filter(|token| {
290 !matches!(
291 token.kind(),
292 SyntaxKind::Whitespace | SyntaxKind::Indent | SyntaxKind::Newline
293 )
294 });
295 let bang = tokens.find(|token| token.kind() == SyntaxKind::Bang)?;
296 let keyword = tokens.next()?;
297 Some(TextRange::new(
298 bang.text_range().start(),
299 keyword.text_range().end(),
300 ))
301 }
302
303 pub fn name(&self) -> Option<SmolStr> {
311 non_trivia_token_texts(&self.syntax).nth(1)
312 }
313
314 pub fn directive_kind(&self) -> Option<DirectiveKind> {
316 self.name().map(|name| DirectiveKind::parse(&name))
317 }
318
319 pub fn value(&self) -> Option<SmolStr> {
327 let value = non_trivia_token_texts(&self.syntax)
328 .skip(2)
329 .collect::<Vec<_>>()
330 .join(" ");
331 (!value.is_empty()).then(|| SmolStr::new(value))
332 }
333
334 pub fn raw_value(&self) -> Option<SmolStr> {
336 let mut non_trivia = 0usize;
337 let mut value = String::new();
338
339 for token in self
340 .syntax
341 .children_with_tokens()
342 .filter_map(|element| element.into_token())
343 {
344 if token.kind() == SyntaxKind::Newline {
345 break;
346 }
347 if !matches!(
348 token.kind(),
349 SyntaxKind::Whitespace | SyntaxKind::Indent | SyntaxKind::Comment
350 ) {
351 non_trivia += 1;
352 }
353 if non_trivia >= 2 && !(non_trivia == 2 && token.kind() == SyntaxKind::Ident) {
354 value.push_str(token.text());
355 }
356 }
357
358 let value = value.trim();
359 (!value.is_empty()).then(|| SmolStr::new(value))
360 }
361
362 pub fn argument_name_range(&self) -> Option<TextRange> {
364 self.syntax
365 .children_with_tokens()
366 .filter_map(|element| element.into_token())
367 .filter(|token| matches!(token.kind(), SyntaxKind::Ident | SyntaxKind::ShellKw))
368 .nth(1)
369 .map(|token| token.text_range())
370 }
371}
372
373impl DocCommentNode {
374 pub fn cast(syntax: SyntaxNode) -> Option<Self> {
382 (syntax.kind() == SyntaxKind::DocComment).then_some(Self { syntax })
383 }
384
385 pub fn range(&self) -> TextRange {
393 self.syntax.text_range()
394 }
395
396 pub fn text(&self) -> Option<SmolStr> {
404 self.syntax
405 .text()
406 .to_string()
407 .trim()
408 .strip_prefix('#')
409 .map(str::trim)
410 .filter(|text| !text.is_empty())
411 .map(SmolStr::new)
412 }
413}
414
415impl NamespaceNode {
416 pub fn cast(syntax: SyntaxNode) -> Option<Self> {
424 (syntax.kind() == SyntaxKind::NamespaceBlock).then_some(Self { syntax })
425 }
426
427 pub fn range(&self) -> TextRange {
435 self.syntax.text_range()
436 }
437
438 pub fn name(&self) -> Option<SmolStr> {
446 let source = self.syntax.text().to_string();
447 let label = source
448 .trim()
449 .strip_prefix('[')
450 .and_then(|text| text.split_once(']'))
451 .map(|(label, _)| label)
452 .map(str::trim)?;
453 (!label.is_empty()).then(|| SmolStr::new(label))
454 }
455
456 pub fn name_range(&self) -> Option<TextRange> {
464 self.syntax
465 .children_with_tokens()
466 .filter_map(|element| element.into_token())
467 .find(|token| token.kind() == SyntaxKind::Ident)
468 .map(|token| token.text_range())
469 }
470
471 pub fn is_close(&self) -> bool {
473 self.syntax.text().to_string().trim() == "}"
474 }
475
476 pub fn has_open_brace(&self) -> bool {
478 self.syntax
479 .descendants_with_tokens()
480 .filter_map(|element| element.into_token())
481 .any(|token| token.kind() == SyntaxKind::LBrace)
482 }
483
484 pub fn is_empty(&self) -> bool {
486 if self.is_close() {
487 return false;
488 }
489 self.syntax
490 .text()
491 .to_string()
492 .trim()
493 .strip_prefix('[')
494 .and_then(|text| text.split_once(']'))
495 .map(|(label, _)| label)
496 .map(str::trim)
497 .filter(|text| !text.is_empty())
498 .is_none()
499 }
500}
501
502impl TaskNode {
503 pub fn cast(syntax: SyntaxNode) -> Option<Self> {
511 (syntax.kind() == SyntaxKind::TaskDecl).then_some(Self { syntax })
512 }
513
514 pub fn range(&self) -> TextRange {
522 self.syntax.text_range()
523 }
524
525 pub fn name_range(&self) -> Option<TextRange> {
533 self.header()?.name_range()
534 }
535
536 pub fn name(&self) -> Option<SmolStr> {
544 self.header()?.name()
545 }
546
547 pub fn header_text(&self) -> Option<SmolStr> {
555 let header = self.header()?.syntax.text().to_string();
556 let header = header.trim().trim_end_matches(':').trim_end();
557 (!header.is_empty()).then(|| SmolStr::new(header))
558 }
559
560 pub fn header(&self) -> Option<TaskHeaderNode> {
561 self.syntax.children().find_map(TaskHeaderNode::cast)
562 }
563
564 pub fn uses_multiline_header(&self) -> bool {
565 self.header()
566 .is_some_and(|header| header.syntax.text().to_string().contains(['\n', '\r']))
567 }
568
569 pub fn header_info(&self) -> TaskHeaderInfo {
577 self.header()
578 .map_or_else(TaskHeaderInfo::default, |header| header.info())
579 }
580
581 pub fn commands(&self) -> std::vec::IntoIter<SmolStr> {
589 self.steps()
590 .map(|step| match step {
591 TaskStepNode::Command(command) => command.text,
592 TaskStepNode::CommandBlock(block) => block.source,
593 })
594 .collect::<Vec<_>>()
595 .into_iter()
596 }
597
598 pub fn steps(&self) -> std::vec::IntoIter<TaskStepNode> {
600 task_body_steps(&self.syntax)
601 .collect::<Vec<_>>()
602 .into_iter()
603 }
604}
605
606#[derive(Debug, Clone, Copy)]
607struct BodyLine<'a> {
608 text: &'a str,
609 start: usize,
610 end_with_newline: usize,
611}
612
613fn task_body_steps(node: &SyntaxNode) -> impl Iterator<Item = TaskStepNode> + '_ {
614 let source = node.text().to_string();
615 let body_start = node
616 .children()
617 .find(|child| child.kind() == SyntaxKind::TaskHeader)
618 .map(|header| usize::from(header.text_range().end() - node.text_range().start()))
619 .unwrap_or_else(|| first_line_end(&source).unwrap_or(source.len()));
620 let base = usize::from(node.text_range().start());
621 let lines = body_lines(&source, body_start).collect::<Vec<_>>();
622 let mut steps = Vec::new();
623 let mut index = 0usize;
624
625 while index < lines.len() {
626 let line = lines[index];
627 let trimmed = line.text.trim_start_matches([' ', '\t']);
628 if block_line_content(trimmed).is_none() {
629 if !trimmed.is_empty() && !trimmed.starts_with("//") {
630 let indent = line.text.len() - trimmed.len();
631 steps.push(TaskStepNode::Command(TaskCommandNode {
632 text: SmolStr::new(trimmed),
633 range: text_range(
634 base + line.start + indent,
635 base + line.start + line.text.len(),
636 ),
637 }));
638 }
639 index += 1;
640 continue;
641 }
642
643 let block_start = line.start;
644 let mut block_end = line.end_with_newline;
645 let mut block_source = String::new();
646 let mut line_ranges = Vec::new();
647 let mut marker_ranges = Vec::new();
648
649 while index < lines.len() {
650 let block_line = lines[index];
651 let trimmed = block_line.text.trim_start_matches([' ', '\t']);
652 let Some(content) = block_line_content(trimmed) else {
653 break;
654 };
655 let indent = block_line.text.len() - trimmed.len();
656 let marker_start = base + block_line.start + indent;
657 block_source.push_str(content);
658 block_source.push('\n');
659 line_ranges.push(text_range(
660 base + block_line.start,
661 base + block_line.start + block_line.text.len(),
662 ));
663 marker_ranges.push(text_range(marker_start, marker_start + 1));
664 block_end = block_line.end_with_newline;
665 index += 1;
666 }
667
668 steps.push(TaskStepNode::CommandBlock(TaskCommandBlockNode {
669 source: SmolStr::new(block_source),
670 range: text_range(base + block_start, base + block_end),
671 line_ranges,
672 marker_ranges,
673 }));
674 }
675
676 steps.into_iter()
677}
678
679fn first_line_end(source: &str) -> Option<usize> {
680 let (index, newline) = source
681 .char_indices()
682 .find(|(_, character)| matches!(character, '\n' | '\r'))?;
683 let newline_len = if newline == '\r' && source.as_bytes().get(index + 1) == Some(&b'\n') {
684 2
685 } else {
686 1
687 };
688 Some(index + newline_len)
689}
690
691fn body_lines(source: &str, start: usize) -> impl Iterator<Item = BodyLine<'_>> {
692 let mut cursor = start;
693 std::iter::from_fn(move || {
694 if cursor >= source.len() {
695 return None;
696 }
697 let line_start = cursor;
698 let rest = &source[cursor..];
699 let newline = rest
700 .char_indices()
701 .find(|(_, character)| matches!(character, '\n' | '\r'));
702 let (line_end, newline_len) = match newline {
703 Some((offset, '\r')) if rest.as_bytes().get(offset + 1) == Some(&b'\n') => {
704 (cursor + offset, 2)
705 }
706 Some((offset, _)) => (cursor + offset, 1),
707 None => (source.len(), 0),
708 };
709 cursor = line_end + newline_len;
710 Some(BodyLine {
711 text: &source[line_start..line_end],
712 start: line_start,
713 end_with_newline: cursor,
714 })
715 })
716}
717
718fn block_line_content(line: &str) -> Option<&str> {
719 let rest = line.strip_prefix('|')?;
720 match rest.as_bytes().first() {
721 None => Some(rest),
722 Some(b' ' | b'\t') => Some(&rest[1..]),
723 Some(_) => None,
724 }
725}
726
727fn text_range(start: usize, end: usize) -> TextRange {
728 TextRange::new(TextSize::from(start as u32), TextSize::from(end as u32))
729}
730
731#[derive(Debug, Default)]
732struct PendingRef {
733 name: String,
734 start: Option<TextSize>,
735 end: Option<TextSize>,
736}
737
738impl PendingRef {
739 fn flush(&mut self, refs: &mut Vec<TaskDependencyRef>, stage: usize) {
740 if let (Some(start), Some(end)) = (self.start, self.end) {
741 let name = self.name.trim();
742 if !name.is_empty() {
743 refs.push(TaskDependencyRef {
744 name: SmolStr::new(name),
745 range: TextRange::new(start, end),
746 stage,
747 });
748 }
749 }
750 self.name.clear();
751 self.start = None;
752 self.end = None;
753 }
754
755 fn extend(&mut self, token: &crate::cst::SyntaxToken) {
756 self.start.get_or_insert(token.text_range().start());
757 self.end = Some(token.text_range().end());
758 self.name.push_str(token.text());
759 }
760}
761
762impl TaskHeaderNode {
763 pub fn cast(syntax: SyntaxNode) -> Option<Self> {
764 (syntax.kind() == SyntaxKind::TaskHeader).then_some(Self { syntax })
765 }
766
767 pub fn range(&self) -> TextRange {
768 self.syntax.text_range()
769 }
770
771 pub fn name(&self) -> Option<SmolStr> {
772 self.name_node()?
773 .first_token()
774 .map(|token| SmolStr::new(token.text()))
775 }
776
777 pub fn name_range(&self) -> Option<TextRange> {
778 self.name_node()?
779 .first_token()
780 .map(|token| token.text_range())
781 }
782
783 pub fn parameter_list(&self) -> Option<ParameterListNode> {
784 self.syntax.children().find_map(ParameterListNode::cast)
785 }
786
787 pub fn guards(&self) -> impl Iterator<Item = GuardClauseNode> + '_ {
788 self.syntax.children().filter_map(GuardClauseNode::cast)
789 }
790
791 pub fn dependencies(&self) -> impl Iterator<Item = DependencyClauseNode> + '_ {
792 self.syntax
793 .children()
794 .filter_map(DependencyClauseNode::cast)
795 }
796
797 pub fn shell(&self) -> Option<ShellClauseNode> {
798 self.syntax.children().find_map(ShellClauseNode::cast)
799 }
800
801 pub fn terminator(&self) -> Option<HeaderTerminatorNode> {
802 self.syntax.children().find_map(HeaderTerminatorNode::cast)
803 }
804
805 pub fn info(&self) -> TaskHeaderInfo {
806 parse_task_header(self)
807 }
808
809 fn name_node(&self) -> Option<SyntaxNode> {
810 self.syntax
811 .children()
812 .find(|node| node.kind() == SyntaxKind::TaskName)
813 }
814}
815
816impl ParameterListNode {
817 pub fn cast(syntax: SyntaxNode) -> Option<Self> {
818 (syntax.kind() == SyntaxKind::ParameterList).then_some(Self { syntax })
819 }
820
821 pub fn range(&self) -> TextRange {
822 self.syntax.text_range()
823 }
824
825 pub fn parameters(&self) -> impl Iterator<Item = ParameterNode> + '_ {
826 self.syntax.children().filter_map(ParameterNode::cast)
827 }
828}
829
830impl ParameterNode {
831 pub fn cast(syntax: SyntaxNode) -> Option<Self> {
832 (syntax.kind() == SyntaxKind::Parameter).then_some(Self { syntax })
833 }
834
835 pub fn range(&self) -> TextRange {
836 self.syntax.text_range()
837 }
838
839 pub fn name(&self) -> Option<SmolStr> {
840 self.name_token().map(|token| SmolStr::new(token.text()))
841 }
842
843 pub fn name_range(&self) -> Option<TextRange> {
844 self.name_token().map(|token| token.text_range())
845 }
846
847 pub fn default_value(&self) -> Option<SmolStr> {
848 node_tokens(&self.syntax)
849 .find(|token| token.kind() == SyntaxKind::String)
850 .and_then(|token| {
851 token
852 .text()
853 .strip_prefix('"')?
854 .strip_suffix('"')
855 .map(SmolStr::new)
856 })
857 }
858
859 pub fn is_slice(&self) -> bool {
860 self.syntax
861 .text()
862 .to_string()
863 .split('=')
864 .next()
865 .is_some_and(|name| name.trim_end().ends_with(".."))
866 }
867
868 fn name_token(&self) -> Option<crate::cst::SyntaxToken> {
869 node_tokens(&self.syntax)
870 .find(|token| matches!(token.kind(), SyntaxKind::Ident | SyntaxKind::ShellKw))
871 }
872}
873
874macro_rules! clause_node {
875 ($type:ident, $kind:ident) => {
876 impl $type {
877 pub fn cast(syntax: SyntaxNode) -> Option<Self> {
878 (syntax.kind() == SyntaxKind::$kind).then_some(Self { syntax })
879 }
880
881 pub fn range(&self) -> TextRange {
882 self.syntax.text_range()
883 }
884
885 pub fn text(&self) -> SmolStr {
886 SmolStr::new(self.syntax.text().to_string().trim())
887 }
888 }
889 };
890}
891
892clause_node!(GuardClauseNode, GuardClause);
893clause_node!(DependencyClauseNode, DependencyClause);
894clause_node!(ShellClauseNode, ShellClause);
895clause_node!(HeaderTerminatorNode, HeaderTerminator);
896
897impl ShellClauseNode {
898 pub fn operator(&self) -> Option<ShellOperator> {
900 node_tokens(&self.syntax).find_map(|token| match token.kind() {
901 SyntaxKind::ShellKw => Some(ShellOperator::Required),
902 SyntaxKind::ShellFallbackKw => Some(ShellOperator::Fallback),
903 _ => None,
904 })
905 }
906
907 pub fn shell_name(&self) -> Option<SmolStr> {
909 node_tokens(&self.syntax)
910 .find(|token| token.kind() == SyntaxKind::Ident)
911 .map(|token| SmolStr::new(token.text()))
912 }
913
914 pub fn content_range(&self) -> Option<TextRange> {
916 let mut tokens = node_tokens(&self.syntax).filter(|token| {
917 !matches!(
918 token.kind(),
919 SyntaxKind::Whitespace | SyntaxKind::Indent | SyntaxKind::Newline
920 )
921 });
922 let first = tokens.next()?;
923 let end = tokens.last().unwrap_or_else(|| first.clone());
924 Some(TextRange::new(
925 first.text_range().start(),
926 end.text_range().end(),
927 ))
928 }
929}
930
931fn parse_task_header(node: &TaskHeaderNode) -> TaskHeaderInfo {
932 let mut info = TaskHeaderInfo::default();
933
934 if let Some(parameters) = node.parameter_list() {
935 let refs = parameters
936 .parameters()
937 .filter_map(|parameter| {
938 Some(TaskParamRef {
939 name: parameter.name()?,
940 range: parameter.name_range()?,
941 default_value: parameter.default_value(),
942 is_slice: parameter.is_slice(),
943 })
944 })
945 .collect::<Vec<_>>();
946 if !refs.is_empty() {
947 info.params = Some(SmolStr::new(
948 refs.iter()
949 .map(render_param_ref)
950 .collect::<Vec<_>>()
951 .join(", "),
952 ));
953 }
954 info.param_refs = refs;
955 }
956
957 info.guards = node.guards().filter_map(parse_guard_ref).collect();
958 info.guard = info
959 .guards
960 .first()
961 .map(|guard| SmolStr::new(format!("@{}(\"{}\")", guard.kind, guard.argument)));
962
963 let mut dependency_text = Vec::new();
964 for (stage, clause) in node.dependencies().enumerate() {
965 dependency_text.push(clause.text().trim_start_matches('&').trim().to_string());
966 parse_dependency_clause(&clause.syntax, stage, &mut info.dependency_refs);
967 }
968 if !dependency_text.is_empty() {
969 info.dependencies = Some(SmolStr::new(dependency_text.join(" & ")));
970 }
971
972 if let Some(shell) = node.shell() {
973 let tokens = node_tokens(&shell.syntax)
974 .filter(|token| {
975 !matches!(
976 token.kind(),
977 SyntaxKind::Whitespace | SyntaxKind::Indent | SyntaxKind::Newline
978 )
979 })
980 .collect::<Vec<_>>();
981 let operator = tokens.first().and_then(|token| match token.kind() {
982 SyntaxKind::ShellKw => Some(ShellOperator::Required),
983 SyntaxKind::ShellFallbackKw => Some(ShellOperator::Fallback),
984 _ => None,
985 });
986 let kind = tokens
987 .iter()
988 .rev()
989 .find(|token| token.kind() == SyntaxKind::Ident)
990 .map(|token| ShellKind::parse(token.text()));
991 info.shell = operator.zip(kind).map(|(operator, kind)| TaskShellRef {
992 selection: ShellSelection { kind, operator },
993 range: shell.content_range().unwrap_or_else(|| shell.range()),
994 });
995 }
996
997 info
998}
999
1000fn render_param_ref(parameter: &TaskParamRef) -> String {
1001 let suffix = if parameter.is_slice { ".." } else { "" };
1002 match ¶meter.default_value {
1003 Some(value) => format!("{}{suffix}=\"{value}\"", parameter.name),
1004 None => format!("{}{suffix}", parameter.name),
1005 }
1006}
1007
1008fn parse_guard_ref(clause: GuardClauseNode) -> Option<TaskGuardRef> {
1009 let tokens = node_tokens(&clause.syntax)
1010 .filter(|token| {
1011 !matches!(
1012 token.kind(),
1013 SyntaxKind::Whitespace | SyntaxKind::Indent | SyntaxKind::Newline
1014 )
1015 })
1016 .collect::<Vec<_>>();
1017 let name = tokens
1018 .iter()
1019 .find(|token| token.kind() == SyntaxKind::Ident)?
1020 .text();
1021 let argument = tokens
1022 .iter()
1023 .find(|token| token.kind() == SyntaxKind::String)?
1024 .text()
1025 .strip_prefix('"')?
1026 .strip_suffix('"')?;
1027
1028 Some(TaskGuardRef {
1029 kind: GuardKind::parse(name),
1030 argument: SmolStr::new(argument),
1031 range: clause.range(),
1032 })
1033}
1034
1035fn parse_dependency_clause(node: &SyntaxNode, stage: usize, refs: &mut Vec<TaskDependencyRef>) {
1036 let mut pending = PendingRef::default();
1037 for token in node_tokens(node) {
1038 match token.kind() {
1039 SyntaxKind::Amp
1040 | SyntaxKind::LParen
1041 | SyntaxKind::Whitespace
1042 | SyntaxKind::Indent
1043 | SyntaxKind::Newline => {}
1044 SyntaxKind::RParen => pending.flush(refs, stage),
1045 SyntaxKind::Unknown if token.text() == "," => pending.flush(refs, stage),
1046 _ => pending.extend(&token),
1047 }
1048 }
1049 pending.flush(refs, stage);
1050}
1051
1052fn node_tokens(node: &SyntaxNode) -> impl Iterator<Item = crate::cst::SyntaxToken> + '_ {
1053 node.descendants_with_tokens()
1054 .filter_map(|element| element.into_token())
1055}
1056
1057fn non_trivia_token_texts(node: &SyntaxNode) -> impl Iterator<Item = SmolStr> + '_ {
1058 node.children_with_tokens()
1059 .filter_map(|element| element.into_token())
1060 .filter(|token| {
1061 !matches!(
1062 token.kind(),
1063 SyntaxKind::Whitespace | SyntaxKind::Indent | SyntaxKind::Newline
1064 )
1065 })
1066 .map(|token| SmolStr::new(token.text()))
1067}