Skip to main content

escriba_core/
register.rs

1//! The register — captured text plus **how it was captured**.
2//!
3//! A register is not a `String`. `dw` and `dd` both leave text behind, and a
4//! put has to replay them differently: `dw`'s text goes back *inside* a line
5//! at a column, `dd`'s goes back *as* a line. vim carries that distinction on
6//! the register, not on the put key, which is why `p` after `dw` and `p` after
7//! `dd` do visibly different things from the same keystroke.
8//!
9//! Carrying it as a typed [`RegisterKind`] rather than a `linewise: bool`
10//! makes the put's `match` total: visual-block's `Blockwise` — the one kind
11//! vim has that escriba does not yet — fails to compile at every consumer when
12//! it lands, instead of silently taking the charwise arm and pasting a
13//! rectangle as a run of text.
14
15use schemars::JsonSchema;
16use serde::{Deserialize, Serialize};
17
18/// How a register's text was captured, and therefore how a put must replay it.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
20pub enum RegisterKind {
21    /// Captured as a run of characters (`dw`, `y$`, `diw`, `d/pat`).
22    /// A put inserts it at a column, inside whatever line the cursor is on.
23    Charwise,
24    /// Captured as whole lines, terminators included (`dd`, `yy`, `3dd`).
25    /// A put opens new lines above or below the cursor's line.
26    Linewise,
27}
28
29/// The unnamed register's contents.
30///
31/// `text` is stored exactly as it was captured — a linewise capture keeps its
32/// trailing newline, because that newline is what makes it a *line*. The put
33/// normalizes rather than the capture, so the register always reads back what
34/// the buffer gave it.
35#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
36pub struct Register {
37    pub text: String,
38    pub kind: RegisterKind,
39}
40
41impl Register {
42    #[must_use]
43    pub fn new(text: impl Into<String>, kind: RegisterKind) -> Self {
44        Self {
45            text: text.into(),
46            kind,
47        }
48    }
49
50    #[must_use]
51    pub fn charwise(text: impl Into<String>) -> Self {
52        Self::new(text, RegisterKind::Charwise)
53    }
54
55    #[must_use]
56    pub fn linewise(text: impl Into<String>) -> Self {
57        Self::new(text, RegisterKind::Linewise)
58    }
59
60    #[must_use]
61    pub const fn is_linewise(&self) -> bool {
62        matches!(self.kind, RegisterKind::Linewise)
63    }
64
65    /// The text a put of `count` copies should insert.
66    ///
67    /// Linewise content is newline-TERMINATED before repeating, so `2p` of a
68    /// register captured from a file with no trailing newline still yields two
69    /// separate lines rather than one glued pair. The capture is left alone
70    /// (see the struct note) and the normalization happens here, once, where
71    /// both put directions share it.
72    #[must_use]
73    pub fn replayed(&self, count: u32) -> String {
74        let unit = match self.kind {
75            RegisterKind::Charwise => self.text.clone(),
76            RegisterKind::Linewise if self.text.ends_with('\n') => self.text.clone(),
77            RegisterKind::Linewise => {
78                let mut t = self.text.clone();
79                t.push('\n');
80                t
81            }
82        };
83        unit.repeat(count.max(1) as usize)
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90
91    #[test]
92    fn a_linewise_replay_is_newline_terminated_even_when_the_capture_was_not() {
93        // The case a naive `text.repeat(n)` gets wrong: the last line of a
94        // file with no trailing newline yanks as `"foxtrot"`, and `2p` of it
95        // must be two lines, not `"foxtrotfoxtrot"`.
96        assert_eq!(
97            Register::linewise("foxtrot").replayed(2),
98            "foxtrot\nfoxtrot\n"
99        );
100    }
101
102    #[test]
103    fn a_charwise_replay_is_verbatim() {
104        assert_eq!(Register::charwise("ab").replayed(3), "ababab");
105    }
106
107    #[test]
108    fn a_zero_count_still_puts_once() {
109        // Counts reach here already defaulted to 1, but a 0 that slipped
110        // through must not silently delete the put.
111        assert_eq!(Register::charwise("x").replayed(0), "x");
112    }
113}