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