Skip to main content

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}