Skip to main content

openquiver_graph/
trivia.rs

1//! Comments, and what they are anchored to.
2
3use std::collections::BTreeMap;
4
5use crate::{Edge, NodeName};
6
7/// Where a comment is anchored.
8///
9/// One enum rather than a bucket per position. Adding a construct to the
10/// grammar then means adding a variant here — a compile error in every `match`
11/// that has to handle it — instead of quietly creating another place for
12/// comments to fall through, which is exactly how three positions were lost
13/// when configuration blocks and `layout` were added.
14#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
15pub enum Anchor {
16    /// Between the schema header and the `graph` block.
17    Header,
18    Node(NodeName),
19    /// One entry inside a node's configuration block.
20    Config(NodeName, String),
21    /// Just before a configuration block's closing brace.
22    ConfigTrailing(NodeName),
23    Edge(Edge),
24    /// Just before the graph block's closing brace.
25    GraphTrailing,
26    /// Between the graph block and `layout`.
27    LayoutLeading,
28    LayoutEntry(NodeName),
29    /// Just before the layout block's closing brace.
30    LayoutTrailing,
31    /// After everything else in the file.
32    FileTrailing,
33}
34
35impl Anchor {
36    /// The node this anchor hangs off, if any. Used to move comments when a
37    /// node is renamed or removed.
38    pub(crate) fn node(&self) -> Option<&NodeName> {
39        match self {
40            Anchor::Node(n)
41            | Anchor::Config(n, _)
42            | Anchor::ConfigTrailing(n)
43            | Anchor::LayoutEntry(n) => Some(n),
44            Anchor::Edge(_)
45            | Anchor::Header
46            | Anchor::GraphTrailing
47            | Anchor::LayoutLeading
48            | Anchor::LayoutTrailing
49            | Anchor::FileTrailing => None,
50        }
51    }
52
53    pub(crate) fn with_node(&self, name: NodeName) -> Self {
54        match self {
55            Anchor::Node(_) => Anchor::Node(name),
56            Anchor::Config(_, pin) => Anchor::Config(name, pin.clone()),
57            Anchor::ConfigTrailing(_) => Anchor::ConfigTrailing(name),
58            Anchor::LayoutEntry(_) => Anchor::LayoutEntry(name),
59            other => other.clone(),
60        }
61    }
62}
63
64/// Ordinary `//` comments, keyed by what they describe.
65///
66/// Keying by identity rather than by position is what lets the editor mutate a
67/// graph and save it without losing anybody's notes: an untouched node keeps
68/// its comments wherever canonical ordering moves it.
69///
70/// This lives beside the IR rather than in the syntax crate for one reason: the
71/// keys are IR identities, and renaming a node has to update the table in the
72/// same operation that updates the graph. A table one layer above the code
73/// performing the rename cannot be kept in step with it.
74///
75/// `///` comments are deliberately absent — those are semantic and live on
76/// [`Node::doc`].
77#[derive(Debug, Clone, Default, PartialEq, Eq)]
78pub struct Trivia(pub(crate) BTreeMap<Anchor, Vec<String>>);
79
80impl Trivia {
81    pub fn get(&self, anchor: &Anchor) -> &[String] {
82        self.0.get(anchor).map_or(&[], Vec::as_slice)
83    }
84
85    /// Appends rather than replaces. Two identical wires drawn on two branches
86    /// dedupe into one edge, but discarding one side's explanation of it is the
87    /// wrong half to throw away.
88    pub fn add(&mut self, anchor: Anchor, comments: Vec<String>) {
89        if comments.is_empty() {
90            return;
91        }
92        self.0.entry(anchor).or_default().extend(comments);
93    }
94
95    pub fn is_empty(&self) -> bool {
96        self.0.is_empty()
97    }
98}