flake_edit/input.rs
1use rnix::TextRange;
2
3use crate::follows::{AttrPath, Segment, strip_outer_quotes};
4
5/// A single flake input declaration.
6#[derive(Debug, Clone, PartialEq, Hash, Eq, PartialOrd, Ord)]
7pub struct Input {
8 pub(crate) id: Segment,
9 pub(crate) flake: bool,
10 /// Stored unquoted. Quoting is re-applied at write-back time.
11 pub(crate) url: String,
12 pub(crate) follows: Vec<Follows>,
13 pub range: Range,
14}
15
16/// Source byte range, half-open: `[start, end)`.
17#[derive(Debug, Default, Clone, PartialEq, Hash, Eq, PartialOrd, Ord)]
18pub struct Range {
19 pub start: usize,
20 pub end: usize,
21}
22
23impl Range {
24 pub fn from_text_range(text_range: TextRange) -> Self {
25 Self {
26 start: text_range.start().into(),
27 end: text_range.end().into(),
28 }
29 }
30
31 /// True if the range is the default (zero) range, used as a sentinel for
32 /// inputs without a write-back location.
33 pub fn is_empty(&self) -> bool {
34 self.start == 0 && self.end == 0
35 }
36}
37
38/// A `follows` declaration on an [`Input`].
39#[derive(Debug, Clone, PartialEq, Hash, Eq, PartialOrd, Ord)]
40pub enum Follows {
41 /// A nested input redirected to another input via `follows = "..."`.
42 ///
43 /// `path` is the nested-input chain relative to the owning [`Input`] and
44 /// does not include the owner's id segment. `target` is the right-hand
45 /// side of the `follows = "..."`; `None` represents the empty-string
46 /// form `follows = ""`, the in-flake equivalent of the lockfile's
47 /// [`crate::lock::Input::Indirect`]`(None)` (an `inputs.X = []` entry).
48 ///
49 /// - `inputs.crane.inputs.nixpkgs.follows = "nixpkgs"` is stored on
50 /// `crane` as `Indirect { path: ["nixpkgs"], target: Some(["nixpkgs"]) }`.
51 /// - `inputs.neovim.inputs.nixvim.inputs.flake-parts.follows =
52 /// "flake-parts"` is stored on `neovim` as `Indirect { path:
53 /// ["nixvim", "flake-parts"], target: Some(["flake-parts"]) }`.
54 /// - `inputs.nix.inputs.flake-compat.follows = ""` is stored on `nix`
55 /// as `Indirect { path: ["flake-compat"], target: None }`.
56 Indirect {
57 path: AttrPath,
58 target: Option<AttrPath>,
59 },
60 /// A nested input declared inline with its own URL.
61 Direct(String, Input),
62}
63
64impl Input {
65 pub(crate) fn new(name: Segment) -> Self {
66 Self {
67 id: name,
68 flake: true,
69 url: String::new(),
70 follows: Vec::new(),
71 range: Range::default(),
72 }
73 }
74
75 /// Build an [`Input`] with `id`, `url`, and the range derived from
76 /// `text_range`. Surrounding double-quotes on `url` are stripped.
77 pub(crate) fn with_url(id: Segment, url: String, text_range: TextRange) -> Self {
78 Self {
79 id,
80 flake: true,
81 url: strip_outer_quotes(&url).to_string(),
82 follows: Vec::new(),
83 range: Range::from_text_range(text_range),
84 }
85 }
86
87 pub fn id(&self) -> &Segment {
88 &self.id
89 }
90
91 pub fn url(&self) -> &str {
92 self.url.as_ref()
93 }
94 pub fn follows(&self) -> &Vec<Follows> {
95 self.follows.as_ref()
96 }
97
98 /// True if the URL can be rewritten in place. False for synthetic inputs
99 /// without a known source range.
100 pub fn has_editable_url(&self) -> bool {
101 !self.url.is_empty() && !self.range.is_empty()
102 }
103
104 /// Append an `Indirect` follows entry and re-normalize the follows vec
105 /// (sort + dedup). Walker insertion sites maintain this invariant so
106 /// callers downstream (validate, follows-graph, snapshots) see one
107 /// canonical ordering.
108 pub(crate) fn push_indirect_follows(&mut self, path: AttrPath, target: Option<AttrPath>) {
109 self.follows.push(Follows::Indirect { path, target });
110 self.follows.sort();
111 self.follows.dedup();
112 }
113}