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 .filter(|line| !line.starts_with("//"))
445 .map(SmolStr::new)
446 .collect::<Vec<_>>()
447 .into_iter()
448 }
449}
450
451#[derive(Debug, Clone, Copy, PartialEq, Eq)]
452enum HeaderPhase {
453 BeforeTail,
454 Params { depth: usize },
455 Guard { depth: usize },
456 Dependencies,
457}
458
459#[derive(Debug, Clone, Copy, PartialEq, Eq)]
460enum ShellExpectation {
461 None,
462 AllowEqOrName,
463 NeedName,
464}
465
466#[derive(Debug, Default)]
467struct PendingRef {
468 name: String,
469 start: Option<TextSize>,
470 end: Option<TextSize>,
471}
472
473impl PendingRef {
474 fn flush(&mut self, refs: &mut Vec<TaskDependencyRef>, stage: usize) {
475 if let (Some(start), Some(end)) = (self.start, self.end) {
476 let name = self.name.trim();
477 if !name.is_empty() {
478 refs.push(TaskDependencyRef {
479 name: SmolStr::new(name),
480 range: TextRange::new(start, end),
481 stage,
482 });
483 }
484 }
485 self.name.clear();
486 self.start = None;
487 self.end = None;
488 }
489
490 fn extend(&mut self, token: &crate::cst::SyntaxToken) {
491 self.start.get_or_insert(token.text_range().start());
492 self.end = Some(token.text_range().end());
493 self.name.push_str(token.text());
494 }
495}
496
497fn parse_task_header(node: &SyntaxNode) -> TaskHeaderInfo {
498 let mut info = TaskHeaderInfo::default();
499 let mut phase = HeaderPhase::BeforeTail;
500 let mut saw_name = false;
501 let mut stage = 0usize;
502 let mut group_depth = 0usize;
503 let mut pending = PendingRef::default();
504 let mut collector = String::new();
505 let mut dependencies_started = false;
506 let mut shell_expectation = ShellExpectation::None;
507
508 for token in node
509 .children_with_tokens()
510 .filter_map(|element| element.into_token())
511 {
512 let kind = token.kind();
513 if matches!(
514 kind,
515 SyntaxKind::Colon | SyntaxKind::Newline | SyntaxKind::Eof
516 ) {
517 pending.flush(&mut info.dependency_refs, stage);
518 flush_header_collector(&mut info, &phase, &collector, dependencies_started);
519 break;
520 }
521
522 if !saw_name {
523 if kind == SyntaxKind::Ident {
524 saw_name = true;
525 }
526 continue;
527 }
528
529 if !matches!(shell_expectation, ShellExpectation::None) {
530 match (shell_expectation, kind) {
531 (_, SyntaxKind::Whitespace | SyntaxKind::Indent) => continue,
532 (ShellExpectation::AllowEqOrName, SyntaxKind::Eq) => {
533 shell_expectation = ShellExpectation::NeedName;
534 continue;
535 }
536 (_, SyntaxKind::Ident) => {
537 info.shell = Some(SmolStr::new(token.text()));
538 shell_expectation = ShellExpectation::None;
539 continue;
540 }
541 _ => {
542 shell_expectation = ShellExpectation::None;
543 }
544 }
545 }
546
547 match &mut phase {
548 HeaderPhase::BeforeTail => match kind {
549 SyntaxKind::LParen => {
550 collector.clear();
551 phase = HeaderPhase::Params { depth: 1 };
552 }
553 SyntaxKind::Question => {
554 collector.clear();
555 phase = HeaderPhase::Guard { depth: 0 };
556 }
557 SyntaxKind::Amp => {
558 collector.clear();
559 dependencies_started = true;
560 phase = HeaderPhase::Dependencies;
561 }
562 SyntaxKind::ShellFallbackKw => {
563 info.shell_fallback = true;
564 shell_expectation = ShellExpectation::NeedName;
565 }
566 SyntaxKind::ShellKw => shell_expectation = ShellExpectation::AllowEqOrName,
567 _ => {}
568 },
569 HeaderPhase::Params { depth } => match kind {
570 SyntaxKind::LParen => {
571 *depth += 1;
572 collector.push_str(token.text());
573 }
574 SyntaxKind::RParen => {
575 *depth -= 1;
576 if *depth == 0 {
577 let trimmed = collector.trim();
578 if !trimmed.is_empty() {
579 info.params = Some(SmolStr::new(trimmed));
580 }
581 collector.clear();
582 phase = HeaderPhase::BeforeTail;
583 } else {
584 collector.push_str(token.text());
585 }
586 }
587 _ => collector.push_str(token.text()),
588 },
589 HeaderPhase::Guard { depth } => match kind {
590 SyntaxKind::LParen => {
591 *depth += 1;
592 collector.push_str(token.text());
593 }
594 SyntaxKind::RParen => {
595 if *depth > 0 {
596 *depth -= 1;
597 }
598 collector.push_str(token.text());
599 if *depth == 0 {
600 let trimmed = collector.trim();
601 if !trimmed.is_empty() {
602 info.guard = Some(SmolStr::new(trimmed));
603 }
604 collector.clear();
605 phase = HeaderPhase::BeforeTail;
606 }
607 }
608 SyntaxKind::Amp => {
609 let trimmed = collector.trim();
610 if !trimmed.is_empty() {
611 info.guard = Some(SmolStr::new(trimmed));
612 }
613 collector.clear();
614 dependencies_started = true;
615 phase = HeaderPhase::Dependencies;
616 }
617 SyntaxKind::ShellFallbackKw => {
618 let trimmed = collector.trim();
619 if !trimmed.is_empty() {
620 info.guard = Some(SmolStr::new(trimmed));
621 }
622 collector.clear();
623 info.shell_fallback = true;
624 shell_expectation = ShellExpectation::NeedName;
625 phase = HeaderPhase::BeforeTail;
626 }
627 SyntaxKind::ShellKw => {
628 let trimmed = collector.trim();
629 if !trimmed.is_empty() {
630 info.guard = Some(SmolStr::new(trimmed));
631 }
632 collector.clear();
633 shell_expectation = ShellExpectation::AllowEqOrName;
634 phase = HeaderPhase::BeforeTail;
635 }
636 _ => collector.push_str(token.text()),
637 },
638 HeaderPhase::Dependencies => match kind {
639 SyntaxKind::Amp if group_depth == 0 => {
640 pending.flush(&mut info.dependency_refs, stage);
641 if !info.dependency_refs.is_empty() {
642 stage += 1;
643 }
644 if !collector.trim().is_empty() {
645 if !info.dependencies.as_deref().unwrap_or_default().is_empty() {
646 collector.push(' ');
647 }
648 collector.push('&');
649 }
650 }
651 SyntaxKind::LParen => {
652 if group_depth > 0 {
653 pending.extend(&token);
654 }
655 group_depth += 1;
656 collector.push_str(token.text());
657 }
658 SyntaxKind::RParen => {
659 if group_depth > 1 {
660 pending.extend(&token);
661 } else {
662 pending.flush(&mut info.dependency_refs, stage);
663 }
664 group_depth = group_depth.saturating_sub(1);
665 collector.push_str(token.text());
666 }
667 SyntaxKind::ShellFallbackKw if group_depth == 0 => {
668 pending.flush(&mut info.dependency_refs, stage);
669 let trimmed = collector.trim();
670 if !trimmed.is_empty() {
671 info.dependencies = Some(SmolStr::new(trimmed));
672 }
673 collector.clear();
674 info.shell_fallback = true;
675 shell_expectation = ShellExpectation::NeedName;
676 phase = HeaderPhase::BeforeTail;
677 }
678 SyntaxKind::ShellKw if group_depth == 0 => {
679 pending.flush(&mut info.dependency_refs, stage);
680 let trimmed = collector.trim();
681 if !trimmed.is_empty() {
682 info.dependencies = Some(SmolStr::new(trimmed));
683 }
684 collector.clear();
685 shell_expectation = ShellExpectation::AllowEqOrName;
686 phase = HeaderPhase::BeforeTail;
687 }
688 SyntaxKind::Whitespace | SyntaxKind::Indent => {
689 collector.push_str(token.text());
690 }
691 SyntaxKind::Unknown if token.text() == "," && group_depth > 0 => {
692 pending.flush(&mut info.dependency_refs, stage);
693 collector.push_str(token.text());
694 }
695 _ => {
696 pending.extend(&token);
697 collector.push_str(token.text());
698 }
699 },
700 }
701 }
702
703 if info.dependencies.is_none() {
704 let trimmed = collector.trim();
705 if dependencies_started && !trimmed.is_empty() {
706 info.dependencies = Some(SmolStr::new(trimmed));
707 }
708 }
709
710 info
711}
712
713fn flush_header_collector(
714 info: &mut TaskHeaderInfo,
715 phase: &HeaderPhase,
716 collector: &str,
717 dependencies_started: bool,
718) {
719 let trimmed = collector.trim();
720 if trimmed.is_empty() {
721 return;
722 }
723
724 match phase {
725 HeaderPhase::Guard { .. } => info.guard = Some(SmolStr::new(trimmed)),
726 HeaderPhase::Dependencies if dependencies_started => {
727 info.dependencies = Some(SmolStr::new(trimmed))
728 }
729 _ => {}
730 }
731}
732
733fn non_trivia_token_texts(node: &SyntaxNode) -> impl Iterator<Item = SmolStr> + '_ {
734 node.children_with_tokens()
735 .filter_map(|element| element.into_token())
736 .filter(|token| {
737 !matches!(
738 token.kind(),
739 SyntaxKind::Whitespace | SyntaxKind::Indent | SyntaxKind::Newline
740 )
741 })
742 .map(|token| SmolStr::new(token.text()))
743}