use std::{path::PathBuf, fs, process::exit}; use pipeline::{Change, ChangeKind}; use rnix::SyntaxNode; use rowan::{ast::AstNode, TextSize, NodeOrToken, TextRange}; use clap::{arg, Parser}; #[allow(dead_code)] mod queries; #[allow(dead_code)] mod status_reporter; mod batchmode; #[allow(dead_code)] mod util; #[allow(dead_code)] mod pipeline; #[derive(clap::Parser)] struct Args { query: String, path: Vec, #[arg(short, long)] debug: bool, #[arg(long)] batchmode: bool } fn parse_nexp(path: &PathBuf) -> anyhow::Result<(String, rnix::Root)> { let content = fs::read_to_string(path)?; let parse = rnix::Root::parse(&content); if !parse.errors().is_empty() { anyhow::bail!("error: {:?}", parse.errors()); } Ok((content, parse.tree())) } fn main() { let args = Args::parse(); let parse = queries::parse(&args.query); if parse.errors.len() != 0 { eprintln!( "syntax {}: \n {}", if parse.errors.len() == 1 { "error" } else { "errors" }, parse.errors.join(" \n") ); exit(1); } // println!("{nexp:#?}"); if args.batchmode { batchmode::batchmode(args.path, parse, args.debug); } else { let (content, nexp) = match parse_nexp(&args.path[0]) { Err(e) => { eprintln!("could not parse file: {e}"); exit(2); }, Ok(exp) => exp }; let (changes, results) = parse.apply(&content, nexp.syntax().clone()).unwrap(); if args.debug { println!("{changes:?}"); } if changes.len() == 0 { for result in results { println!("{result}"); } } else { let changed = apply_changes(&content, changes, args.debug); println!("{changed}"); } } } fn apply_changes(content: &str, mut changes: Vec, debug: bool) -> String { let mut last_pos = 0; let mut ncontent = String::new(); changes.sort_by_key(|change| change.node.text_range().start()); for change in changes { match change.kind { ChangeKind::Remove => { let (before, after) = remove_node(&change.node); if last_pos > before { continue } ncontent += &content[last_pos..before]; if debug { println!("removing: {}", &content[before..after]) } last_pos = after; } ChangeKind::Keep => { // let (before, after) = surrounding_whitespace(&change.node); let (before, after) = (change.node.text_range().start().into(), change.node.text_range().end().into()); if debug { println!("keeping: {}", &content[before..after]) }; ncontent += &content[before..after]; // last_pos = after; } } } ncontent += &content[last_pos..]; ncontent } // TODO: does rnix really not have an alias for this? type NixNodeOrToken = NodeOrToken, rowan::SyntaxToken>; /// eat all tokens of the given kind (backwards or forwards) fn eat_kind(node: &NixNodeOrToken, kind: rnix::SyntaxKind, backwards: bool) -> NixNodeOrToken { let mut span = node.clone(); loop { span = match if backwards { span.prev_sibling_or_token() } else { span.next_sibling_or_token() } { Some(NodeOrToken::Token(t)) if t.kind() == kind => NodeOrToken::Token(t), _ => break }; } span } /// eat whitespace outwards both ways fn eat_whitespace(begin: &NixNodeOrToken, end: &NixNodeOrToken) -> (NixNodeOrToken, NixNodeOrToken) { let begin = eat_kind(begin, rnix::SyntaxKind::TOKEN_WHITESPACE, true); let end = eat_kind(end, rnix::SyntaxKind::TOKEN_WHITESPACE, false); (begin, end) } /// for a node, get all the "syntactic noise" which surrounds it /// (i.e. brackets, whitespace, etc.) which should be removed if /// the node itself is removed, except the final bit of whitespace /// potentially separating it from neighbouring nodes, which should /// be handled separately to keep formatting nice fn surrounding_noise(node: &rnix::SyntaxNode) -> (NixNodeOrToken, NixNodeOrToken) { fn eat_matching_parens(begin: &NixNodeOrToken, end: &NixNodeOrToken) -> Option<(NixNodeOrToken, NixNodeOrToken)> { match (begin.prev_sibling_or_token(), end.next_sibling_or_token()) { (Some(begin), Some(end)) if begin.kind() == rnix::SyntaxKind::TOKEN_L_PAREN && end.kind() == rnix::SyntaxKind::TOKEN_R_PAREN => Some((begin, end)), _ => None } } let (mut begin, mut end): (NixNodeOrToken, NixNodeOrToken) = (NodeOrToken::Node(node.clone()), NodeOrToken::Node(node.clone())); loop { let (a, b) = eat_whitespace(&begin, &end); if let Some(res) = eat_matching_parens(&a, &b) { (begin, end) = res; } else { break } } (begin, end) } fn remove_node(node: &rnix::SyntaxNode) -> (usize, usize) { let (before, after) = surrounding_noise(node); let (prev, next) = eat_whitespace(&before, &after); if prev.kind() == rnix::SyntaxKind::TOKEN_WHITESPACE && next.kind() == rnix::SyntaxKind::TOKEN_WHITESPACE { if prev.to_string().lines().count() < next.to_string().lines().count() { (Into::::into(node.text_range().start() - prev.text_range().len()) + previous_indentation(&node).unwrap_or(0) , Into::::into(node.text_range().end()) + next_indentation(&node).unwrap_or(0)) } else { (Into::::into(node.text_range().start()) - previous_indentation(&node).unwrap_or(0), Into::::into(node.text_range().end() + next.text_range().len()) - next_indentation(&node).unwrap_or(0)) } } else { (prev.text_range().start().into(), next.text_range().end().into()) } } fn previous_indentation(node: &rnix::SyntaxNode) -> Option { let whitespace_token = node.prev_sibling_or_token()?; if whitespace_token.kind() == rnix::SyntaxKind::TOKEN_WHITESPACE { Some(whitespace_token.to_string().lines().last().unwrap().len()) } else { None } } fn next_indentation(node: &rnix::SyntaxNode) -> Option { let whitespace_token = node.next_sibling_or_token()?; if whitespace_token.kind() == rnix::SyntaxKind::TOKEN_WHITESPACE { Some(whitespace_token.to_string().lines().last().unwrap().len()) } else { None } }