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
252impl DocCommentNode {
253 pub fn cast(syntax: SyntaxNode) -> Option<Self> {
261 (syntax.kind() == SyntaxKind::DocComment).then_some(Self { syntax })
262 }
263
264 pub fn range(&self) -> TextRange {
272 self.syntax.text_range()
273 }
274
275 pub fn text(&self) -> Option<SmolStr> {
283 self.syntax
284 .text()
285 .to_string()
286 .trim()
287 .strip_prefix('%')
288 .map(str::trim)
289 .filter(|text| !text.is_empty())
290 .map(SmolStr::new)
291 }
292}
293
294impl NamespaceNode {
295 pub fn cast(syntax: SyntaxNode) -> Option<Self> {
303 (syntax.kind() == SyntaxKind::NamespaceBlock).then_some(Self { syntax })
304 }
305
306 pub fn range(&self) -> TextRange {
314 self.syntax.text_range()
315 }
316
317 pub fn name(&self) -> Option<SmolStr> {
325 self.syntax
326 .text()
327 .to_string()
328 .trim()
329 .strip_prefix('[')
330 .and_then(|text| text.strip_suffix(']'))
331 .map(str::trim)
332 .filter(|text| !text.is_empty())
333 .map(SmolStr::new)
334 }
335}
336
337impl TaskNode {
338 pub fn cast(syntax: SyntaxNode) -> Option<Self> {
346 (syntax.kind() == SyntaxKind::TaskDecl).then_some(Self { syntax })
347 }
348
349 pub fn range(&self) -> TextRange {
357 self.syntax.text_range()
358 }
359
360 pub fn name_range(&self) -> Option<TextRange> {
368 self.syntax
369 .children_with_tokens()
370 .filter_map(|element| element.into_token())
371 .find(|token| token.kind() == SyntaxKind::Ident)
372 .map(|token| token.text_range())
373 }
374
375 pub fn name(&self) -> Option<SmolStr> {
383 self.syntax
384 .children_with_tokens()
385 .filter_map(|element| element.into_token())
386 .find(|token| token.kind() == SyntaxKind::Ident)
387 .map(|token| SmolStr::new(token.text()))
388 }
389
390 pub fn header_text(&self) -> Option<SmolStr> {
398 let mut header = String::new();
399
400 for token in self
401 .syntax
402 .children_with_tokens()
403 .filter_map(|element| element.into_token())
404 {
405 if token.kind() == SyntaxKind::Colon {
406 break;
407 }
408 if token.kind() == SyntaxKind::Newline {
409 break;
410 }
411 header.push_str(token.text());
412 }
413
414 let header = header.trim();
415 (!header.is_empty()).then(|| SmolStr::new(header))
416 }
417
418 pub fn header_info(&self) -> TaskHeaderInfo {
426 parse_task_header(&self.syntax)
427 }
428
429 pub fn commands(&self) -> std::vec::IntoIter<SmolStr> {
437 self.syntax
438 .text()
439 .to_string()
440 .lines()
441 .skip(1)
442 .map(str::trim_start)
443 .filter(|line| !line.is_empty())
444 .map(SmolStr::new)
445 .collect::<Vec<_>>()
446 .into_iter()
447 }
448}
449
450#[derive(Debug, Clone, Copy, PartialEq, Eq)]
451enum HeaderPhase {
452 BeforeTail,
453 Params { depth: usize },
454 Guard { depth: usize },
455 Dependencies,
456}
457
458#[derive(Debug, Clone, Copy, PartialEq, Eq)]
459enum ShellExpectation {
460 None,
461 AllowEqOrName,
462 NeedName,
463}
464
465#[derive(Debug, Default)]
466struct PendingRef {
467 name: String,
468 start: Option<TextSize>,
469 end: Option<TextSize>,
470}
471
472impl PendingRef {
473 fn flush(&mut self, refs: &mut Vec<TaskDependencyRef>, stage: usize) {
474 if let (Some(start), Some(end)) = (self.start, self.end) {
475 let name = self.name.trim();
476 if !name.is_empty() {
477 refs.push(TaskDependencyRef {
478 name: SmolStr::new(name),
479 range: TextRange::new(start, end),
480 stage,
481 });
482 }
483 }
484 self.name.clear();
485 self.start = None;
486 self.end = None;
487 }
488
489 fn extend(&mut self, token: &crate::cst::SyntaxToken) {
490 self.start.get_or_insert(token.text_range().start());
491 self.end = Some(token.text_range().end());
492 self.name.push_str(token.text());
493 }
494}
495
496fn parse_task_header(node: &SyntaxNode) -> TaskHeaderInfo {
497 let mut info = TaskHeaderInfo::default();
498 let mut phase = HeaderPhase::BeforeTail;
499 let mut saw_name = false;
500 let mut stage = 0usize;
501 let mut group_depth = 0usize;
502 let mut pending = PendingRef::default();
503 let mut collector = String::new();
504 let mut dependencies_started = false;
505 let mut shell_expectation = ShellExpectation::None;
506
507 for token in node
508 .children_with_tokens()
509 .filter_map(|element| element.into_token())
510 {
511 let kind = token.kind();
512 if matches!(
513 kind,
514 SyntaxKind::Colon | SyntaxKind::Newline | SyntaxKind::Eof
515 ) {
516 pending.flush(&mut info.dependency_refs, stage);
517 flush_header_collector(&mut info, &phase, &collector, dependencies_started);
518 break;
519 }
520
521 if !saw_name {
522 if kind == SyntaxKind::Ident {
523 saw_name = true;
524 }
525 continue;
526 }
527
528 if !matches!(shell_expectation, ShellExpectation::None) {
529 match (shell_expectation, kind) {
530 (_, SyntaxKind::Whitespace | SyntaxKind::Indent) => continue,
531 (ShellExpectation::AllowEqOrName, SyntaxKind::Eq) => {
532 shell_expectation = ShellExpectation::NeedName;
533 continue;
534 }
535 (_, SyntaxKind::Ident) => {
536 info.shell = Some(SmolStr::new(token.text()));
537 shell_expectation = ShellExpectation::None;
538 continue;
539 }
540 _ => {
541 shell_expectation = ShellExpectation::None;
542 }
543 }
544 }
545
546 match &mut phase {
547 HeaderPhase::BeforeTail => match kind {
548 SyntaxKind::LParen => {
549 collector.clear();
550 phase = HeaderPhase::Params { depth: 1 };
551 }
552 SyntaxKind::Question => {
553 collector.clear();
554 phase = HeaderPhase::Guard { depth: 0 };
555 }
556 SyntaxKind::Amp => {
557 collector.clear();
558 dependencies_started = true;
559 phase = HeaderPhase::Dependencies;
560 }
561 SyntaxKind::ShellFallbackKw => {
562 info.shell_fallback = true;
563 shell_expectation = ShellExpectation::NeedName;
564 }
565 SyntaxKind::ShellKw => shell_expectation = ShellExpectation::AllowEqOrName,
566 _ => {}
567 },
568 HeaderPhase::Params { depth } => match kind {
569 SyntaxKind::LParen => {
570 *depth += 1;
571 collector.push_str(token.text());
572 }
573 SyntaxKind::RParen => {
574 *depth -= 1;
575 if *depth == 0 {
576 let trimmed = collector.trim();
577 if !trimmed.is_empty() {
578 info.params = Some(SmolStr::new(trimmed));
579 }
580 collector.clear();
581 phase = HeaderPhase::BeforeTail;
582 } else {
583 collector.push_str(token.text());
584 }
585 }
586 _ => collector.push_str(token.text()),
587 },
588 HeaderPhase::Guard { depth } => match kind {
589 SyntaxKind::LParen => {
590 *depth += 1;
591 collector.push_str(token.text());
592 }
593 SyntaxKind::RParen => {
594 if *depth > 0 {
595 *depth -= 1;
596 }
597 collector.push_str(token.text());
598 if *depth == 0 {
599 let trimmed = collector.trim();
600 if !trimmed.is_empty() {
601 info.guard = Some(SmolStr::new(trimmed));
602 }
603 collector.clear();
604 phase = HeaderPhase::BeforeTail;
605 }
606 }
607 SyntaxKind::Amp => {
608 let trimmed = collector.trim();
609 if !trimmed.is_empty() {
610 info.guard = Some(SmolStr::new(trimmed));
611 }
612 collector.clear();
613 dependencies_started = true;
614 phase = HeaderPhase::Dependencies;
615 }
616 SyntaxKind::ShellFallbackKw => {
617 let trimmed = collector.trim();
618 if !trimmed.is_empty() {
619 info.guard = Some(SmolStr::new(trimmed));
620 }
621 collector.clear();
622 info.shell_fallback = true;
623 shell_expectation = ShellExpectation::NeedName;
624 phase = HeaderPhase::BeforeTail;
625 }
626 SyntaxKind::ShellKw => {
627 let trimmed = collector.trim();
628 if !trimmed.is_empty() {
629 info.guard = Some(SmolStr::new(trimmed));
630 }
631 collector.clear();
632 shell_expectation = ShellExpectation::AllowEqOrName;
633 phase = HeaderPhase::BeforeTail;
634 }
635 _ => collector.push_str(token.text()),
636 },
637 HeaderPhase::Dependencies => match kind {
638 SyntaxKind::Amp if group_depth == 0 => {
639 pending.flush(&mut info.dependency_refs, stage);
640 if !info.dependency_refs.is_empty() {
641 stage += 1;
642 }
643 if !collector.trim().is_empty() {
644 if !info.dependencies.as_deref().unwrap_or_default().is_empty() {
645 collector.push(' ');
646 }
647 collector.push('&');
648 }
649 }
650 SyntaxKind::LParen => {
651 if group_depth > 0 {
652 pending.extend(&token);
653 }
654 group_depth += 1;
655 collector.push_str(token.text());
656 }
657 SyntaxKind::RParen => {
658 if group_depth > 1 {
659 pending.extend(&token);
660 } else {
661 pending.flush(&mut info.dependency_refs, stage);
662 }
663 group_depth = group_depth.saturating_sub(1);
664 collector.push_str(token.text());
665 }
666 SyntaxKind::ShellFallbackKw if group_depth == 0 => {
667 pending.flush(&mut info.dependency_refs, stage);
668 let trimmed = collector.trim();
669 if !trimmed.is_empty() {
670 info.dependencies = Some(SmolStr::new(trimmed));
671 }
672 collector.clear();
673 info.shell_fallback = true;
674 shell_expectation = ShellExpectation::NeedName;
675 phase = HeaderPhase::BeforeTail;
676 }
677 SyntaxKind::ShellKw if group_depth == 0 => {
678 pending.flush(&mut info.dependency_refs, stage);
679 let trimmed = collector.trim();
680 if !trimmed.is_empty() {
681 info.dependencies = Some(SmolStr::new(trimmed));
682 }
683 collector.clear();
684 shell_expectation = ShellExpectation::AllowEqOrName;
685 phase = HeaderPhase::BeforeTail;
686 }
687 SyntaxKind::Whitespace | SyntaxKind::Indent => {
688 collector.push_str(token.text());
689 }
690 SyntaxKind::Unknown if token.text() == "," && group_depth > 0 => {
691 pending.flush(&mut info.dependency_refs, stage);
692 collector.push_str(token.text());
693 }
694 _ => {
695 pending.extend(&token);
696 collector.push_str(token.text());
697 }
698 },
699 }
700 }
701
702 if info.dependencies.is_none() {
703 let trimmed = collector.trim();
704 if dependencies_started && !trimmed.is_empty() {
705 info.dependencies = Some(SmolStr::new(trimmed));
706 }
707 }
708
709 info
710}
711
712fn flush_header_collector(
713 info: &mut TaskHeaderInfo,
714 phase: &HeaderPhase,
715 collector: &str,
716 dependencies_started: bool,
717) {
718 let trimmed = collector.trim();
719 if trimmed.is_empty() {
720 return;
721 }
722
723 match phase {
724 HeaderPhase::Guard { .. } => info.guard = Some(SmolStr::new(trimmed)),
725 HeaderPhase::Dependencies if dependencies_started => {
726 info.dependencies = Some(SmolStr::new(trimmed))
727 }
728 _ => {}
729 }
730}
731
732fn non_trivia_token_texts(node: &SyntaxNode) -> impl Iterator<Item = SmolStr> + '_ {
733 node.children_with_tokens()
734 .filter_map(|element| element.into_token())
735 .filter(|token| {
736 !matches!(
737 token.kind(),
738 SyntaxKind::Whitespace | SyntaxKind::Indent | SyntaxKind::Newline
739 )
740 })
741 .map(|token| SmolStr::new(token.text()))
742}