openquiver-graph 0.1.0

OpenQuiver graph IR: nodes, pins, types. Bottom layer, no internal dependencies.
Documentation
//! Comments, and what they are anchored to.

use std::collections::BTreeMap;

use crate::{Edge, NodeName};

/// Where a comment is anchored.
///
/// One enum rather than a bucket per position. Adding a construct to the
/// grammar then means adding a variant here — a compile error in every `match`
/// that has to handle it — instead of quietly creating another place for
/// comments to fall through, which is exactly how three positions were lost
/// when configuration blocks and `layout` were added.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum Anchor {
    /// Between the schema header and the `graph` block.
    Header,
    Node(NodeName),
    /// One entry inside a node's configuration block.
    Config(NodeName, String),
    /// Just before a configuration block's closing brace.
    ConfigTrailing(NodeName),
    Edge(Edge),
    /// Just before the graph block's closing brace.
    GraphTrailing,
    /// Between the graph block and `layout`.
    LayoutLeading,
    LayoutEntry(NodeName),
    /// Just before the layout block's closing brace.
    LayoutTrailing,
    /// After everything else in the file.
    FileTrailing,
}

impl Anchor {
    /// The node this anchor hangs off, if any. Used to move comments when a
    /// node is renamed or removed.
    pub(crate) fn node(&self) -> Option<&NodeName> {
        match self {
            Anchor::Node(n)
            | Anchor::Config(n, _)
            | Anchor::ConfigTrailing(n)
            | Anchor::LayoutEntry(n) => Some(n),
            Anchor::Edge(_)
            | Anchor::Header
            | Anchor::GraphTrailing
            | Anchor::LayoutLeading
            | Anchor::LayoutTrailing
            | Anchor::FileTrailing => None,
        }
    }

    pub(crate) fn with_node(&self, name: NodeName) -> Self {
        match self {
            Anchor::Node(_) => Anchor::Node(name),
            Anchor::Config(_, pin) => Anchor::Config(name, pin.clone()),
            Anchor::ConfigTrailing(_) => Anchor::ConfigTrailing(name),
            Anchor::LayoutEntry(_) => Anchor::LayoutEntry(name),
            other => other.clone(),
        }
    }
}

/// Ordinary `//` comments, keyed by what they describe.
///
/// Keying by identity rather than by position is what lets the editor mutate a
/// graph and save it without losing anybody's notes: an untouched node keeps
/// its comments wherever canonical ordering moves it.
///
/// This lives beside the IR rather than in the syntax crate for one reason: the
/// keys are IR identities, and renaming a node has to update the table in the
/// same operation that updates the graph. A table one layer above the code
/// performing the rename cannot be kept in step with it.
///
/// `///` comments are deliberately absent — those are semantic and live on
/// [`Node::doc`].
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Trivia(pub(crate) BTreeMap<Anchor, Vec<String>>);

impl Trivia {
    pub fn get(&self, anchor: &Anchor) -> &[String] {
        self.0.get(anchor).map_or(&[], Vec::as_slice)
    }

    /// Appends rather than replaces. Two identical wires drawn on two branches
    /// dedupe into one edge, but discarding one side's explanation of it is the
    /// wrong half to throw away.
    pub fn add(&mut self, anchor: Anchor, comments: Vec<String>) {
        if comments.is_empty() {
            return;
        }
        self.0.entry(anchor).or_default().extend(comments);
    }

    pub fn is_empty(&self) -> bool {
        self.0.is_empty()
    }
}