From 422dbd5209fddac75412d8e7de0137f732c7dbc4 Mon Sep 17 00:00:00 2001 From: stuebinm Date: Fri, 12 Apr 2024 16:17:06 +0200 Subject: some restructuring, arg handling, re-enable batchmode --- src/batchmode.rs | 38 ++++++--- src/main.rs | 206 ++++++++++++++++++++++++++++++++++-------------- src/pipeline.rs | 203 ++++++++++++++++++++++++++++++++++++++++++++++++ src/queries.rs | 233 ++++++++++--------------------------------------------- 4 files changed, 419 insertions(+), 261 deletions(-) create mode 100644 src/pipeline.rs (limited to 'src') diff --git a/src/batchmode.rs b/src/batchmode.rs index 8398bce..84b0dff 100644 --- a/src/batchmode.rs +++ b/src/batchmode.rs @@ -1,30 +1,43 @@ use std::{path::PathBuf, fs, sync::{Arc, Mutex}}; +use rowan::ast::AstNode; use threadpool::ThreadPool; -use crate::status_reporter::*; +use crate::{status_reporter::*, queries::{SyntaxNode, Parse}, parse_nexp, apply_changes}; -// TODO: make this usable -// (this module just here to keep old code around for a bit) -pub enum Task {} #[allow(unreachable_code, unused)] -pub fn batchmode(tasks: Vec<(PathBuf, Task)>) { +pub fn batchmode(tasks: Vec, query: Parse, debug: bool) { + fn do_task(path: PathBuf, query: Parse, debug: bool) -> anyhow::Result<(PathBuf, Option)> { + + let (content, nexp) = match parse_nexp(&path) { + Err(e) => { + anyhow::bail!("could not parse file {path:?}") + }, + Ok(exp) => exp + }; + + let (changes, _) = query.apply(&content, nexp.syntax().clone())?; + + let changed = apply_changes(&content, changes, debug); + + Ok((path, if changed != content { Some(changed) } else { None })) + } let pool = ThreadPool::new(16); let results = Arc::new(Mutex::new(vec![])); let printer = Arc::new(StatusReport::new(tasks.len(), tasks.len())); - for (path, task) in tasks { + for path in tasks { pool.execute({ let results = Arc::clone(&results); let printer = Arc::clone(&printer); + let query = query.clone(); move || { printer.enter_file(&format!("{path:?}")); - let result: anyhow::Result<(PathBuf, String)> = todo!(); - + let result = do_task(path, query, debug); results.lock().unwrap().push(result); } }); @@ -35,18 +48,20 @@ pub fn batchmode(tasks: Vec<(PathBuf, Task)>) { println!("\n\nSummary:"); let mut c_errors = 0; let mut c_total = 0; + let mut c_changes = 0; for r in results.lock().unwrap().iter() { match r { Err(e) => { println!(" {}", e); c_errors += 1; }, + Ok((_, Some(_))) => c_changes += 1, _ => () } c_total += 1; } - println!("\n ({c_total} sites total, {c_errors} errors, generated {} edits)", c_total - c_errors); + println!("\n ({c_total} sites total, {c_errors} errors, generated {} edits)", c_changes); let edits: Vec<_> = Arc::into_inner(results).unwrap().into_inner().unwrap() .into_iter() @@ -55,7 +70,8 @@ pub fn batchmode(tasks: Vec<(PathBuf, Task)>) { println!("applying changes ..."); for (filename, content) in edits { - fs::write(&filename, content.as_bytes()).unwrap(); - // println!("{}", content); + if let Some(content) = content { + fs::write(&filename, content.as_bytes()).unwrap(); + } } } diff --git a/src/main.rs b/src/main.rs index a09f4ef..2bc44ab 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ use std::{path::PathBuf, fs, process::exit}; -use rowan::{ast::AstNode, TextSize}; -use clap::{arg, command, value_parser}; +use pipeline::{Change, ChangeKind}; +use rowan::{ast::AstNode, TextSize, NodeOrToken}; +use clap::{arg, Parser}; #[allow(dead_code)] mod queries; @@ -9,6 +10,19 @@ 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)> { @@ -23,24 +37,9 @@ fn parse_nexp(path: &PathBuf) -> anyhow::Result<(String, rnix::Root)> { fn main() { - let matches = command!() - .arg(arg!(--batchmode "run in batch mode") - .required(false) - ) - .arg(arg!([query] "query to run") - .required(true)) - .arg(arg!([file] "file to operate on") - .required(true) - .value_parser(value_parser!(PathBuf)) - ) - .arg(arg!(--edit "what to do") - .required(false)) - .get_matches(); - - let query_string = matches.get_one::("query").unwrap(); - let files = matches.get_one::("file").unwrap(); - - let parse = queries::parse(query_string); + let args = Args::parse(); + + let parse = queries::parse(&args.query); if parse.errors.len() != 0 { eprintln!( "syntax {}: \n {}", @@ -50,61 +49,144 @@ fn main() { exit(1); } - let (content, nexp) = match parse_nexp(files) { - Err(e) => { - eprintln!("could not parse file: {e}"); - exit(2); - }, - Ok(exp) => exp - }; - // println!("{nexp:#?}"); - let results = parse.apply(&content, nexp.syntax().clone()).unwrap(); - + 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 let Some(op) = matches.get_one::("edit") { - match &op[..] { - "remove" => { - let new = remove_nodes(content, &results); - println!("{new}"); + if changes.len() == 0 { + for result in results { + println!("{result}"); } - _ => () + } else { + let changed = apply_changes(&content, changes, args.debug); + + println!("{changed}"); } - } else { - for result in &results { - println!("{result}"); + } +} + + +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 } -fn remove_nodes(content: String, results: &Vec) -> String { - - assert!(results.len() == 1); +#[allow(unused)] +fn surrounding_whitespace(node: &rnix::SyntaxNode) -> (usize, usize) { + let before = node + .prev_sibling_or_token() + .filter(|n| n.kind() == rnix::SyntaxKind::TOKEN_WHITESPACE) + .map(|n| n.text_range().start().into()) + .unwrap_or_else(|| node.text_range().start().into()); + let after = node + .next_sibling_or_token() + .filter(|n| n.kind() == rnix::SyntaxKind::TOKEN_WHITESPACE) + .map(|n| n.text_range().end().into()) + .unwrap_or_else(|| node.text_range().end().into()); + + (before, after) +} - let span = &results[0]; - let (before, after) = match (span.prev_sibling_or_token(), span.next_sibling_or_token()) { - (Some(prev), Some(next)) - if prev.kind() == rnix::SyntaxKind::TOKEN_WHITESPACE - && next.kind() == rnix::SyntaxKind::TOKEN_WHITESPACE - => { - if prev.to_string().lines().count() < next.to_string().lines().count() { - (prev.text_range().len(), TextSize::new(0)) - } else { - (TextSize::new(0), next.text_range().len()) +fn remove_node(span: &rnix::SyntaxNode) -> (usize, usize) { + + // TODO: how to remove brackets around the node? + + fn eat_to_bracket(node: &rnix::SyntaxNode, backwards: bool) -> (TextSize, usize) { + let mut span = NodeOrToken::Node(node.clone()); + let mut bracket = span.clone(); + let mut brackets = 0; + loop { + match if backwards { span.prev_sibling_or_token() } else { span.next_sibling_or_token() } { + Some(NodeOrToken::Token(t)) => match t.kind() { + rnix::SyntaxKind::TOKEN_WHITESPACE => span = NodeOrToken::Token(t), + rnix::SyntaxKind::TOKEN_L_PAREN if backwards => { + span = NodeOrToken::Token(t); + bracket = span.clone(); + brackets += 1; + }, + rnix::SyntaxKind::TOKEN_R_PAREN if !backwards => { + span = NodeOrToken::Token(t); + bracket = span.clone(); + brackets += 1; + }, + _ => break, + }, + _ => break, } } - _ => (TextSize::default(),TextSize::default()) + (if backwards { bracket.text_range().start() } else { bracket.text_range().end() }, brackets) + } + + let (before, after) = match (eat_to_bracket(span, true), eat_to_bracket(span, false)) { + ((before, n1), (after, n2)) if n1 == n2 && n1 != 0 => (before.into(), after.into()), + _ => match (span.prev_sibling_or_token(), span.next_sibling_or_token()) { + (Some(prev), Some(next)) + 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(span.text_range().start() - prev.text_range().len()) + previous_indentation(&span).unwrap_or(0) + , Into::::into(span.text_range().end()) + next_indentation(&span).unwrap_or(0)) + } else { + (Into::::into(span.text_range().start()) - previous_indentation(&span).unwrap_or(0), + Into::::into(span.text_range().end() + next.text_range().len()) - next_indentation(&span).unwrap_or(0)) + } + } + _ => (span.text_range().start().into(), span.text_range().end().into()) + } }; - String::new() - + &content[..Into::::into(span.text_range().start() - before) - previous_indentation(span).unwrap_or(0)] - + &content[(span.text_range().end() + after).into()..] -} + (before, after) + // ( Into::::into(span.text_range().start()) - before + // , Into::::into(span.text_range().end()) + after + // ) +} fn previous_indentation(node: &rnix::SyntaxNode) -> Option { @@ -116,3 +198,13 @@ fn previous_indentation(node: &rnix::SyntaxNode) -> Option { 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 + } +} diff --git a/src/pipeline.rs b/src/pipeline.rs new file mode 100644 index 0000000..cba046f --- /dev/null +++ b/src/pipeline.rs @@ -0,0 +1,203 @@ + +use itertools::Itertools; +use rnix::{match_ast, ast}; +use rowan::ast::AstNode; +use crate::queries::*; +use crate::queries::SyntaxKind::*; + +#[derive(Debug)] +pub struct Change { + pub node: rnix::SyntaxNode, + pub kind: ChangeKind +} + +#[derive(Copy, Clone, Debug)] +pub enum ChangeKind { + Remove, + Keep +} + +type NixExprs = Box>; +type Pipe = (Vec, NixExprs); + +macro_rules! ast_node { + ($ast:ident, $kind:ident) => { + #[derive(PartialEq, Eq, Hash)] + #[repr(transparent)] + struct $ast(SyntaxNode); + impl $ast { + #[allow(unused)] + fn cast(node: SyntaxNode) -> Option { + if node.kind() == $kind { + Some(Self(node)) + } else { + None + } + } + } + }; +} + +ast_node!(Root, ROOT); +ast_node!(Atom, ATOM); +ast_node!(List, LIST); + +#[derive(PartialEq, Eq, Hash, Debug)] +#[repr(transparent)] +struct Qexp(SyntaxNode); + +enum QexpKind { + Atom(Atom), + List(List), +} + +impl Qexp { + fn cast(node: SyntaxNode) -> Option { + if Atom::cast(node.clone()).is_some() || List::cast(node.clone()).is_some() { + Some(Qexp(node)) + } else { + None + } + } + + fn kind(&self) -> QexpKind { + Atom::cast(self.0.clone()) + .map(QexpKind::Atom) + .or_else(|| List::cast(self.0.clone()).map(QexpKind::List)) + .unwrap() + } + + fn apply(&self, _acc: Pipe) -> Pipe { + todo!() + } +} + +impl Root { + fn qexps(&self) -> impl Iterator + '_ { + self.0.children().filter_map(Qexp::cast) + } +} + +enum Op { + Down, + DownRecursive, + Up, + UpRecursive, + NixSyntaxNode(rnix::SyntaxKind), + Named(String) +} + +impl Atom { + fn eval(&self) -> Option { + self.text().parse().ok() + } + fn as_op(&self) -> Option { + let op = match self.text().as_str() { + ">" => Op::Down, + ">>" => Op::DownRecursive, + "<" => Op::Up, + "<<" => Op::UpRecursive, + "Inherit" => Op::NixSyntaxNode(rnix::SyntaxKind::NODE_INHERIT), + "String" => Op::NixSyntaxNode(rnix::SyntaxKind::NODE_STRING), + // TODO other syntax nodes + name => Op::Named(name.to_owned()), + }; + Some(op) + } + fn as_change(&self) -> Option { + let change = match self.text().as_str() { + "remove" => ChangeKind::Remove, + "keep" => ChangeKind::Keep, + _ => return None + }; + Some(change) + } + fn iter_args(&self) -> impl Iterator { + self.0.children().find_map(List::cast).into_iter().map(|arglist| arglist.iter()).flatten() + } + fn text(&self) -> String { + match self.0.green().children().next() { + Some(rowan::NodeOrToken::Token(token)) => token.text().to_string(), + _ => unreachable!(), + } + } + fn apply(&self, (mut changes, acc): Pipe) -> Pipe { + let mut acc: NixExprs = match self.as_op() { + Some(Op::Down) => Box::new(acc.map(|s| s.children()).flatten()), + Some(Op::DownRecursive) => Box::new(acc.map(|s| s.descendants()).flatten()), + Some(Op::Up) => Box::new(acc.filter_map(|s| s.parent())), + Some(Op::UpRecursive) => Box::new(acc.map(|s| s.ancestors()).flatten()), + // TODO: how to select roles relative to previous node? + Some(Op::NixSyntaxNode(kind)) => Box::new(acc.filter(move |s| s.kind() == kind)), + Some(Op::Named(name)) => + Box::new(acc + .filter(move |node| match_ast! { match node { + ast::AttrpathValue(value) => { + name == value.attrpath().unwrap().to_string() + }, + ast::Apply(value) => { + // TODO: special case lambda = NODE_SELECT here? + name == value.lambda().unwrap().to_string() + }, + // TODO: this is difficult — I want to use free-form names + // to select things below, too, but that might not always be + // possible. perhaps it is possible to skip over descendants? + ast::Ident(value) => { + name == value.to_string() + }, + _ => false + }})), + _ => todo!() + }; + + if let Ok(arg) = self.iter_args().exactly_one() { + if let Some(change) = arg.as_change() { + let (mut nchanges, nacc): (Vec<_>, Vec<_>) = acc + .map(|node| (Change { node: node.clone(), kind: change }, node)) + .unzip(); + acc = Box::new(nacc.into_iter()); + changes.append(&mut nchanges); + } + } + + (changes, acc) + } +} + +impl List { + fn sexps(&self) -> impl Iterator + '_ { + self.0.children().filter_map(Qexp::cast) + } + + fn iter(&self) -> impl Iterator { + self.0.children().filter_map(Atom::cast) + } +} + + + +impl Parse { + fn root(&self) -> Root { + Root::cast(self.syntax()).unwrap() + } + + pub fn apply(&self, _content: &str, nexp: rnix::SyntaxNode) -> anyhow::Result<(Vec, Vec)> { + + let mut pipe: Pipe = (Vec::new(), Box::new(std::iter::once(nexp))); + + for qexp in self.root().qexps() { + match qexp.kind() { + QexpKind::Atom(filter) => { + pipe = filter.apply(pipe); + } + _ => panic!("???") + } + } + + // let results = + // acc.map(|node| content[node.text_range().start().into()..node.text_range().end().into()].to_owned()) + // .collect(); + + Ok((pipe.0, pipe.1.collect())) + } +} diff --git a/src/queries.rs b/src/queries.rs index b07224a..c1e16df 100644 --- a/src/queries.rs +++ b/src/queries.rs @@ -1,9 +1,43 @@ // this is mostly based on the s-exp tutorial // https://github.com/rust-analyzer/rowan/blob/master/examples/s_expressions.rs -use rnix::{match_ast, ast}; -use rowan::{GreenNode, GreenNodeBuilder, ast::AstNode}; +use rowan::{GreenNode, GreenNodeBuilder}; +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[allow(non_camel_case_types)] +#[repr(u16)] +pub enum SyntaxKind { + L_BRACKET = 0, // '[' + R_BRACKET, // ']' + WORD, // 'Attrset', 'meta', '.', '>', ... + WHITESPACE, // whitespaces is explicit + ERROR, // as well as errors + + // composite nodes + LIST, // `[..]` + ATOM, // wraps WORD + ROOT, // top-level (a complete query) +} +use SyntaxKind::*; + +impl From for rowan::SyntaxKind { + fn from(kind: SyntaxKind) -> Self { + Self(kind as u16) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum QueryLang {} +impl rowan::Language for QueryLang { + type Kind = SyntaxKind; + fn kind_from_raw(raw: rowan::SyntaxKind) -> Self::Kind { + assert!(raw.0 <= ROOT as u16); + unsafe { std::mem::transmute::(raw.0) } + } + fn kind_to_raw(kind: Self::Kind) -> rowan::SyntaxKind { + kind.into() + } +} fn lex(text: &str) -> Vec<(SyntaxKind, String)> { fn tok(t: SyntaxKind) -> m_lexer::TokenKind { @@ -43,42 +77,7 @@ fn lex(text: &str) -> Vec<(SyntaxKind, String)> { } -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -#[allow(non_camel_case_types)] -#[repr(u16)] -enum SyntaxKind { - L_BRACKET = 0, // '[' - R_BRACKET, // ']' - WORD, // 'Attrset', 'meta', '.', '>', ... - WHITESPACE, // whitespaces is explicit - ERROR, // as well as errors - - // composite nodes - LIST, // `[..]` - ATOM, // wraps WORD - ROOT, // top-level (a complete query) -} -use SyntaxKind::*; - -impl From for rowan::SyntaxKind { - fn from(kind: SyntaxKind) -> Self { - Self(kind as u16) - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -enum Lang {} -impl rowan::Language for Lang { - type Kind = SyntaxKind; - fn kind_from_raw(raw: rowan::SyntaxKind) -> Self::Kind { - assert!(raw.0 <= ROOT as u16); - unsafe { std::mem::transmute::(raw.0) } - } - fn kind_to_raw(kind: Self::Kind) -> rowan::SyntaxKind { - kind.into() - } -} - +#[derive(Clone)] pub struct Parse { pub green_node: GreenNode, pub errors: Vec, @@ -207,14 +206,14 @@ pub fn parse(text: &str) -> Parse { /// but it contains parent pointers, offsets, and /// has identity semantics. -type SyntaxNode = rowan::SyntaxNode; +pub type SyntaxNode = rowan::SyntaxNode; #[allow(unused)] -type SyntaxToken = rowan::SyntaxToken; +type SyntaxToken = rowan::SyntaxToken; #[allow(unused)] type SyntaxElement = rowan::NodeOrToken; impl Parse { - fn syntax(&self) -> SyntaxNode { + pub fn syntax(&self) -> SyntaxNode { SyntaxNode::new_root(self.green_node.clone()) } } @@ -257,155 +256,3 @@ fn test_parser() { -type NixExprs = Box>; - -macro_rules! ast_node { - ($ast:ident, $kind:ident) => { - #[derive(PartialEq, Eq, Hash)] - #[repr(transparent)] - struct $ast(SyntaxNode); - impl $ast { - #[allow(unused)] - fn cast(node: SyntaxNode) -> Option { - if node.kind() == $kind { - Some(Self(node)) - } else { - None - } - } - } - }; -} - -ast_node!(Root, ROOT); -ast_node!(Atom, ATOM); -ast_node!(List, LIST); - -// Sexp is slightly different, so let's do it by hand. -#[derive(PartialEq, Eq, Hash, Debug)] -#[repr(transparent)] -struct Qexp(SyntaxNode); - -enum QexpKind { - Atom(Atom), - List(List), -} - -impl Qexp { - fn cast(node: SyntaxNode) -> Option { - if Atom::cast(node.clone()).is_some() || List::cast(node.clone()).is_some() { - Some(Qexp(node)) - } else { - None - } - } - - fn kind(&self) -> QexpKind { - Atom::cast(self.0.clone()) - .map(QexpKind::Atom) - .or_else(|| List::cast(self.0.clone()).map(QexpKind::List)) - .unwrap() - } - - fn apply(&self, _acc: NixExprs) -> NixExprs { - todo!() - } -} - -// Let's enhance AST nodes with ancillary functions and -// eval. -impl Root { - fn qexps(&self) -> impl Iterator + '_ { - self.0.children().filter_map(Qexp::cast) - } -} - -enum Op { - Down, - DownRecursive, - Up, - UpRecursive, - Named(String) -} - -impl Atom { - fn eval(&self) -> Option { - self.text().parse().ok() - } - fn as_op(&self) -> Option { - let op = match self.text().as_str() { - ">" => Op::Down, - ">>" => Op::DownRecursive, - "<" => Op::Up, - "<<" => Op::UpRecursive, - name => Op::Named(name.to_owned()), - }; - Some(op) - } - fn text(&self) -> String { - match self.0.green().children().next() { - Some(rowan::NodeOrToken::Token(token)) => token.text().to_string(), - _ => unreachable!(), - } - } - fn apply(&self, acc: NixExprs) -> NixExprs { - match self.as_op() { - Some(Op::Down) => Box::new(acc.map(|s| s.children()).flatten()), - Some(Op::DownRecursive) => Box::new(acc.map(|s| s.descendants()).flatten()), - Some(Op::Up) => Box::new(acc.filter_map(|s| s.parent())), - Some(Op::UpRecursive) => Box::new(acc.map(|s| s.ancestors()).flatten()), - Some(Op::Named(name)) => - Box::new(acc - .filter(move |node| match_ast! { match node { - ast::AttrpathValue(value) => { - name == value.attrpath().unwrap().to_string() - }, - ast::Apply(value) => { - // TODO: special case lambda = NODE_SELECT here? - name == value.lambda().unwrap().to_string() - }, - // TODO: this is difficult — I want to use free-form names - // to select things below, too, but that might not always be - // possible - ast::Ident(value) => { - name == value.to_string() - }, - _ => false - }})), - _ => todo!() - } - } -} - -impl List { - fn sexps(&self) -> impl Iterator + '_ { - self.0.children().filter_map(Qexp::cast) - } -} - - -impl Parse { - fn root(&self) -> Root { - Root::cast(self.syntax()).unwrap() - } - - pub fn apply(&self, _content: &str, nexp: rnix::SyntaxNode) -> anyhow::Result> { - - let mut acc: NixExprs = Box::new(std::iter::once(nexp)); - - for qexp in self.root().qexps() { - match qexp.kind() { - QexpKind::Atom(filter) => { - acc = filter.apply(acc); - } - _ => panic!("???") - } - } - - // let results = - // acc.map(|node| content[node.text_range().start().into()..node.text_range().end().into()].to_owned()) - // .collect(); - - Ok(acc.collect()) - } -} -- cgit v1.2.3