use rowan::{NodeOrToken, SyntaxElement, TextRange, TextSize};
use arity_parser::directive::{Parsed, Verb, parse};
use super::ir::Ir;
use super::trivia::is_trivia;
use crate::syntax::{RLanguage, SyntaxKind, SyntaxToken};
pub fn is_honored_position(token: &SyntaxToken) -> bool {
token
.parent()
.is_some_and(|parent| matches!(parent.kind(), SyntaxKind::ROOT | SyntaxKind::BLOCK_EXPR))
}
pub(super) fn is_honored_directive(token: &SyntaxToken, verb: Verb) -> bool {
if verb == Verb::SkipFile {
return true;
}
is_honored_position(token) && comment_occupies_line(token)
}
fn comment_occupies_line(token: &SyntaxToken) -> bool {
let before_is_clear = std::iter::successors(token.prev_token(), |token| token.prev_token())
.take_while(|token| token.kind() != SyntaxKind::NEWLINE)
.all(|token| token.kind() == SyntaxKind::WHITESPACE);
let after_is_clear = std::iter::successors(token.next_token(), |token| token.next_token())
.take_while(|token| token.kind() != SyntaxKind::NEWLINE)
.all(|token| token.kind() == SyntaxKind::WHITESPACE);
before_is_clear && after_is_clear
}
pub(super) fn is_skip_file(text: &str) -> bool {
matches!(parse(text), Some(Parsed::Directive(d))
if d.tool.affects_format() && d.verb == Verb::SkipFile)
}
pub(super) fn dcf_file_is_skipped(root: &crate::dcf::SyntaxNode) -> bool {
root.descendants_with_tokens().any(|element| {
let NodeOrToken::Token(token) = element else {
return false;
};
token.kind() == crate::dcf::SyntaxKind::COMMENT && is_skip_file(token.text())
})
}
#[derive(Debug, Default)]
pub(super) struct SkipPlan {
runs: Vec<(usize, usize, TextRange)>,
}
impl SkipPlan {
pub(super) fn run_at(&self, line: usize) -> Option<(usize, TextRange)> {
self.runs
.iter()
.find(|(start, _, _)| *start == line)
.map(|(_, end, range)| (*end, *range))
}
pub(super) fn contains(&self, line: usize) -> bool {
self.runs
.iter()
.any(|(start, end, _)| *start <= line && line <= *end)
}
}
pub(super) fn plan(
lines: &[Vec<SyntaxElement<RLanguage>>],
ignored_directive: Option<TextRange>,
) -> SkipPlan {
let mut plan = SkipPlan::default();
let mut pending_skip = false;
let mut region_start: Option<usize> = None;
for (idx, line) in lines.iter().enumerate() {
match line_directive(line, ignored_directive) {
Some(Verb::Skip) => {
pending_skip = true;
continue;
}
Some(Verb::Off) => {
region_start.get_or_insert(idx + 1);
continue;
}
Some(Verb::On) => {
if let Some(start) = region_start.take() {
push_run(&mut plan, lines, start, idx.saturating_sub(1));
}
continue;
}
Some(Verb::SkipFile) | None => {}
}
if region_start.is_some() || is_blank(line) || is_comment_only(line) {
continue;
}
if pending_skip {
pending_skip = false;
push_run(&mut plan, lines, idx, idx);
}
}
if let Some(start) = region_start {
push_run(&mut plan, lines, start, lines.len().saturating_sub(1));
}
plan
}
fn push_run(
plan: &mut SkipPlan,
lines: &[Vec<SyntaxElement<RLanguage>>],
start: usize,
end: usize,
) {
let significant = |idx: usize| lines.get(idx).is_some_and(|line| !is_blank(line));
let Some(first) = (start..=end).find(|&idx| significant(idx)) else {
return;
};
let last = (first..=end)
.rfind(|&idx| significant(idx))
.unwrap_or(first);
let (Some(from), Some(to)) = (line_start(&lines[first]), line_end(&lines[last])) else {
return;
};
plan.runs.push((first, last, TextRange::new(from, to)));
}
fn line_directive(
line: &[SyntaxElement<RLanguage>],
ignored_directive: Option<TextRange>,
) -> Option<Verb> {
let elements = significant(line);
let [NodeOrToken::Token(token)] = elements.as_slice() else {
return None;
};
if token.kind() != SyntaxKind::COMMENT {
return None;
}
if ignored_directive == Some(token.text_range()) {
return None;
}
match parse(token.text())? {
Parsed::Directive(d) if d.tool.affects_format() => Some(d.verb),
_ => None,
}
}
fn line_start(line: &[SyntaxElement<RLanguage>]) -> Option<TextSize> {
let first = significant(line).into_iter().next()?;
let start = match first.prev_sibling_or_token() {
Some(NodeOrToken::Token(ws)) if ws.kind() == SyntaxKind::WHITESPACE => {
ws.text_range().start()
}
_ => first.text_range().start(),
};
Some(start)
}
fn line_end(line: &[SyntaxElement<RLanguage>]) -> Option<TextSize> {
significant(line).last().map(|el| el.text_range().end())
}
fn significant(line: &[SyntaxElement<RLanguage>]) -> Vec<SyntaxElement<RLanguage>> {
line.iter()
.filter(|el| !is_trivia(el.kind()))
.cloned()
.collect()
}
fn is_blank(line: &[SyntaxElement<RLanguage>]) -> bool {
significant(line).is_empty()
}
fn is_comment_only(line: &[SyntaxElement<RLanguage>]) -> bool {
let significant = significant(line);
!significant.is_empty()
&& significant
.iter()
.all(|el| el.kind() == SyntaxKind::COMMENT)
}
pub(super) fn skipped_at(
lines: &[Vec<SyntaxElement<RLanguage>>],
plan: &SkipPlan,
idx: usize,
) -> Option<(Ir, usize)> {
let (last, range) = plan.run_at(idx)?;
let anchor = significant(lines.get(idx)?).into_iter().next()?;
Some((Ir::skipped(source_text(&anchor, range)), last))
}
fn source_text(anchor: &SyntaxElement<RLanguage>, range: TextRange) -> String {
let root = match anchor {
NodeOrToken::Node(node) => node.ancestors().last(),
NodeOrToken::Token(token) => token.parent_ancestors().last(),
};
root.map(|root| root.text().slice(range).to_string())
.unwrap_or_default()
}