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}