use rnix::{Root, SyntaxKind, SyntaxNode};
use crate::change::Change;
use crate::follows::{AttrPath, Segment};
use super::context::Context;
pub(crate) type Node = SyntaxNode;
pub(crate) fn parse_node(s: &str) -> Node {
Root::parse(s).syntax()
}
pub(crate) fn substitute_child(parent: &SyntaxNode, index: usize, new_child: &SyntaxNode) -> Node {
let green = parent
.green()
.replace_child(index, new_child.green().into());
SyntaxNode::new_root(green)
}
pub(crate) fn empty_node() -> Node {
Root::parse("").syntax()
}
pub(crate) fn is_attrset_content_empty(node: &SyntaxNode) -> bool {
let attr_set = if node.kind() == SyntaxKind::NODE_ROOT {
match node.first_child() {
Some(inner) => inner,
None => return false,
}
} else {
node.clone()
};
if attr_set.kind() != SyntaxKind::NODE_ATTR_SET {
return false;
}
if attr_set
.children()
.any(|c| c.kind() == SyntaxKind::NODE_ATTRPATH_VALUE)
{
return false;
}
!attr_set
.children_with_tokens()
.any(|t| t.kind() == SyntaxKind::TOKEN_COMMENT)
}
pub(crate) fn get_sibling_whitespace(node: &SyntaxNode) -> Option<Node> {
if let Some(prev) = node.prev_sibling_or_token()
&& prev.kind() == SyntaxKind::TOKEN_WHITESPACE
{
return Some(parse_node(prev.as_token().unwrap().green().text()));
}
if let Some(next) = node.next_sibling_or_token()
&& next.kind() == SyntaxKind::TOKEN_WHITESPACE
{
return Some(parse_node(next.as_token().unwrap().green().text()));
}
None
}
pub(crate) fn insertion_index_after(node: &SyntaxNode) -> usize {
let element: rnix::SyntaxElement = node.clone().into();
let mut cursor = element.next_sibling_or_token();
let mut last_index = node.index() + 1;
while let Some(ref tok) = cursor {
match tok.kind() {
SyntaxKind::TOKEN_WHITESPACE => {
let text = tok.to_string();
if text.contains('\n') {
break;
}
last_index = tok.index() + 1;
}
SyntaxKind::TOKEN_COMMENT => {
last_index = tok.index() + 1;
}
_ => break,
}
cursor = tok.next_sibling_or_token();
}
last_index
}
pub(crate) fn trailing_inline_comment_indices(child: &rnix::SyntaxElement) -> Vec<usize> {
let mut pending = Vec::new();
let mut cursor = child.next_sibling_or_token();
while let Some(tok) = cursor {
match tok.kind() {
SyntaxKind::TOKEN_WHITESPACE => {
if tok.to_string().contains('\n') {
break;
}
pending.push(tok.index());
}
SyntaxKind::TOKEN_COMMENT => {
pending.push(tok.index());
return pending;
}
_ => break,
}
cursor = tok.next_sibling_or_token();
}
Vec::new()
}
pub(crate) fn remove_child_with_whitespace(
parent: &SyntaxNode,
node: &SyntaxNode,
index: usize,
) -> SyntaxNode {
let element: rnix::SyntaxElement = node.clone().into();
let mut to_remove = vec![index];
to_remove.extend(trailing_inline_comment_indices(&element));
if let Some(ws_index) = adjacent_whitespace_index(&element) {
to_remove.push(ws_index);
}
to_remove.sort_unstable();
let mut green = parent.green().into_owned();
for idx in to_remove.into_iter().rev() {
green = green.remove_child(idx);
}
SyntaxNode::new_root(green)
}
pub(crate) fn uses_attrset_style(parent: &SyntaxNode) -> bool {
let mut attrset_count = 0usize;
let mut flat_url_count = 0usize;
for child in parent.children() {
if child.kind() != SyntaxKind::NODE_ATTRPATH_VALUE {
continue;
}
if child
.children()
.any(|c| c.kind() == SyntaxKind::NODE_ATTR_SET)
{
attrset_count += 1;
continue;
}
if let Some(attrpath) = child
.children()
.find(|c| c.kind() == SyntaxKind::NODE_ATTRPATH)
{
let idents: Vec<_> = attrpath.children().collect();
if idents.len() >= 2
&& idents
.last()
.map(|i| i.to_string() == "url")
.unwrap_or(false)
{
flat_url_count += 1;
}
}
}
attrset_count > flat_url_count
}
pub(crate) fn extract_indent(ws_str: &str) -> &str {
if let Some(last_nl) = ws_str.rfind('\n') {
&ws_str[last_nl + 1..]
} else {
ws_str
}
}
pub(crate) fn last_line_with_newline(ws_str: &str) -> &str {
if let Some(last_nl) = ws_str.rfind('\n') {
&ws_str[last_nl..]
} else {
ws_str
}
}
pub(crate) fn adjacent_whitespace_index(child: &rnix::SyntaxElement) -> Option<usize> {
if let Some(prev) = child.prev_sibling_or_token()
&& prev.kind() == SyntaxKind::TOKEN_WHITESPACE
{
Some(prev.index())
} else if let Some(next) = child.next_sibling_or_token()
&& next.kind() == SyntaxKind::TOKEN_WHITESPACE
{
Some(next.index())
} else {
None
}
}
pub(crate) fn should_remove_input(
change: &Change,
ctx: &Option<Context>,
input_id: &Segment,
) -> bool {
if !change.is_remove() {
return false;
}
if let Some(id) = change.id()
&& id.input() == input_id
&& id.follows().is_none()
{
return true;
}
if let Some(ctx) = ctx
&& ctx.first_matches(input_id)
{
return true;
}
false
}
pub(crate) fn should_remove_nested_input(
change: &Change,
ctx: &Option<Context>,
input_id: &Segment,
) -> bool {
if !change.is_remove() {
return false;
}
if let Some(id) = change.id() {
return id.matches_with_ctx(input_id, ctx.clone());
}
false
}
pub(crate) fn make_quoted_string(s: &str) -> Node {
parse_node(&format!("\"{}\"", s))
}
pub(crate) fn make_toplevel_url_attr(id: &str, uri: &str) -> Node {
parse_node(&format!("inputs.{}.url = \"{}\";", id, uri))
}
pub(crate) fn make_toplevel_flake_false_attr(id: &str) -> Node {
parse_node(&format!("inputs.{}.flake = false;", id))
}
pub(crate) fn make_url_attr(id: &str, uri: &str) -> Node {
parse_node(&format!("{}.url = \"{}\";", id, uri))
}
pub(crate) fn make_flake_false_attr(id: &str) -> Node {
parse_node(&format!("{}.flake = false;", id))
}
pub(crate) fn make_attrset_url_attr(id: &str, uri: &str, indent: &str) -> Node {
parse_node(&format!(
"{} = {{\n{} url = \"{}\";\n{}}};",
id, indent, uri, indent
))
}
pub(crate) fn make_attrset_url_flake_false_attr(id: &str, uri: &str, indent: &str) -> Node {
parse_node(&format!(
"{} = {{\n{} url = \"{}\";\n{} flake = false;\n{}}};",
id, indent, uri, indent, indent
))
}
pub(crate) enum FollowsKind<'a> {
TopLevelFlat { id: &'a Segment, target: &'a str },
TopLevelNested { path: &'a AttrPath, target: &'a str },
InputsBlockNested { path: &'a AttrPath, target: &'a str },
BlockNested {
rest: &'a [Segment],
target: &'a str,
},
BlockBare { target: &'a str },
}
fn render_inputs_chain(segments: &[Segment]) -> String {
let mut out = String::new();
for (i, seg) in segments.iter().enumerate() {
if i > 0 {
out.push_str(".inputs.");
}
out.push_str(&seg.render());
}
out
}
impl FollowsKind<'_> {
pub(crate) fn emit(&self) -> Node {
match self {
FollowsKind::TopLevelFlat { id, target } => {
parse_node(&format!("inputs.{}.follows = \"{}\";", id.render(), target))
}
FollowsKind::TopLevelNested { path, target } => {
let chain = render_inputs_chain(path.segments());
parse_node(&format!("inputs.{chain}.follows = \"{target}\";"))
}
FollowsKind::InputsBlockNested { path, target } => {
let chain = render_inputs_chain(path.segments());
parse_node(&format!("{chain}.follows = \"{target}\";"))
}
FollowsKind::BlockNested { rest, target } => {
let chain = render_inputs_chain(rest);
parse_node(&format!("inputs.{chain}.follows = \"{target}\";"))
}
FollowsKind::BlockBare { target } => parse_node(&format!("follows = \"{}\";", target)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn seg(s: &str) -> Segment {
Segment::from_unquoted(s).expect("valid segment")
}
#[test]
fn follows_kind_top_level_flat_bare_ident() {
let id = seg("nixpkgs");
let node = FollowsKind::TopLevelFlat {
id: &id,
target: "github:NixOS/nixpkgs",
}
.emit();
assert_eq!(
node.to_string(),
"inputs.nixpkgs.follows = \"github:NixOS/nixpkgs\";"
);
}
#[test]
fn follows_kind_top_level_flat_quotes_dotted_segment() {
let id = seg("hls-1.10");
let node = FollowsKind::TopLevelFlat {
id: &id,
target: "nixpkgs",
}
.emit();
assert_eq!(
node.to_string(),
"inputs.\"hls-1.10\".follows = \"nixpkgs\";"
);
}
fn path(s: &str) -> AttrPath {
AttrPath::parse(s).expect("valid attrpath")
}
#[test]
fn follows_kind_top_level_nested() {
let p = path("crane.nixpkgs");
let node = FollowsKind::TopLevelNested {
path: &p,
target: "nixpkgs",
}
.emit();
assert_eq!(
node.to_string(),
"inputs.crane.inputs.nixpkgs.follows = \"nixpkgs\";"
);
}
#[test]
fn follows_kind_top_level_nested_quotes_dotted_parent() {
let p = path("\"hls-1.10\".nixpkgs");
let node = FollowsKind::TopLevelNested {
path: &p,
target: "nixpkgs",
}
.emit();
assert_eq!(
node.to_string(),
"inputs.\"hls-1.10\".inputs.nixpkgs.follows = \"nixpkgs\";"
);
}
#[test]
fn follows_kind_top_level_nested_depth_three() {
let p = path("neovim.nixvim.flake-parts");
let node = FollowsKind::TopLevelNested {
path: &p,
target: "flake-parts",
}
.emit();
assert_eq!(
node.to_string(),
"inputs.neovim.inputs.nixvim.inputs.flake-parts.follows = \"flake-parts\";"
);
}
#[test]
fn follows_kind_inputs_block_nested() {
let p = path("harmonia.nixpkgs");
let node = FollowsKind::InputsBlockNested {
path: &p,
target: "nixpkgs",
}
.emit();
assert_eq!(
node.to_string(),
"harmonia.inputs.nixpkgs.follows = \"nixpkgs\";"
);
}
#[test]
fn follows_kind_inputs_block_nested_depth_three() {
let p = path("neovim.nixvim.flake-parts");
let node = FollowsKind::InputsBlockNested {
path: &p,
target: "flake-parts",
}
.emit();
assert_eq!(
node.to_string(),
"neovim.inputs.nixvim.inputs.flake-parts.follows = \"flake-parts\";"
);
}
#[test]
fn follows_kind_block_nested() {
let rest = [seg("nixpkgs")];
let node = FollowsKind::BlockNested {
rest: &rest,
target: "nixpkgs",
}
.emit();
assert_eq!(node.to_string(), "inputs.nixpkgs.follows = \"nixpkgs\";");
}
#[test]
fn follows_kind_block_nested_quotes_dotted() {
let rest = [seg("hls-1.10")];
let node = FollowsKind::BlockNested {
rest: &rest,
target: "nixpkgs",
}
.emit();
assert_eq!(
node.to_string(),
"inputs.\"hls-1.10\".follows = \"nixpkgs\";"
);
}
#[test]
fn follows_kind_block_nested_depth_two() {
let rest = [seg("nixvim"), seg("flake-parts")];
let node = FollowsKind::BlockNested {
rest: &rest,
target: "flake-parts",
}
.emit();
assert_eq!(
node.to_string(),
"inputs.nixvim.inputs.flake-parts.follows = \"flake-parts\";"
);
}
#[test]
fn follows_kind_block_bare() {
let node = FollowsKind::BlockBare { target: "nixpkgs" }.emit();
assert_eq!(node.to_string(), "follows = \"nixpkgs\";");
}
#[test]
fn remove_child_strips_trailing_whitespace_only() {
let root = parse_node("{a = 1;\n b = 2;}");
let attr_set = root.first_child().expect("attr set");
let a = attr_set
.children()
.find(|c| c.to_string().starts_with("a ="))
.expect("`a` binding present");
let result = remove_child_with_whitespace(&attr_set, &a, a.index());
assert_eq!(result.to_string(), "{b = 2;}");
}
}