use rnix::{SyntaxKind, SyntaxNode, SyntaxToken, TextRange, TextSize};
use crate::follows::strip_outer_quotes;
use crate::input::Input;
use super::node::{extract_indent, parse_node, trailing_inline_comments};
#[derive(Debug, Clone)]
pub(crate) struct Alternate {
pub(crate) url: String,
token: SyntaxToken,
}
pub(crate) fn url_binding(root: &SyntaxNode, input: &Input) -> Option<SyntaxNode> {
if input.url().is_empty() || input.range.is_empty() {
return None;
}
let range = TextRange::new(
TextSize::from(input.range.start as u32),
TextSize::from(input.range.end as u32),
);
if !root.text_range().contains_range(range) {
return None;
}
let covering = root.covering_element(range);
let node = match covering {
rnix::NodeOrToken::Node(node) => node,
rnix::NodeOrToken::Token(token) => token.parent()?,
};
let binding = node
.ancestors()
.find(|a| a.kind() == SyntaxKind::NODE_ATTRPATH_VALUE)?;
let segments = attrpath_segments(&binding)?;
(segments.last().map(String::as_str) == Some("url")).then_some(binding)
}
fn attrpath_segments(binding: &SyntaxNode) -> Option<Vec<String>> {
let attrpath = binding
.children()
.find(|c| c.kind() == SyntaxKind::NODE_ATTRPATH)?;
Some(
attrpath
.children()
.map(|c| strip_outer_quotes(&c.to_string()).to_string())
.collect(),
)
}
pub(crate) fn alternates(binding: &SyntaxNode) -> Vec<Alternate> {
let Some(expected) = attrpath_segments(binding) else {
return Vec::new();
};
let mut above = Vec::new();
let mut cursor = binding.prev_sibling_or_token();
while let Some(el) = cursor.clone() {
match el.kind() {
SyntaxKind::TOKEN_WHITESPACE => {
if el.to_string().matches('\n').count() >= 2 {
break;
}
}
SyntaxKind::TOKEN_COMMENT => {
if !on_own_line(&el) {
break;
}
if let Some(token) = el.as_token()
&& let Some(url) = parse_alternate(&token.to_string(), &expected)
{
above.push(Alternate {
url,
token: token.clone(),
});
}
}
_ => break,
}
cursor = el.prev_sibling_or_token();
}
above.reverse();
let mut found = above;
let mut cursor = binding.next_sibling_or_token();
while let Some(el) = cursor.clone() {
match el.kind() {
SyntaxKind::TOKEN_WHITESPACE => {
if el.to_string().matches('\n').count() >= 2 {
break;
}
}
SyntaxKind::TOKEN_COMMENT => {
if on_own_line(&el)
&& let Some(token) = el.as_token()
&& let Some(url) = parse_alternate(&token.to_string(), &expected)
{
found.push(Alternate {
url,
token: token.clone(),
});
}
}
_ => break,
}
cursor = el.next_sibling_or_token();
}
found
}
fn on_own_line(el: &rnix::SyntaxElement) -> bool {
match el.prev_sibling_or_token() {
None => true,
Some(prev) => {
prev.kind() == SyntaxKind::TOKEN_WHITESPACE && prev.to_string().contains('\n')
}
}
}
fn uncomment(comment: &str) -> &str {
let body = comment.strip_prefix('#').unwrap_or(comment);
body.strip_prefix(' ').unwrap_or(body)
}
fn parse_alternate(comment: &str, expected: &[String]) -> Option<String> {
if !comment.starts_with('#') {
return None;
}
let body = uncomment(comment);
let parse = rnix::Root::parse(&format!("{{\n{body}\n}}"));
if !parse.errors().is_empty() {
return None;
}
let attr_set = parse.syntax().first_child()?;
if attr_set.kind() != SyntaxKind::NODE_ATTR_SET {
return None;
}
let mut bindings = attr_set.children();
let binding = bindings.next()?;
if bindings.next().is_some() || binding.kind() != SyntaxKind::NODE_ATTRPATH_VALUE {
return None;
}
let segments = attrpath_segments(&binding)?;
if segments != expected {
return None;
}
let attrpath = binding
.children()
.find(|c| c.kind() == SyntaxKind::NODE_ATTRPATH)?;
let value = attrpath.next_sibling()?;
if value.kind() != SyntaxKind::NODE_STRING {
return None;
}
Some(strip_outer_quotes(&value.to_string()).to_string())
}
fn deactivated_line(binding: &SyntaxNode) -> (String, Vec<usize>) {
let element: rnix::SyntaxElement = binding.clone().into();
let tail = trailing_inline_comments(&element);
let mut text = format!("# {binding}");
for token in &tail {
text.push_str(&token.to_string());
}
(text, tail.iter().map(|token| token.index()).collect())
}
pub(crate) fn flip(parent: &SyntaxNode, binding: &SyntaxNode, alternate: &Alternate) -> SyntaxNode {
let activated = parse_node(uncomment(&alternate.token.to_string()));
let (comment_text, tail) = deactivated_line(binding);
let mut green = parent
.green()
.replace_child(alternate.token.index(), activated.green().into());
green = green.replace_child(binding.index(), parse_node(&comment_text).green().into());
for index in tail.iter().rev() {
green = green.remove_child(*index);
}
SyntaxNode::new_root(parent.replace_with(green))
}
pub(crate) fn remove_alternate(parent: &SyntaxNode, alternate: &Alternate) -> SyntaxNode {
let token = &alternate.token;
let mut green = parent.green().remove_child(token.index());
if let Some(prev) = token.prev_sibling_or_token()
&& prev.kind() == SyntaxKind::TOKEN_WHITESPACE
{
green = green.remove_child(prev.index());
}
SyntaxNode::new_root(parent.replace_with(green))
}
pub(crate) fn flip_remove(
parent: &SyntaxNode,
binding: &SyntaxNode,
alternate: &Alternate,
) -> SyntaxNode {
let activated = parse_node(uncomment(&alternate.token.to_string()));
let element: rnix::SyntaxElement = binding.clone().into();
let mut to_remove: Vec<usize> = trailing_inline_comments(&element)
.iter()
.map(|t| t.index())
.collect();
to_remove.push(binding.index());
if let Some(prev) = binding.prev_sibling_or_token()
&& prev.kind() == SyntaxKind::TOKEN_WHITESPACE
{
to_remove.push(prev.index());
}
to_remove.sort_unstable();
let mut green = parent
.green()
.replace_child(alternate.token.index(), activated.green().into());
for index in to_remove.iter().rev() {
green = green.remove_child(*index);
}
SyntaxNode::new_root(parent.replace_with(green))
}
pub(crate) fn synthesize(parent: &SyntaxNode, binding: &SyntaxNode, uri: &str) -> SyntaxNode {
let attrpath_text = binding
.children()
.find(|c| c.kind() == SyntaxKind::NODE_ATTRPATH)
.map(|c| c.to_string())
.unwrap_or_default();
let indent = binding
.prev_sibling_or_token()
.filter(|t| t.kind() == SyntaxKind::TOKEN_WHITESPACE)
.map(|t| extract_indent(&t.to_string()).to_string())
.unwrap_or_default();
let (comment_text, tail) = deactivated_line(binding);
let mut green = parent
.green()
.replace_child(binding.index(), parse_node(&comment_text).green().into());
for index in tail.iter().rev() {
green = green.remove_child(*index);
}
let new_line = parse_node(&format!("{attrpath_text} = \"{uri}\";"));
green = green.insert_child(
binding.index() + 1,
parse_node(&format!("\n{indent}")).green().into(),
);
green = green.insert_child(binding.index() + 2, new_line.green().into());
SyntaxNode::new_root(parent.replace_with(green))
}