1use smol_str::SmolStr;
2use text_size::{TextRange, TextSize};
3
4use crate::{SyntaxKind, SyntaxNode};
5
6#[derive(Debug, Clone)]
14pub struct DocumentNode {
15 syntax: SyntaxNode,
16}
17
18#[derive(Debug, Clone)]
26pub struct DirectiveNode {
27 syntax: SyntaxNode,
28}
29
30#[derive(Debug, Clone)]
38pub struct DocCommentNode {
39 syntax: SyntaxNode,
40}
41
42#[derive(Debug, Clone)]
50pub struct NamespaceNode {
51 syntax: SyntaxNode,
52}
53
54#[derive(Debug, Clone)]
62pub struct TaskNode {
63 syntax: SyntaxNode,
64}
65
66#[derive(Debug, Clone, PartialEq, Eq)]
74pub struct TaskDependencyRef {
75 pub name: SmolStr,
76 pub range: TextRange,
77 pub stage: usize,
78}
79
80#[derive(Debug, Clone, Default, PartialEq, Eq)]
88pub struct TaskHeaderInfo {
89 pub params: Option<SmolStr>,
90 pub guard: Option<SmolStr>,
91 pub dependencies: Option<SmolStr>,
92 pub shell: Option<SmolStr>,
93 pub shell_fallback: bool,
94 pub dependency_refs: Vec<TaskDependencyRef>,
95}
96
97impl DocumentNode {
98 pub fn cast(syntax: SyntaxNode) -> Option<Self> {
106 (syntax.kind() == SyntaxKind::Document).then_some(Self { syntax })
107 }
108
109 pub fn syntax(&self) -> &SyntaxNode {
117 &self.syntax
118 }
119
120 pub fn range(&self) -> TextRange {
128 self.syntax.text_range()
129 }
130
131 pub fn directives(&self) -> impl Iterator<Item = DirectiveNode> + '_ {
139 self.syntax.children().filter_map(DirectiveNode::cast)
140 }
141
142 pub fn doc_comments(&self) -> impl Iterator<Item = DocCommentNode> + '_ {
150 self.syntax.children().filter_map(DocCommentNode::cast)
151 }
152
153 pub fn namespaces(&self) -> impl Iterator<Item = NamespaceNode> + '_ {
161 self.syntax.children().filter_map(NamespaceNode::cast)
162 }
163
164 pub fn tasks(&self) -> impl Iterator<Item = TaskNode> + '_ {
172 self.syntax.children().filter_map(TaskNode::cast)
173 }
174}
175
176impl DirectiveNode {
177 pub fn cast(syntax: SyntaxNode) -> Option<Self> {
185 (syntax.kind() == SyntaxKind::Directive).then_some(Self { syntax })
186 }
187
188 pub fn range(&self) -> TextRange {
196 self.syntax.text_range()
197 }
198
199 pub fn keyword_range(&self) -> Option<TextRange> {
207 let mut tokens = self
208 .syntax
209 .children_with_tokens()
210 .filter_map(|element| element.into_token())
211 .filter(|token| {
212 !matches!(
213 token.kind(),
214 SyntaxKind::Whitespace | SyntaxKind::Indent | SyntaxKind::Newline
215 )
216 });
217 let bang = tokens.find(|token| token.kind() == SyntaxKind::Bang)?;
218 let keyword = tokens.next()?;
219 Some(TextRange::new(
220 bang.text_range().start(),
221 keyword.text_range().end(),
222 ))
223 }
224
225 pub fn name(&self) -> Option<SmolStr> {
233 non_trivia_token_texts(&self.syntax).nth(1)
234 }
235
236 pub fn value(&self) -> Option<SmolStr> {
244 let value = non_trivia_token_texts(&self.syntax)
245 .skip(2)
246 .collect::<Vec<_>>()
247 .join(" ");
248 (!value.is_empty()).then(|| SmolStr::new(value))
249 }
250
251 pub fn raw_value(&self) -> Option<SmolStr> {
253 let mut non_trivia = 0usize;
254 let mut value = String::new();
255
256 for token in self
257 .syntax
258 .children_with_tokens()
259 .filter_map(|element| element.into_token())
260 {
261 if token.kind() == SyntaxKind::Newline {
262 break;
263 }
264 if !matches!(
265 token.kind(),
266 SyntaxKind::Whitespace | SyntaxKind::Indent | SyntaxKind::Comment
267 ) {
268 non_trivia += 1;
269 }
270 if non_trivia >= 2 && !(non_trivia == 2 && token.kind() == SyntaxKind::Ident) {
271 value.push_str(token.text());
272 }
273 }
274
275 let value = value.trim();
276 (!value.is_empty()).then(|| SmolStr::new(value))
277 }
278}
279
280impl DocCommentNode {
281 pub fn cast(syntax: SyntaxNode) -> Option<Self> {
289 (syntax.kind() == SyntaxKind::DocComment).then_some(Self { syntax })
290 }
291
292 pub fn range(&self) -> TextRange {
300 self.syntax.text_range()
301 }
302
303 pub fn text(&self) -> Option<SmolStr> {
311 self.syntax
312 .text()
313 .to_string()
314 .trim()
315 .strip_prefix('#')
316 .map(str::trim)
317 .filter(|text| !text.is_empty())
318 .map(SmolStr::new)
319 }
320}
321
322impl NamespaceNode {
323 pub fn cast(syntax: SyntaxNode) -> Option<Self> {
331 (syntax.kind() == SyntaxKind::NamespaceBlock).then_some(Self { syntax })
332 }
333
334 pub fn range(&self) -> TextRange {
342 self.syntax.text_range()
343 }
344
345 pub fn name(&self) -> Option<SmolStr> {
353 self.syntax
354 .text()
355 .to_string()
356 .trim()
357 .strip_prefix('[')
358 .and_then(|text| text.strip_suffix(']'))
359 .map(str::trim)
360 .filter(|text| !text.is_empty())
361 .map(SmolStr::new)
362 }
363}
364
365impl TaskNode {
366 pub fn cast(syntax: SyntaxNode) -> Option<Self> {
374 (syntax.kind() == SyntaxKind::TaskDecl).then_some(Self { syntax })
375 }
376
377 pub fn range(&self) -> TextRange {
385 self.syntax.text_range()
386 }
387
388 pub fn name_range(&self) -> Option<TextRange> {
396 self.syntax
397 .children_with_tokens()
398 .filter_map(|element| element.into_token())
399 .find(|token| token.kind() == SyntaxKind::Ident)
400 .map(|token| token.text_range())
401 }
402
403 pub fn name(&self) -> Option<SmolStr> {
411 self.syntax
412 .children_with_tokens()
413 .filter_map(|element| element.into_token())
414 .find(|token| token.kind() == SyntaxKind::Ident)
415 .map(|token| SmolStr::new(token.text()))
416 }
417
418 pub fn header_text(&self) -> Option<SmolStr> {
426 let mut header = String::new();
427
428 for token in self
429 .syntax
430 .children_with_tokens()
431 .filter_map(|element| element.into_token())
432 {
433 if token.kind() == SyntaxKind::Colon {
434 break;
435 }
436 if token.kind() == SyntaxKind::Newline {
437 break;
438 }
439 header.push_str(token.text());
440 }
441
442 let header = header.trim();
443 (!header.is_empty()).then(|| SmolStr::new(header))
444 }
445
446 pub fn header_info(&self) -> TaskHeaderInfo {
454 parse_task_header(&self.syntax)
455 }
456
457 pub fn commands(&self) -> std::vec::IntoIter<SmolStr> {
465 self.syntax
466 .text()
467 .to_string()
468 .lines()
469 .skip(1)
470 .map(str::trim_start)
471 .filter(|line| !line.is_empty())
472 .filter(|line| !line.starts_with("//"))
473 .map(SmolStr::new)
474 .collect::<Vec<_>>()
475 .into_iter()
476 }
477}
478
479#[derive(Debug, Clone, Copy, PartialEq, Eq)]
480enum HeaderPhase {
481 BeforeTail,
482 Params { depth: usize },
483 Guard { depth: usize },
484 Dependencies,
485}
486
487#[derive(Debug, Clone, Copy, PartialEq, Eq)]
488enum ShellExpectation {
489 None,
490 AllowEqOrName,
491 NeedName,
492}
493
494#[derive(Debug, Default)]
495struct PendingRef {
496 name: String,
497 start: Option<TextSize>,
498 end: Option<TextSize>,
499}
500
501impl PendingRef {
502 fn flush(&mut self, refs: &mut Vec<TaskDependencyRef>, stage: usize) {
503 if let (Some(start), Some(end)) = (self.start, self.end) {
504 let name = self.name.trim();
505 if !name.is_empty() {
506 refs.push(TaskDependencyRef {
507 name: SmolStr::new(name),
508 range: TextRange::new(start, end),
509 stage,
510 });
511 }
512 }
513 self.name.clear();
514 self.start = None;
515 self.end = None;
516 }
517
518 fn extend(&mut self, token: &crate::cst::SyntaxToken) {
519 self.start.get_or_insert(token.text_range().start());
520 self.end = Some(token.text_range().end());
521 self.name.push_str(token.text());
522 }
523}
524
525fn parse_task_header(node: &SyntaxNode) -> TaskHeaderInfo {
526 let mut info = TaskHeaderInfo::default();
527 let mut phase = HeaderPhase::BeforeTail;
528 let mut saw_name = false;
529 let mut stage = 0usize;
530 let mut group_depth = 0usize;
531 let mut pending = PendingRef::default();
532 let mut collector = String::new();
533 let mut dependencies_started = false;
534 let mut shell_expectation = ShellExpectation::None;
535
536 for token in node
537 .children_with_tokens()
538 .filter_map(|element| element.into_token())
539 {
540 let kind = token.kind();
541 if matches!(
542 kind,
543 SyntaxKind::Colon | SyntaxKind::Newline | SyntaxKind::Eof
544 ) {
545 pending.flush(&mut info.dependency_refs, stage);
546 flush_header_collector(&mut info, &phase, &collector, dependencies_started);
547 break;
548 }
549
550 if !saw_name {
551 if kind == SyntaxKind::Ident {
552 saw_name = true;
553 }
554 continue;
555 }
556
557 if !matches!(shell_expectation, ShellExpectation::None) {
558 match (shell_expectation, kind) {
559 (_, SyntaxKind::Whitespace | SyntaxKind::Indent) => continue,
560 (ShellExpectation::AllowEqOrName, SyntaxKind::Eq) => {
561 shell_expectation = ShellExpectation::NeedName;
562 continue;
563 }
564 (_, SyntaxKind::Ident) => {
565 info.shell = Some(SmolStr::new(token.text()));
566 shell_expectation = ShellExpectation::None;
567 continue;
568 }
569 _ => {
570 shell_expectation = ShellExpectation::None;
571 }
572 }
573 }
574
575 match &mut phase {
576 HeaderPhase::BeforeTail => match kind {
577 SyntaxKind::LParen => {
578 collector.clear();
579 phase = HeaderPhase::Params { depth: 1 };
580 }
581 SyntaxKind::Question => {
582 collector.clear();
583 phase = HeaderPhase::Guard { depth: 0 };
584 }
585 SyntaxKind::Amp => {
586 collector.clear();
587 dependencies_started = true;
588 phase = HeaderPhase::Dependencies;
589 }
590 SyntaxKind::ShellFallbackKw => {
591 info.shell_fallback = true;
592 shell_expectation = ShellExpectation::NeedName;
593 }
594 SyntaxKind::ShellKw => shell_expectation = ShellExpectation::AllowEqOrName,
595 _ => {}
596 },
597 HeaderPhase::Params { depth } => match kind {
598 SyntaxKind::LParen => {
599 *depth += 1;
600 collector.push_str(token.text());
601 }
602 SyntaxKind::RParen => {
603 *depth -= 1;
604 if *depth == 0 {
605 let trimmed = collector.trim();
606 if !trimmed.is_empty() {
607 info.params = Some(SmolStr::new(trimmed));
608 }
609 collector.clear();
610 phase = HeaderPhase::BeforeTail;
611 } else {
612 collector.push_str(token.text());
613 }
614 }
615 _ => collector.push_str(token.text()),
616 },
617 HeaderPhase::Guard { depth } => match kind {
618 SyntaxKind::LParen => {
619 *depth += 1;
620 collector.push_str(token.text());
621 }
622 SyntaxKind::RParen => {
623 if *depth > 0 {
624 *depth -= 1;
625 }
626 collector.push_str(token.text());
627 if *depth == 0 {
628 let trimmed = collector.trim();
629 if !trimmed.is_empty() {
630 info.guard = Some(SmolStr::new(trimmed));
631 }
632 collector.clear();
633 phase = HeaderPhase::BeforeTail;
634 }
635 }
636 SyntaxKind::Amp => {
637 let trimmed = collector.trim();
638 if !trimmed.is_empty() {
639 info.guard = Some(SmolStr::new(trimmed));
640 }
641 collector.clear();
642 dependencies_started = true;
643 phase = HeaderPhase::Dependencies;
644 }
645 SyntaxKind::ShellFallbackKw => {
646 let trimmed = collector.trim();
647 if !trimmed.is_empty() {
648 info.guard = Some(SmolStr::new(trimmed));
649 }
650 collector.clear();
651 info.shell_fallback = true;
652 shell_expectation = ShellExpectation::NeedName;
653 phase = HeaderPhase::BeforeTail;
654 }
655 SyntaxKind::ShellKw => {
656 let trimmed = collector.trim();
657 if !trimmed.is_empty() {
658 info.guard = Some(SmolStr::new(trimmed));
659 }
660 collector.clear();
661 shell_expectation = ShellExpectation::AllowEqOrName;
662 phase = HeaderPhase::BeforeTail;
663 }
664 _ => collector.push_str(token.text()),
665 },
666 HeaderPhase::Dependencies => match kind {
667 SyntaxKind::Amp if group_depth == 0 => {
668 pending.flush(&mut info.dependency_refs, stage);
669 if !info.dependency_refs.is_empty() {
670 stage += 1;
671 }
672 if !collector.trim().is_empty() {
673 if !info.dependencies.as_deref().unwrap_or_default().is_empty() {
674 collector.push(' ');
675 }
676 collector.push('&');
677 }
678 }
679 SyntaxKind::LParen => {
680 if group_depth > 0 {
681 pending.extend(&token);
682 }
683 group_depth += 1;
684 collector.push_str(token.text());
685 }
686 SyntaxKind::RParen => {
687 if group_depth > 1 {
688 pending.extend(&token);
689 } else {
690 pending.flush(&mut info.dependency_refs, stage);
691 }
692 group_depth = group_depth.saturating_sub(1);
693 collector.push_str(token.text());
694 }
695 SyntaxKind::ShellFallbackKw if group_depth == 0 => {
696 pending.flush(&mut info.dependency_refs, stage);
697 let trimmed = collector.trim();
698 if !trimmed.is_empty() {
699 info.dependencies = Some(SmolStr::new(trimmed));
700 }
701 collector.clear();
702 info.shell_fallback = true;
703 shell_expectation = ShellExpectation::NeedName;
704 phase = HeaderPhase::BeforeTail;
705 }
706 SyntaxKind::ShellKw if group_depth == 0 => {
707 pending.flush(&mut info.dependency_refs, stage);
708 let trimmed = collector.trim();
709 if !trimmed.is_empty() {
710 info.dependencies = Some(SmolStr::new(trimmed));
711 }
712 collector.clear();
713 shell_expectation = ShellExpectation::AllowEqOrName;
714 phase = HeaderPhase::BeforeTail;
715 }
716 SyntaxKind::Whitespace | SyntaxKind::Indent => {
717 collector.push_str(token.text());
718 }
719 SyntaxKind::Unknown if token.text() == "," && group_depth > 0 => {
720 pending.flush(&mut info.dependency_refs, stage);
721 collector.push_str(token.text());
722 }
723 _ => {
724 pending.extend(&token);
725 collector.push_str(token.text());
726 }
727 },
728 }
729 }
730
731 if info.dependencies.is_none() {
732 let trimmed = collector.trim();
733 if dependencies_started && !trimmed.is_empty() {
734 info.dependencies = Some(SmolStr::new(trimmed));
735 }
736 }
737
738 info
739}
740
741fn flush_header_collector(
742 info: &mut TaskHeaderInfo,
743 phase: &HeaderPhase,
744 collector: &str,
745 dependencies_started: bool,
746) {
747 let trimmed = collector.trim();
748 if trimmed.is_empty() {
749 return;
750 }
751
752 match phase {
753 HeaderPhase::Guard { .. } => info.guard = Some(SmolStr::new(trimmed)),
754 HeaderPhase::Dependencies if dependencies_started => {
755 info.dependencies = Some(SmolStr::new(trimmed))
756 }
757 _ => {}
758 }
759}
760
761fn non_trivia_token_texts(node: &SyntaxNode) -> impl Iterator<Item = SmolStr> + '_ {
762 node.children_with_tokens()
763 .filter_map(|element| element.into_token())
764 .filter(|token| {
765 !matches!(
766 token.kind(),
767 SyntaxKind::Whitespace | SyntaxKind::Indent | SyntaxKind::Newline
768 )
769 })
770 .map(|token| SmolStr::new(token.text()))
771}