Skip to main content

lattice_core/
wrap.rs

1//! `autowrap` — whether typing past `textwidth` breaks the line.
2//!
3//! Lives here rather than in `lattice-config` for the same reason
4//! [`crate::IndentMethod`] does: the reflow engine in `lattice-grammar`
5//! consumes the resolved value, and `lattice-config` → `lattice-grammar`
6//! would be the wrong direction. This crate is the shared floor both
7//! sides already stand on.
8//!
9//! See `docs/dev/architecture/text-reflow.md` §8.
10
11/// `textwidth`, resolved for a specific buffer — the column reflow
12/// targets and, when `autowrap` allows, the column typing breaks at.
13///
14/// **A newtype rather than a bare `usize`, and that is load-bearing.**
15/// It is carried by two env structs (`GrammarEnv` and the actor's
16/// `DispatchEnv`), both of which derive `Default` for their ~40
17/// hand-built call sites. A bare `usize` defaults to `0`, which is not a
18/// column anything can wrap at — so every one of those sites would
19/// silently get a reflow that does nothing, and the bug would look like
20/// "gq is broken in tests but works in the editor". Giving the *type* a
21/// meaningful default puts the answer in one place and makes a third env
22/// struct impossible to get wrong.
23///
24/// Same reasoning that made [`crate::IndentUnit`] a type rather than
25/// three loose fields.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
27pub struct WrapWidth(pub usize);
28
29/// The column a buffer wraps at when nobody has resolved config.
30///
31/// Matches `lattice_config::core_options::TextWidth`'s registered
32/// default; `lattice-core` cannot import it (the dependency points the
33/// other way), so a test in `lattice-config` — which can see both —
34/// pins them together.
35pub const DEFAULT_TEXTWIDTH: usize = 80;
36
37impl Default for WrapWidth {
38    fn default() -> Self {
39        WrapWidth(DEFAULT_TEXTWIDTH)
40    }
41}
42
43impl WrapWidth {
44    /// The target column.
45    pub fn columns(self) -> usize {
46        self.0
47    }
48}
49
50impl From<usize> for WrapWidth {
51    fn from(n: usize) -> Self {
52        WrapWidth(n)
53    }
54}
55
56crate::labeled_enum! {
57    /// `:set autowrap=...`. Whether inserting a character past
58    /// `textwidth` breaks the line and carries the remainder down.
59    ///
60    /// ## Why this exists as one named option
61    ///
62    /// Vim has no toggle for this at all — it is `formatoptions`, a bag
63    /// of nineteen single-letter flags of which `t` (wrap text) and `c`
64    /// (wrap comments) are the two anyone means. That is why nobody
65    /// remembers them. Emacs has `auto-fill-mode`, which names a 1980s
66    /// implementation ("filling") rather than the effect.
67    ///
68    /// ## Why it is an option and not a minor mode
69    ///
70    /// It owns no keymap, no lifecycle subscription, no decoration
71    /// provider and no buffer — it is a behaviour flag consulted on the
72    /// insert path, which is exactly what `electricindent` (IN.6)
73    /// already is. Same seam, same per-major override mechanism via
74    /// `Mode::options()`. A mode that is really a bool is a mode in name
75    /// only.
76    ///
77    /// ## Not to be confused with `wrap`
78    ///
79    /// `wrap` is SOFT wrap — a display decision, no bytes change. This
80    /// inserts real newlines. The two are orthogonal and compose: a
81    /// buffer may soft-wrap at the window edge while hard-wrapping at
82    /// `textwidth`.
83    pub enum AutoWrap {
84        /// Never break a line while typing. `gq` still reflows on
85        /// demand — `textwidth` remains the measure either way.
86        Off = "off" | "false" | "no"
87            => "Never wrap while typing (gq still reflows on demand)",
88        /// Break only inside comments, judged by the line's leading
89        /// comment marker. The default for code: a long comment wraps,
90        /// a long string literal does not.
91        #[default]
92        Comments = "comments"
93            => "Wrap comment lines only (the default for code)",
94        /// Break any line past `textwidth`. The default for prose
95        /// majors — markdown, org, text, git commit messages.
96        All = "all" | "true" | "yes"
97            => "Wrap any line past textwidth (the default for prose)",
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    #![allow(clippy::unwrap_used)]
104    use super::*;
105
106    #[test]
107    fn labels_round_trip() {
108        for v in AutoWrap::all() {
109            assert_eq!(AutoWrap::parse_label(v.label()).unwrap(), *v);
110        }
111    }
112
113    /// `off`/`all` take boolean-ish aliases because the option reads as
114    /// a toggle to anyone arriving from `auto-fill-mode` or from
115    /// `formatoptions+=t`, and `:set autowrap=true` failing with
116    /// "invalid value" would be a papercut for no gain.
117    #[test]
118    fn boolean_aliases_parse_to_the_ends_of_the_range() {
119        for yes in ["all", "true", "yes"] {
120            assert_eq!(AutoWrap::parse_label(yes).unwrap(), AutoWrap::All);
121        }
122        for no in ["off", "false", "no"] {
123            assert_eq!(AutoWrap::parse_label(no).unwrap(), AutoWrap::Off);
124        }
125    }
126
127    /// The default is `comments`, not `all`. Auto-wrapping a code line
128    /// mid-expression is destructive in a way wrapping prose is not, so
129    /// the global default is the conservative rung and prose majors opt
130    /// up through `Mode::options()` (RF.4).
131    #[test]
132    fn the_global_default_is_comments() {
133        assert_eq!(AutoWrap::default(), AutoWrap::Comments);
134    }
135
136    #[test]
137    fn an_unknown_label_is_rejected() {
138        assert!(AutoWrap::parse_label("sometimes").is_err());
139    }
140}