use std::{path::Path, fs}; use anyhow::{Result, Context}; use itertools::Itertools; use rnix::{SyntaxKind, ast::{AttrpathValue, AttrSet, HasEntry, Entry::*}, SyntaxNode}; use rowan::{ast::AstNode, TextSize}; use crate::status_reporter::StatusReport; fn textsize_at_line(s: &str, line: usize) -> TextSize { s .split('\n') .map(|l| TextSize::of(l) + TextSize::new(1) ) .take(line-1) .sum() } fn dig_to_kind(kind: SyntaxKind, node: &SyntaxNode) -> Option { if node.kind() == kind { return Some(node.clone()); } node.descendants() .filter(|node| node.kind() == kind) .next() } fn add_to_meta_block(rough_pos: SyntaxNode, content: &str, main_program: &str, file: &Path, line: usize) -> Result<(String, usize)> { let meta_node = dig_to_kind(SyntaxKind::NODE_ATTR_SET, &rough_pos).unwrap(); let meta_set = AttrSet::cast(meta_node.clone()).unwrap(); let description_entry = meta_set.entries() .filter(|entry| { match &entry { Inherit(it) => it.attrs().any(|c| c.to_string() == "description"), AttrpathValue(it) => it.attrpath().unwrap().to_string() == "description", } }) .exactly_one().ok() .with_context(|| format!("meta node has no description attribute in {:?} at {}", file, line))?; let description = description_entry.syntax(); let pos = description.text_range(); let indent = content[..pos.start().into()].chars().rev().position(|c| c == '\n').unwrap(); let patch = String::new() + "\n" + &" ".repeat(indent) + "mainProgram = \"" + main_program + "\";"; Ok((patch, pos.end().into())) } fn edit_one(file: &Path, line: usize, main_program: &str, p: &StatusReport) -> Result { let mut content = fs::read_to_string(file)?; let searchpos = textsize_at_line(&content, line); p.update_item(format!("doing {:?}", file)); let parse = rnix::Root::parse(&content); if !parse.errors().is_empty() { anyhow::bail!("error: {:?}", parse.errors()); } let tree = parse.tree(); let pos_node = tree.syntax().descendants() .filter(|node| { if node.kind() == SyntaxKind::NODE_ATTRPATH_VALUE { let value = AttrpathValue::cast(node.clone()).unwrap(); node.text_range().contains(searchpos) && value.attrpath().unwrap().to_string() == "meta" } else { false } }) .exactly_one().ok(); // do we have a meta attrset already? let (patch, insert_offset) = match pos_node { None => { let version_node = tree .syntax() .descendants() .filter(|node| { if node.kind() == SyntaxKind::NODE_ATTRPATH_VALUE { let value = AttrpathValue::cast(node.clone()).unwrap(); let name = value.attrpath().unwrap().to_string(); node.text_range().contains(searchpos + TextSize::new(5)) && (name == "version" || name == "pname" || name == "name") } else { false } }) .exactly_one().ok() .with_context(|| format!("neither meta nor version node found for {:?} at {}", file, line))?; let attrset = version_node.parent().unwrap(); if attrset.kind() != SyntaxKind::NODE_ATTR_SET { anyhow::bail!("name not in an attrset in {:?} at {}", file, line) } // does a meta block already exist? let maybe_meta_block = attrset .descendants() .filter(|node| { if node.kind() == SyntaxKind::NODE_ATTRPATH_VALUE { let value = AttrpathValue::cast(node.clone()).unwrap(); let name = value.attrpath().unwrap().to_string(); name == "meta" } else { false } }) .exactly_one(); if let Ok(meta) = maybe_meta_block { add_to_meta_block(meta.clone(), &content, main_program, file, line)? } else { let before_attrset_end = Into::::into(attrset.text_range().end()) - 1 - content[..attrset.text_range().end().into()] .chars().rev().position(|c| c == '\n').unwrap(); let indent = content[..version_node.text_range().start().into()] .chars().rev().position(|c| c == '\n').unwrap(); // some language specific build systems don't use meta as its own attrset // there's no good way to recognise these, but this seems to work fine let weird_nonstandard_meta = attrset .descendants() .any(|node| { if node.kind() == SyntaxKind::NODE_ATTRPATH_VALUE { let value = AttrpathValue::cast(node.clone()).unwrap(); let name = value.attrpath().unwrap().to_string(); name == "description" || name == "homepage" || name == "license" } else { false } }); let patch = String::new() + "\n" + &" ".repeat(indent) + if weird_nonstandard_meta { "mainProgram = \"" } else { "meta.mainProgram = \"" } + main_program + "\";"; (patch, before_attrset_end) } }, Some(pos) => { add_to_meta_block(pos.clone(), &content, main_program, file, line)? } }; content = String::new() + &content[..insert_offset] + &patch + &content[insert_offset..]; p.changed_item(); Ok(content) }