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}