1mod context;
4mod error;
5mod inputs;
6mod node;
7mod outputs;
8pub(crate) mod toggle;
9
10use std::collections::HashMap;
11
12use rnix::{Root, SyntaxKind, SyntaxNode};
13
14use crate::change::Change;
15use crate::edit::{OutputChange, Outputs};
16use crate::follows::path::follows_idents_prefixed;
17use crate::follows::{AttrPath, Segment, strip_outer_quotes};
18use crate::input::Input;
19
20pub(crate) use context::Context;
21pub use error::WalkerError;
22
23use inputs::walk_inputs;
24use node::{
25 FollowsKind, adjacent_whitespace_index, get_sibling_whitespace, insertion_index_after,
26 last_line_with_newline, make_quoted_string, make_toplevel_flake_false_attr,
27 make_toplevel_url_attr, parse_node, substitute_child,
28};
29
30pub(crate) fn flake_attr_set(root: &SyntaxNode) -> Option<SyntaxNode> {
41 let first = root.first_child()?;
42 if first.kind() != SyntaxKind::NODE_LET_IN {
43 return Some(first);
44 }
45 let body = first.last_child()?;
48 (body.kind() == SyntaxKind::NODE_ATTR_SET).then_some(body)
49}
50
51fn idents_match(have: &[String], expected: &[&str]) -> bool {
54 if have.len() != expected.len() {
55 return false;
56 }
57 have.iter()
58 .zip(expected.iter())
59 .all(|(h, e)| strip_outer_quotes(h) == *e)
60}
61
62fn is_flat_inputs_attr_for(idents: &[String], parent_id: &str) -> bool {
63 idents.len() >= 2 && idents[0] == "inputs" && strip_outer_quotes(&idents[1]) == parent_id
64}
65
66fn block_parent_attrset(
67 toplevel: &SyntaxNode,
68 idents: &[String],
69 parent_id: &str,
70) -> Option<SyntaxNode> {
71 if idents.len() != 2 {
72 return None;
73 }
74 if !is_flat_inputs_attr_for(idents, parent_id) {
75 return None;
76 }
77 toplevel
78 .children()
79 .find(|c| c.kind() == SyntaxKind::NODE_ATTR_SET)
80}
81
82fn retarget_existing_flat_follows(
86 attr_set: &SyntaxNode,
87 toplevel: &SyntaxNode,
88 value_node: Option<SyntaxNode>,
89 target: &str,
90) -> Option<SyntaxNode> {
91 let current_target = value_node
92 .as_ref()
93 .map(|v| strip_outer_quotes(&v.to_string()).to_string())
94 .unwrap_or_default();
95
96 if current_target == target {
97 return attr_set.ancestors().last();
98 }
99
100 let value = value_node?;
101 let new_value = make_quoted_string(target);
102 let new_toplevel = substitute_child(toplevel, value.index(), &new_value);
103 let green = attr_set
104 .green()
105 .replace_child(toplevel.index(), new_toplevel.green().into());
106 Some(SyntaxNode::new_root(attr_set.replace_with(green)))
107}
108
109fn insert_flat_follows_after(
113 attr_set: &SyntaxNode,
114 ref_child: &SyntaxNode,
115 path: &AttrPath,
116 target: &str,
117) -> SyntaxNode {
118 let follows_node = FollowsKind::TopLevelNested { path, target }.emit();
119 let insert_index = insertion_index_after(ref_child);
120
121 let mut green = attr_set
122 .green()
123 .insert_child(insert_index, follows_node.green().into());
124
125 if let Some(whitespace) = get_sibling_whitespace(ref_child) {
126 let ws_str = whitespace.to_string();
127 let ws_node = parse_node(last_line_with_newline(&ws_str));
128 green = green.insert_child(insert_index, ws_node.green().into());
129 }
130
131 SyntaxNode::new_root(attr_set.replace_with(green))
132}
133
134#[derive(Debug, Clone)]
135pub struct Walker {
136 pub(crate) root: SyntaxNode,
137 pub(crate) inputs: HashMap<String, Input>,
138 pub(crate) add_toplevel: bool,
139}
140
141impl<'a> Walker {
142 pub fn new(stream: &'a str) -> Self {
143 let root = Root::parse(stream).syntax();
144 Self::from_root(root)
145 }
146
147 pub fn from_root(root: SyntaxNode) -> Self {
150 Self {
151 root,
152 inputs: HashMap::new(),
153 add_toplevel: false,
154 }
155 }
156
157 pub fn walk(&mut self, change: &Change) -> Result<Option<SyntaxNode>, WalkerError> {
163 let cst = self.root.clone();
164 if cst.kind() != SyntaxKind::NODE_ROOT {
165 return Err(WalkerError::NotARoot);
166 }
167 self.walk_toplevel(cst, None, change)
168 }
169
170 pub(crate) fn list_outputs(&mut self) -> Result<Outputs, WalkerError> {
172 outputs::list_outputs(&self.root)
173 }
174
175 pub(crate) fn change_outputs(
177 &mut self,
178 change: OutputChange,
179 ) -> Result<Option<SyntaxNode>, WalkerError> {
180 outputs::change_outputs(&self.root, change)
181 }
182
183 fn walk_toplevel(
185 &mut self,
186 node: SyntaxNode,
187 ctx: Option<Context>,
188 change: &Change,
189 ) -> Result<Option<SyntaxNode>, WalkerError> {
190 let Some(attr_set) = flake_attr_set(&node) else {
191 return Ok(None);
192 };
193
194 for toplevel in attr_set.children() {
195 if toplevel.kind() != SyntaxKind::NODE_ATTRPATH_VALUE {
196 let range = toplevel.text_range();
197 return Err(WalkerError::unexpected_top_level(
198 &toplevel.to_string(),
199 range.start().into(),
200 ));
201 }
202
203 let Some(attrpath) = toplevel
207 .children()
208 .find(|c| c.kind() == SyntaxKind::NODE_ATTRPATH)
209 else {
210 continue;
211 };
212 let mut path_idents = attrpath.children();
213 let Some(first_ident) = path_idents.next() else {
214 continue;
215 };
216 let has_more_idents = path_idents.next().is_some();
217 let first_text = first_ident.to_string();
218 let first_unquoted = strip_outer_quotes(&first_text);
219
220 if !has_more_idents && first_unquoted == "description" {
221 continue;
222 }
223
224 if first_unquoted == "inputs" {
225 if has_more_idents {
226 if let Some(result) =
227 self.handle_inputs_flat(&attr_set, &toplevel, &attrpath, &ctx, change)
228 {
229 return Ok(Some(result));
230 }
231 } else if let Some(result) =
232 self.handle_inputs_attr(&toplevel, &attrpath, &ctx, change)
233 {
234 return Ok(Some(result));
235 }
236 continue;
237 }
238
239 if !has_more_idents
240 && first_unquoted == "outputs"
241 && let Some(result) = self.handle_add_at_outputs(&attr_set, &toplevel, change)
242 {
243 return Ok(Some(result));
244 }
245 }
246
247 if let Change::Follows { input, target } = change {
249 let path = input.path();
250 if path.len() >= 2 {
251 let parent_id = input.input();
252 if self.inputs.contains_key(parent_id.as_str()) {
253 let target_str = target.to_flake_follows_string();
254 return self.handle_follows_flat_toplevel(&attr_set, path, &target_str);
255 }
256 }
257 }
258
259 Ok(None)
260 }
261
262 fn handle_follows_flat_toplevel(
270 &self,
271 attr_set: &SyntaxNode,
272 path: &AttrPath,
273 target: &str,
274 ) -> Result<Option<SyntaxNode>, WalkerError> {
275 let parent_id = path.first();
276 let expected_flat = follows_idents_prefixed(path.segments());
277 let mut last_parent_attr: Option<SyntaxNode> = None;
278 let mut block_parent: Option<(SyntaxNode, SyntaxNode)> = None;
279
280 for toplevel in attr_set.children() {
281 if toplevel.kind() != SyntaxKind::NODE_ATTRPATH_VALUE {
282 continue;
283 }
284 let Some(attrpath) = toplevel
285 .children()
286 .find(|c| c.kind() == SyntaxKind::NODE_ATTRPATH)
287 else {
288 continue;
289 };
290 let idents: Vec<String> = attrpath.children().map(|c| c.to_string()).collect();
291
292 if let Some(block_attr_set) =
293 block_parent_attrset(&toplevel, &idents, parent_id.as_str())
294 {
295 block_parent = Some((toplevel.clone(), block_attr_set));
296 }
297
298 if idents_match(&idents, &expected_flat)
299 && let Some(rebuilt) = retarget_existing_flat_follows(
300 attr_set,
301 &toplevel,
302 attrpath.next_sibling(),
303 target,
304 )
305 {
306 return Ok(Some(rebuilt));
307 }
308
309 if is_flat_inputs_attr_for(&idents, parent_id.as_str()) {
310 last_parent_attr = Some(toplevel.clone());
311 }
312 }
313
314 if let Some((toplevel, block_attr_set)) = block_parent {
315 let rest: Vec<Segment> = path.segments()[1..].to_vec();
316 return self.handle_follows_block_toplevel(
317 attr_set,
318 &toplevel,
319 &block_attr_set,
320 &rest,
321 target,
322 );
323 }
324
325 if let Some(ref_child) = last_parent_attr {
326 return Ok(Some(insert_flat_follows_after(
327 attr_set, &ref_child, path, target,
328 )));
329 }
330
331 Ok(None)
332 }
333
334 fn handle_follows_block_toplevel(
335 &self,
336 attr_set: &SyntaxNode,
337 toplevel: &SyntaxNode,
338 block_attr_set: &SyntaxNode,
339 rest: &[Segment],
340 target: &str,
341 ) -> Result<Option<SyntaxNode>, WalkerError> {
342 let expected_block = follows_idents_prefixed(rest);
343 for attr in block_attr_set.children() {
344 if attr.kind() != SyntaxKind::NODE_ATTRPATH_VALUE {
345 continue;
346 }
347 let Some(attrpath) = attr
348 .children()
349 .find(|c| c.kind() == SyntaxKind::NODE_ATTRPATH)
350 else {
351 continue;
352 };
353 let idents: Vec<String> = attrpath.children().map(|c| c.to_string()).collect();
354
355 if idents_match(&idents, &expected_block) {
356 let value_node = attrpath.next_sibling();
357 let current_target = value_node
358 .as_ref()
359 .map(|v| strip_outer_quotes(&v.to_string()).to_string())
360 .unwrap_or_default();
361
362 if current_target == target {
363 return Ok(attr_set.ancestors().last());
364 }
365
366 if let Some(value) = value_node {
367 let new_value = make_quoted_string(target);
368 let new_attr = substitute_child(&attr, value.index(), &new_value);
369 let new_block = substitute_child(block_attr_set, attr.index(), &new_attr);
370 let new_toplevel =
371 substitute_child(toplevel, block_attr_set.index(), &new_block);
372 let green = attr_set
373 .green()
374 .replace_child(toplevel.index(), new_toplevel.green().into());
375 return Ok(Some(SyntaxNode::new_root(attr_set.replace_with(green))));
376 }
377 }
378 }
379
380 let follows_node = FollowsKind::BlockNested { rest, target }.emit();
381 let children: Vec<_> = block_attr_set.children().collect();
382 if let Some(last_child) = children.last() {
383 let insert_index = last_child.index() + 1;
384
385 let mut green = block_attr_set
386 .green()
387 .insert_child(insert_index, follows_node.green().into());
388
389 if let Some(whitespace) = get_sibling_whitespace(last_child) {
390 green = green.insert_child(insert_index, whitespace.green().into());
391 }
392
393 let new_block = SyntaxNode::new_root(green);
394 let new_toplevel = substitute_child(toplevel, block_attr_set.index(), &new_block);
395 let green = attr_set
396 .green()
397 .replace_child(toplevel.index(), new_toplevel.green().into());
398 return Ok(Some(SyntaxNode::new_root(attr_set.replace_with(green))));
399 }
400
401 Ok(None)
402 }
403
404 fn handle_inputs_attr(
409 &mut self,
410 toplevel: &SyntaxNode,
411 child: &SyntaxNode,
412 ctx: &Option<Context>,
413 change: &Change,
414 ) -> Option<SyntaxNode> {
415 let sibling = child.next_sibling()?;
416 let replacement = walk_inputs(&mut self.inputs, sibling.clone(), ctx, change)?;
417
418 let green = toplevel
419 .green()
420 .replace_child(sibling.index(), replacement.green().into());
421 let green = toplevel.replace_with(green);
422 Some(SyntaxNode::new_root(green))
423 }
424
425 fn handle_inputs_flat(
430 &mut self,
431 attr_set: &SyntaxNode,
432 toplevel: &SyntaxNode,
433 child: &SyntaxNode,
434 ctx: &Option<Context>,
435 change: &Change,
436 ) -> Option<SyntaxNode> {
437 let replacement = walk_inputs(&mut self.inputs, child.clone(), ctx, change)?;
438
439 if replacement.to_string().is_empty() {
442 let element: rnix::SyntaxElement = toplevel.clone().into();
443 let mut green = attr_set.green().remove_child(toplevel.index());
444 if let Some(ws_index) = adjacent_whitespace_index(&element) {
445 green = green.remove_child(ws_index);
446 }
447 return Some(SyntaxNode::new_root(attr_set.replace_with(green)));
448 }
449
450 let sibling = child.next_sibling()?;
451 let green = toplevel
452 .green()
453 .replace_child(sibling.index(), replacement.green().into());
454 let green = toplevel.replace_with(green);
455 Some(SyntaxNode::new_root(green))
456 }
457
458 fn handle_add_at_outputs(
463 &mut self,
464 attr_set: &SyntaxNode,
465 toplevel: &SyntaxNode,
466 change: &Change,
467 ) -> Option<SyntaxNode> {
468 if !self.add_toplevel {
469 return None;
470 }
471
472 let Change::Add {
473 id: Some(id),
474 uri: Some(uri),
475 flake,
476 } = change
477 else {
478 return None;
479 };
480 let id = id.input().as_str();
481
482 if toplevel.index() == 0 {
483 return None;
484 }
485
486 let ws_node = {
490 let mut ws: Option<SyntaxNode> = None;
491 let mut cursor = toplevel.prev_sibling_or_token();
492 while let Some(ref tok) = cursor {
493 if tok.kind() == SyntaxKind::TOKEN_WHITESPACE {
494 let ws_str = tok.to_string();
495 ws = Some(parse_node(last_line_with_newline(&ws_str)));
496 break;
497 }
498 cursor = tok.prev_sibling_or_token();
499 }
500 ws
501 };
502
503 let addition = make_toplevel_url_attr(id, uri);
504 let insert_pos = toplevel.index() - 1;
505
506 let mut green = attr_set
507 .green()
508 .insert_child(insert_pos, addition.green().into());
509
510 if let Some(ref ws) = ws_node {
511 green = green.insert_child(insert_pos, ws.green().into());
512 }
513
514 if !flake {
516 let no_flake = make_toplevel_flake_false_attr(id);
517 green = green.insert_child(toplevel.index() + 1, no_flake.green().into());
518
519 if let Some(ref ws) = ws_node {
520 green = green.insert_child(toplevel.index() + 1, ws.green().into());
521 }
522 }
523
524 Some(SyntaxNode::new_root(attr_set.replace_with(green)))
525 }
526}
527
528#[cfg(test)]
529mod tests {
530 use super::*;
531 use crate::change::{Change, ChangeId};
532 use crate::follows::AttrPath;
533
534 fn apply(flake_text: &str, change: &Change) -> String {
535 let mut walker = Walker::new(flake_text);
536 walker
537 .walk(change)
538 .expect("walker error")
539 .expect("walker did not rewrite the tree")
540 .to_string()
541 }
542
543 fn follows_change(input: &str, target: &str) -> Change {
544 Change::Follows {
545 input: ChangeId::parse(input).unwrap(),
546 target: AttrPath::parse(target).unwrap(),
547 }
548 }
549
550 #[test]
551 fn handle_follows_flat_toplevel_inserts_follows_after_last_parent_attr() {
552 let flake = "{
553 inputs.flake-edit.url = \"github:a-kenji/flake-edit\";
554 inputs.nixpkgs.url = \"github:NixOS/nixpkgs\";
555
556 outputs = { self, ... }: { };
557}
558";
559 let result = apply(flake, &follows_change("flake-edit.nixpkgs", "nixpkgs"));
560 assert_eq!(
561 result,
562 "{
563 inputs.flake-edit.url = \"github:a-kenji/flake-edit\";
564 inputs.flake-edit.inputs.nixpkgs.follows = \"nixpkgs\";
565 inputs.nixpkgs.url = \"github:NixOS/nixpkgs\";
566
567 outputs = { self, ... }: { };
568}
569"
570 );
571 }
572
573 #[test]
574 fn handle_follows_flat_toplevel_retargets_existing_follows() {
575 let flake = "{
576 inputs.flake-edit.url = \"github:a-kenji/flake-edit\";
577 inputs.flake-edit.inputs.nixpkgs.follows = \"old-pkgs\";
578 inputs.nixpkgs.url = \"github:NixOS/nixpkgs\";
579
580 outputs = { self, ... }: { };
581}
582";
583 let result = apply(flake, &follows_change("flake-edit.nixpkgs", "nixpkgs"));
584 assert_eq!(
585 result,
586 "{
587 inputs.flake-edit.url = \"github:a-kenji/flake-edit\";
588 inputs.flake-edit.inputs.nixpkgs.follows = \"nixpkgs\";
589 inputs.nixpkgs.url = \"github:NixOS/nixpkgs\";
590
591 outputs = { self, ... }: { };
592}
593"
594 );
595 }
596
597 #[test]
598 fn handle_follows_flat_toplevel_is_noop_when_target_already_matches() {
599 let flake = "{
600 inputs.flake-edit.url = \"github:a-kenji/flake-edit\";
601 inputs.flake-edit.inputs.nixpkgs.follows = \"nixpkgs\";
602 inputs.nixpkgs.url = \"github:NixOS/nixpkgs\";
603
604 outputs = { self, ... }: { };
605}
606";
607 let result = apply(flake, &follows_change("flake-edit.nixpkgs", "nixpkgs"));
608 assert_eq!(result, flake);
609 }
610
611 #[test]
612 fn handle_follows_flat_toplevel_delegates_to_block_parent_when_present() {
613 let flake = "{
614 inputs.nixpkgs.url = \"github:NixOS/nixpkgs\";
615 inputs.flake-edit = {
616 url = \"github:a-kenji/flake-edit\";
617 };
618
619 outputs = { self, ... }: { };
620}
621";
622 let result = apply(flake, &follows_change("flake-edit.nixpkgs", "nixpkgs"));
623 assert_eq!(
624 result,
625 "{
626 inputs.nixpkgs.url = \"github:NixOS/nixpkgs\";
627 inputs.flake-edit = {
628 url = \"github:a-kenji/flake-edit\";
629 inputs.nixpkgs.follows = \"nixpkgs\";
630 };
631
632 outputs = { self, ... }: { };
633}
634"
635 );
636 }
637
638 #[test]
639 fn is_flat_inputs_attr_for_only_matches_matching_parent_id() {
640 let yes = [
641 "inputs".to_string(),
642 "flake-edit".to_string(),
643 "url".to_string(),
644 ];
645 let no = [
646 "inputs".to_string(),
647 "nixpkgs".to_string(),
648 "url".to_string(),
649 ];
650 assert!(is_flat_inputs_attr_for(&yes, "flake-edit"));
651 assert!(!is_flat_inputs_attr_for(&no, "flake-edit"));
652 let quoted = [
655 "inputs".to_string(),
656 "\"flake-edit\"".to_string(),
657 "url".to_string(),
658 ];
659 assert!(is_flat_inputs_attr_for("ed, "flake-edit"));
660 }
661}