use smol_str::SmolStr;
use text_size::{TextRange, TextSize};
use crate::{SyntaxKind, SyntaxNode};
#[derive(Debug, Clone)]
pub struct DocumentNode {
syntax: SyntaxNode,
}
#[derive(Debug, Clone)]
pub struct DirectiveNode {
syntax: SyntaxNode,
}
#[derive(Debug, Clone)]
pub struct DocCommentNode {
syntax: SyntaxNode,
}
#[derive(Debug, Clone)]
pub struct NamespaceNode {
syntax: SyntaxNode,
}
#[derive(Debug, Clone)]
pub struct TaskNode {
syntax: SyntaxNode,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TaskDependencyRef {
pub name: SmolStr,
pub range: TextRange,
pub stage: usize,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct TaskHeaderInfo {
pub params: Option<SmolStr>,
pub guard: Option<SmolStr>,
pub dependencies: Option<SmolStr>,
pub shell: Option<SmolStr>,
pub shell_fallback: bool,
pub dependency_refs: Vec<TaskDependencyRef>,
}
impl DocumentNode {
pub fn cast(syntax: SyntaxNode) -> Option<Self> {
(syntax.kind() == SyntaxKind::Document).then_some(Self { syntax })
}
pub fn syntax(&self) -> &SyntaxNode {
&self.syntax
}
pub fn range(&self) -> TextRange {
self.syntax.text_range()
}
pub fn directives(&self) -> impl Iterator<Item = DirectiveNode> + '_ {
self.syntax.children().filter_map(DirectiveNode::cast)
}
pub fn doc_comments(&self) -> impl Iterator<Item = DocCommentNode> + '_ {
self.syntax.children().filter_map(DocCommentNode::cast)
}
pub fn namespaces(&self) -> impl Iterator<Item = NamespaceNode> + '_ {
self.syntax.children().filter_map(NamespaceNode::cast)
}
pub fn tasks(&self) -> impl Iterator<Item = TaskNode> + '_ {
self.syntax.children().filter_map(TaskNode::cast)
}
}
impl DirectiveNode {
pub fn cast(syntax: SyntaxNode) -> Option<Self> {
(syntax.kind() == SyntaxKind::Directive).then_some(Self { syntax })
}
pub fn range(&self) -> TextRange {
self.syntax.text_range()
}
pub fn keyword_range(&self) -> Option<TextRange> {
let mut tokens = self
.syntax
.children_with_tokens()
.filter_map(|element| element.into_token())
.filter(|token| {
!matches!(
token.kind(),
SyntaxKind::Whitespace | SyntaxKind::Indent | SyntaxKind::Newline
)
});
let bang = tokens.find(|token| token.kind() == SyntaxKind::Bang)?;
let keyword = tokens.next()?;
Some(TextRange::new(
bang.text_range().start(),
keyword.text_range().end(),
))
}
pub fn name(&self) -> Option<SmolStr> {
non_trivia_token_texts(&self.syntax).nth(1)
}
pub fn value(&self) -> Option<SmolStr> {
let value = non_trivia_token_texts(&self.syntax)
.skip(2)
.collect::<Vec<_>>()
.join(" ");
(!value.is_empty()).then(|| SmolStr::new(value))
}
}
impl DocCommentNode {
pub fn cast(syntax: SyntaxNode) -> Option<Self> {
(syntax.kind() == SyntaxKind::DocComment).then_some(Self { syntax })
}
pub fn range(&self) -> TextRange {
self.syntax.text_range()
}
pub fn text(&self) -> Option<SmolStr> {
self.syntax
.text()
.to_string()
.trim()
.strip_prefix('%')
.map(str::trim)
.filter(|text| !text.is_empty())
.map(SmolStr::new)
}
}
impl NamespaceNode {
pub fn cast(syntax: SyntaxNode) -> Option<Self> {
(syntax.kind() == SyntaxKind::NamespaceBlock).then_some(Self { syntax })
}
pub fn range(&self) -> TextRange {
self.syntax.text_range()
}
pub fn name(&self) -> Option<SmolStr> {
self.syntax
.text()
.to_string()
.trim()
.strip_prefix('[')
.and_then(|text| text.strip_suffix(']'))
.map(str::trim)
.filter(|text| !text.is_empty())
.map(SmolStr::new)
}
}
impl TaskNode {
pub fn cast(syntax: SyntaxNode) -> Option<Self> {
(syntax.kind() == SyntaxKind::TaskDecl).then_some(Self { syntax })
}
pub fn range(&self) -> TextRange {
self.syntax.text_range()
}
pub fn name_range(&self) -> Option<TextRange> {
self.syntax
.children_with_tokens()
.filter_map(|element| element.into_token())
.find(|token| token.kind() == SyntaxKind::Ident)
.map(|token| token.text_range())
}
pub fn name(&self) -> Option<SmolStr> {
self.syntax
.children_with_tokens()
.filter_map(|element| element.into_token())
.find(|token| token.kind() == SyntaxKind::Ident)
.map(|token| SmolStr::new(token.text()))
}
pub fn header_text(&self) -> Option<SmolStr> {
let mut header = String::new();
for token in self
.syntax
.children_with_tokens()
.filter_map(|element| element.into_token())
{
if token.kind() == SyntaxKind::Colon {
break;
}
if token.kind() == SyntaxKind::Newline {
break;
}
header.push_str(token.text());
}
let header = header.trim();
(!header.is_empty()).then(|| SmolStr::new(header))
}
pub fn header_info(&self) -> TaskHeaderInfo {
parse_task_header(&self.syntax)
}
pub fn commands(&self) -> std::vec::IntoIter<SmolStr> {
self.syntax
.text()
.to_string()
.lines()
.skip(1)
.map(str::trim_start)
.filter(|line| !line.is_empty())
.map(SmolStr::new)
.collect::<Vec<_>>()
.into_iter()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum HeaderPhase {
BeforeTail,
Params { depth: usize },
Guard { depth: usize },
Dependencies,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ShellExpectation {
None,
AllowEqOrName,
NeedName,
}
#[derive(Debug, Default)]
struct PendingRef {
name: String,
start: Option<TextSize>,
end: Option<TextSize>,
}
impl PendingRef {
fn flush(&mut self, refs: &mut Vec<TaskDependencyRef>, stage: usize) {
if let (Some(start), Some(end)) = (self.start, self.end) {
let name = self.name.trim();
if !name.is_empty() {
refs.push(TaskDependencyRef {
name: SmolStr::new(name),
range: TextRange::new(start, end),
stage,
});
}
}
self.name.clear();
self.start = None;
self.end = None;
}
fn extend(&mut self, token: &crate::cst::SyntaxToken) {
self.start.get_or_insert(token.text_range().start());
self.end = Some(token.text_range().end());
self.name.push_str(token.text());
}
}
fn parse_task_header(node: &SyntaxNode) -> TaskHeaderInfo {
let mut info = TaskHeaderInfo::default();
let mut phase = HeaderPhase::BeforeTail;
let mut saw_name = false;
let mut stage = 0usize;
let mut group_depth = 0usize;
let mut pending = PendingRef::default();
let mut collector = String::new();
let mut dependencies_started = false;
let mut shell_expectation = ShellExpectation::None;
for token in node
.children_with_tokens()
.filter_map(|element| element.into_token())
{
let kind = token.kind();
if matches!(
kind,
SyntaxKind::Colon | SyntaxKind::Newline | SyntaxKind::Eof
) {
pending.flush(&mut info.dependency_refs, stage);
flush_header_collector(&mut info, &phase, &collector, dependencies_started);
break;
}
if !saw_name {
if kind == SyntaxKind::Ident {
saw_name = true;
}
continue;
}
if !matches!(shell_expectation, ShellExpectation::None) {
match (shell_expectation, kind) {
(_, SyntaxKind::Whitespace | SyntaxKind::Indent) => continue,
(ShellExpectation::AllowEqOrName, SyntaxKind::Eq) => {
shell_expectation = ShellExpectation::NeedName;
continue;
}
(_, SyntaxKind::Ident) => {
info.shell = Some(SmolStr::new(token.text()));
shell_expectation = ShellExpectation::None;
continue;
}
_ => {
shell_expectation = ShellExpectation::None;
}
}
}
match &mut phase {
HeaderPhase::BeforeTail => match kind {
SyntaxKind::LParen => {
collector.clear();
phase = HeaderPhase::Params { depth: 1 };
}
SyntaxKind::Question => {
collector.clear();
phase = HeaderPhase::Guard { depth: 0 };
}
SyntaxKind::Amp => {
collector.clear();
dependencies_started = true;
phase = HeaderPhase::Dependencies;
}
SyntaxKind::ShellFallbackKw => {
info.shell_fallback = true;
shell_expectation = ShellExpectation::NeedName;
}
SyntaxKind::ShellKw => shell_expectation = ShellExpectation::AllowEqOrName,
_ => {}
},
HeaderPhase::Params { depth } => match kind {
SyntaxKind::LParen => {
*depth += 1;
collector.push_str(token.text());
}
SyntaxKind::RParen => {
*depth -= 1;
if *depth == 0 {
let trimmed = collector.trim();
if !trimmed.is_empty() {
info.params = Some(SmolStr::new(trimmed));
}
collector.clear();
phase = HeaderPhase::BeforeTail;
} else {
collector.push_str(token.text());
}
}
_ => collector.push_str(token.text()),
},
HeaderPhase::Guard { depth } => match kind {
SyntaxKind::LParen => {
*depth += 1;
collector.push_str(token.text());
}
SyntaxKind::RParen => {
if *depth > 0 {
*depth -= 1;
}
collector.push_str(token.text());
if *depth == 0 {
let trimmed = collector.trim();
if !trimmed.is_empty() {
info.guard = Some(SmolStr::new(trimmed));
}
collector.clear();
phase = HeaderPhase::BeforeTail;
}
}
SyntaxKind::Amp => {
let trimmed = collector.trim();
if !trimmed.is_empty() {
info.guard = Some(SmolStr::new(trimmed));
}
collector.clear();
dependencies_started = true;
phase = HeaderPhase::Dependencies;
}
SyntaxKind::ShellFallbackKw => {
let trimmed = collector.trim();
if !trimmed.is_empty() {
info.guard = Some(SmolStr::new(trimmed));
}
collector.clear();
info.shell_fallback = true;
shell_expectation = ShellExpectation::NeedName;
phase = HeaderPhase::BeforeTail;
}
SyntaxKind::ShellKw => {
let trimmed = collector.trim();
if !trimmed.is_empty() {
info.guard = Some(SmolStr::new(trimmed));
}
collector.clear();
shell_expectation = ShellExpectation::AllowEqOrName;
phase = HeaderPhase::BeforeTail;
}
_ => collector.push_str(token.text()),
},
HeaderPhase::Dependencies => match kind {
SyntaxKind::Amp if group_depth == 0 => {
pending.flush(&mut info.dependency_refs, stage);
if !info.dependency_refs.is_empty() {
stage += 1;
}
if !collector.trim().is_empty() {
if !info.dependencies.as_deref().unwrap_or_default().is_empty() {
collector.push(' ');
}
collector.push('&');
}
}
SyntaxKind::LParen => {
if group_depth > 0 {
pending.extend(&token);
}
group_depth += 1;
collector.push_str(token.text());
}
SyntaxKind::RParen => {
if group_depth > 1 {
pending.extend(&token);
} else {
pending.flush(&mut info.dependency_refs, stage);
}
group_depth = group_depth.saturating_sub(1);
collector.push_str(token.text());
}
SyntaxKind::ShellFallbackKw if group_depth == 0 => {
pending.flush(&mut info.dependency_refs, stage);
let trimmed = collector.trim();
if !trimmed.is_empty() {
info.dependencies = Some(SmolStr::new(trimmed));
}
collector.clear();
info.shell_fallback = true;
shell_expectation = ShellExpectation::NeedName;
phase = HeaderPhase::BeforeTail;
}
SyntaxKind::ShellKw if group_depth == 0 => {
pending.flush(&mut info.dependency_refs, stage);
let trimmed = collector.trim();
if !trimmed.is_empty() {
info.dependencies = Some(SmolStr::new(trimmed));
}
collector.clear();
shell_expectation = ShellExpectation::AllowEqOrName;
phase = HeaderPhase::BeforeTail;
}
SyntaxKind::Whitespace | SyntaxKind::Indent => {
collector.push_str(token.text());
}
SyntaxKind::Unknown if token.text() == "," && group_depth > 0 => {
pending.flush(&mut info.dependency_refs, stage);
collector.push_str(token.text());
}
_ => {
pending.extend(&token);
collector.push_str(token.text());
}
},
}
}
if info.dependencies.is_none() {
let trimmed = collector.trim();
if dependencies_started && !trimmed.is_empty() {
info.dependencies = Some(SmolStr::new(trimmed));
}
}
info
}
fn flush_header_collector(
info: &mut TaskHeaderInfo,
phase: &HeaderPhase,
collector: &str,
dependencies_started: bool,
) {
let trimmed = collector.trim();
if trimmed.is_empty() {
return;
}
match phase {
HeaderPhase::Guard { .. } => info.guard = Some(SmolStr::new(trimmed)),
HeaderPhase::Dependencies if dependencies_started => {
info.dependencies = Some(SmolStr::new(trimmed))
}
_ => {}
}
}
fn non_trivia_token_texts(node: &SyntaxNode) -> impl Iterator<Item = SmolStr> + '_ {
node.children_with_tokens()
.filter_map(|element| element.into_token())
.filter(|token| {
!matches!(
token.kind(),
SyntaxKind::Whitespace | SyntaxKind::Indent | SyntaxKind::Newline
)
})
.map(|token| SmolStr::new(token.text()))
}