use std::path::PathBuf; use rowan::NodeOrToken; use crate::pipeline::Change; pub 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 { use crate::queries::Operator::*; match change.kind { 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; } Keep => { let (before, after) = surrounding_noise(&change.node); let (before, after) = eat_whitespace(&before, &after); // let (before, after) = (change.node.text_range().start().into(), change.node.text_range().end().into()); let (before, after) = (before.text_range().start().into(), after.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); // println!("loop: {begin}, {end}"); // println!("loop: {a}, {b}"); // println!("{:?}", (begin.prev_sibling_or_token(), end.next_sibling_or_token())); if let Some(res) = eat_matching_parens(&a, &b) { // println!("ate parens!"); (begin, end) = res; } else { break } } (begin, end) } fn remove_node(node: &rnix::SyntaxNode) -> (usize, usize) { let (before, after) = surrounding_noise(node); let (mut prev, /*mut*/ next) = eat_whitespace(&before, &after); // println!("{prev:?}, {next:?}"); if let Some(t) = prev.as_token().map(|t| t.prev_token()).flatten() { if t.kind() == rnix::SyntaxKind::TOKEN_WHITESPACE { prev = NodeOrToken::Token(t); } }; // if let Some(t) = next.as_token().map(|t| t.next_token()).flatten() { // if t.kind() == rnix::SyntaxKind::TOKEN_WHITESPACE { // next = NodeOrToken::Token(t); // } // }; // println!("{prev:?}, {next:?}"); if prev.kind() == rnix::SyntaxKind::TOKEN_WHITESPACE && next.kind() == rnix::SyntaxKind::TOKEN_WHITESPACE { // println!("inside if!"); if prev.to_string().lines().count() < next.to_string().lines().count() { let start: usize = (node.text_range().start() - prev.text_range().len()).into(); let end: usize = node.text_range().end().into(); (start + previous_indentation(&node).unwrap_or(0) , end + next_indentation(&node).unwrap_or(0)) } else { let start: usize = node.text_range().start().into(); let end: usize = (node.text_range().end() + next.text_range().len()).into(); (start, end) // (start - previous_indentation(&node).unwrap_or(0), // end - next_indentation(&node).unwrap_or(0)) } } else { (prev.text_range().start().into(), next.text_range().end().into()) } } fn indentation_of_string(string: &str) -> Option { if string.contains("\n") { None } else { Some(string.lines().last().unwrap().len()) } } fn previous_indentation(node: &rnix::SyntaxNode) -> Option { let whitespace_token = node.prev_sibling_or_token()?; if whitespace_token.kind() == rnix::SyntaxKind::TOKEN_WHITESPACE { indentation_of_string(&whitespace_token.to_string()) } 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 { indentation_of_string(&whitespace_token.to_string()) // Some(whitespace_token.to_string().lines().last().unwrap().len()) } else { None } } /// give line & column of a position marker in the given string fn line_at_textpos(content: &str, pos: rowan::TextSize) -> Option<(usize, usize)> { let pos: usize = pos.into(); content .split("\n") .scan(0usize, |acc,line| { let start = *acc; let end = line.len() + 1 + start; *acc = end; Some((start, end)) }) .enumerate() .find(|(_, (_, end))| *end > pos) .map(|(line, (start, _))| (line + 1, pos + 1 - start)) } pub fn format_pos(path: &PathBuf, content: &str, pos1: rowan::TextSize) -> String { let (line1, col1) = line_at_textpos(content, pos1).unwrap(); format!("{path:?}:{line1}:{col1}") } pub fn format_range(path: &PathBuf, content: &str, range: rowan::TextRange) -> String { let (line1, col1) = line_at_textpos(content, range.start()).unwrap(); let (line2, col2) = line_at_textpos(content, range.end()).unwrap(); format!("{}:{line1}:{col1}-{line2}:{col2}", path.to_string_lossy()) } pub fn format_range_json(path: &PathBuf, content: &str, range: rowan::TextRange) -> serde_json::Value { let (line1, col1) = line_at_textpos(content, range.start()).unwrap(); let (line2, col2) = line_at_textpos(content, range.end()).unwrap(); serde_json::json!({ "file": path, "start": { "line": line1, "column": col1 }, "end": { "line": line2, "column": col2 } }) }