Skip to main content

ochrepad_core/
column_edit.rs

1//! The Column Editor (Notepad++'s Edit > Column Editor, Alt+C): the same
2//! text, or a sequence of numbers, inserted at one column on many lines.
3//!
4//! Where it goes, as in Notepad++:
5//! - with a column selection, on each of its lines at its left column (the
6//!   text inside the rectangle is replaced);
7//! - with several selections, at each of them (replacing it);
8//! - with one caret, on its line and every line below it, at the caret's
9//!   column (the empty line after a final line break is left out);
10//! - with one selection, on each of its lines at the column it starts in.
11//!
12//! A line too short to reach the column is padded with spaces.
13
14use std::collections::HashMap;
15use std::ops::Range;
16
17use serde::{Deserialize, Serialize};
18
19use crate::changeset::{Bias, ChangeSet, Edit};
20use crate::history::EditKind;
21use crate::position::{ByteOffset, VisualCol, byte_to_visual_col, visual_col_to_byte};
22use crate::resolve::Resolved;
23use crate::selection::{Selection, SelectionSet};
24use crate::text::Document;
25
26/// What the Column Editor inserts.
27#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
28pub enum ColumnInsert {
29    /// The same text on every line.
30    Text(String),
31    /// One number per line.
32    Numbers(NumberSequence),
33}
34
35/// Numbers for the Column Editor: `initial`, then `initial + increase`, and
36/// so on, each on `repeat` lines in a row.
37#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
38#[serde(default)]
39pub struct NumberSequence {
40    pub initial: i64,
41    pub increase: i64,
42    /// How many lines get each number (at least 1).
43    pub repeat: usize,
44    pub leading: Leading,
45    pub format: NumberFormat,
46}
47
48impl Default for NumberSequence {
49    fn default() -> Self {
50        Self {
51            initial: 1,
52            increase: 1,
53            repeat: 1,
54            leading: Leading::None,
55            format: NumberFormat::Dec,
56        }
57    }
58}
59
60/// Padding that makes all the numbers equally wide.
61#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
62pub enum Leading {
63    #[default]
64    None,
65    /// Zeros between the sign and the digits.
66    Zeros,
67    /// Spaces before the number (right-aligned).
68    Spaces,
69}
70
71#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
72pub enum NumberFormat {
73    #[default]
74    Dec,
75    /// Upper-case hexadecimal digits, as Notepad++ writes them.
76    Hex,
77    Oct,
78    Bin,
79}
80
81impl NumberSequence {
82    /// The first `count` numbers, formatted. Arithmetic saturates rather
83    /// than overflowing.
84    pub fn render(&self, count: usize) -> Vec<String> {
85        let repeat = self.repeat.max(1);
86        let parts: Vec<(bool, String)> = (0..count)
87            .map(|i| {
88                let steps = i64::try_from(i / repeat).unwrap_or(i64::MAX);
89                let value = self
90                    .initial
91                    .saturating_add(steps.saturating_mul(self.increase));
92                (value < 0, digits(value.unsigned_abs(), self.format))
93            })
94            .collect();
95        let digits_width = parts.iter().map(|(_, d)| d.len()).max().unwrap_or(0);
96        let full_width = parts
97            .iter()
98            .map(|(neg, d)| d.len() + usize::from(*neg))
99            .max()
100            .unwrap_or(0);
101        parts
102            .into_iter()
103            .map(|(neg, d)| {
104                let sign = if neg { "-" } else { "" };
105                match self.leading {
106                    Leading::None => format!("{sign}{d}"),
107                    Leading::Zeros => format!("{sign}{d:0>digits_width$}"),
108                    Leading::Spaces => format!("{:>full_width$}", format!("{sign}{d}")),
109                }
110            })
111            .collect()
112    }
113}
114
115fn digits(n: u64, format: NumberFormat) -> String {
116    match format {
117        NumberFormat::Dec => n.to_string(),
118        NumberFormat::Hex => format!("{n:X}"),
119        NumberFormat::Oct => format!("{n:o}"),
120        NumberFormat::Bin => format!("{n:b}"),
121    }
122}
123
124impl ColumnInsert {
125    /// The text for each of `count` lines.
126    pub fn items(&self, count: usize) -> Vec<String> {
127        match self {
128            ColumnInsert::Text(text) => vec![text.clone(); count],
129            ColumnInsert::Numbers(seq) => seq.render(count),
130        }
131    }
132}
133
134/// The most spaces one insertion may add to take short lines out to its
135/// column, in all.
136const MAX_PADDING: usize = 16 << 20;
137
138/// Where one item goes: the text it replaces (empty for an insertion), and
139/// the spaces that first take a short line out to the column.
140struct Target {
141    range: Range<ByteOffset>,
142    pad: usize,
143}
144
145/// The insertion point at visual column `col` of `line`: the grapheme
146/// boundary at or before it, or the line's end plus padding.
147fn at_column(doc: &Document, line: usize, col: VisualCol, tab_width: usize) -> Target {
148    let end = doc.line_end(line);
149    let width = byte_to_visual_col(doc, end, tab_width).0;
150    if width < col.0 {
151        Target {
152            range: end..end,
153            pad: col.0 - width,
154        }
155    } else {
156        let at = visual_col_to_byte(doc, line, col, tab_width);
157        Target {
158            range: at..at,
159            pad: 0,
160        }
161    }
162}
163
164/// Resolves the Column Editor's insertion (see the module documentation).
165pub(crate) fn insert(
166    doc: &Document,
167    sels: &SelectionSet,
168    what: &ColumnInsert,
169    tab_width: usize,
170) -> Resolved {
171    if matches!(what, ColumnInsert::Text(t) if t.is_empty()) {
172        return Resolved::Selections(sels.clone());
173    }
174    let many = sels.rect().is_some() || sels.len() > 1;
175    let targets: Vec<Target> = if let Some(r) = sels.rect() {
176        // A column selection has one selection per line; a line too short
177        // to reach the rectangle has a caret at its end.
178        let on_line: HashMap<usize, &Selection> = sels
179            .ranges()
180            .iter()
181            .map(|s| (doc.line_of(s.start()), s))
182            .collect();
183        (r.top()..=r.bottom().min(doc.line_count() - 1))
184            .map(|line| match on_line.get(&line) {
185                Some(s) if byte_to_visual_col(doc, s.start(), tab_width) >= r.left() => Target {
186                    range: s.range(),
187                    pad: 0,
188                },
189                _ => at_column(doc, line, r.left(), tab_width),
190            })
191            .collect()
192    } else if sels.len() > 1 {
193        sels.ranges()
194            .iter()
195            .map(|s| Target {
196                range: s.range(),
197                pad: 0,
198            })
199            .collect()
200    } else {
201        let s = sels.primary();
202        let first = doc.line_of(s.start());
203        let last = if s.is_empty() {
204            // Down to the end, without the empty line a final line break
205            // leaves.
206            let last = doc.line_count() - 1;
207            if last > first && doc.line_start(last) == doc.line_end(last) {
208                last - 1
209            } else {
210                last
211            }
212        } else {
213            let last = doc.line_of(s.end());
214            if last > first && s.end() == doc.line_start(last) {
215                last - 1
216            } else {
217                last
218            }
219        };
220        let col = byte_to_visual_col(doc, s.start(), tab_width);
221        (first..=last)
222            .map(|line| at_column(doc, line, col, tab_width))
223            .collect()
224    };
225    // Padding out to a column far past every line (a hand-written macro)
226    // would make a document of that size: refused.
227    if targets.iter().map(|t| t.pad).sum::<usize>() > MAX_PADDING {
228        return Resolved::Selections(sels.clone());
229    }
230    let items = what.items(targets.len());
231    let edits: Vec<Edit> = targets
232        .iter()
233        .zip(&items)
234        .map(|(t, item)| {
235            Edit::new(
236                t.range.start,
237                t.range.end,
238                format!("{}{item}", " ".repeat(t.pad)),
239            )
240        })
241        .collect();
242    let changes = ChangeSet::new(doc.len(), edits);
243    if changes.is_empty() {
244        return Resolved::Selections(sels.clone());
245    }
246    // A caret after each item (only after the first for one caret, which
247    // may have filled the rest of the document).
248    let after_item = |t: &Target| Selection::caret(changes.map_position(t.range.end, Bias::After));
249    let after = if many {
250        SelectionSet::new(
251            targets.iter().map(after_item).collect(),
252            sels.primary_index(),
253        )
254    } else {
255        SelectionSet::single(after_item(&targets[0]))
256    };
257    Resolved::Edit {
258        changes,
259        after,
260        kind: EditKind::Other,
261    }
262}
263
264#[cfg(test)]
265mod tests {
266    use super::*;
267
268    fn seq(f: impl FnOnce(&mut NumberSequence)) -> NumberSequence {
269        let mut s = NumberSequence::default();
270        f(&mut s);
271        s
272    }
273
274    #[test]
275    fn numbers_count_repeat_and_pad() {
276        assert_eq!(seq(|_| {}).render(3), ["1", "2", "3"]);
277        assert_eq!(
278            seq(|s| {
279                s.initial = 0;
280                s.increase = 5;
281                s.repeat = 2;
282            })
283            .render(5),
284            ["0", "0", "5", "5", "10"]
285        );
286        assert_eq!(
287            seq(|s| {
288                s.initial = 8;
289                s.leading = Leading::Zeros;
290            })
291            .render(3),
292            ["08", "09", "10"]
293        );
294        assert_eq!(
295            seq(|s| {
296                s.initial = 9;
297                s.leading = Leading::Spaces;
298            })
299            .render(2),
300            [" 9", "10"]
301        );
302    }
303
304    #[test]
305    fn number_formats_and_negative_numbers() {
306        let hex = seq(|s| {
307            s.initial = 9;
308            s.format = NumberFormat::Hex;
309        });
310        assert_eq!(hex.render(3), ["9", "A", "B"]);
311        let bin = seq(|s| {
312            s.initial = 1;
313            s.format = NumberFormat::Bin;
314            s.leading = Leading::Zeros;
315        });
316        assert_eq!(bin.render(3), ["01", "10", "11"]);
317        let oct = seq(|s| {
318            s.initial = 7;
319            s.format = NumberFormat::Oct;
320        });
321        assert_eq!(oct.render(2), ["7", "10"]);
322        let down = seq(|s| {
323            s.initial = 1;
324            s.increase = -1;
325            s.leading = Leading::Zeros;
326        });
327        assert_eq!(down.render(3), ["1", "0", "-1"]);
328        let spaced = seq(|s| {
329            s.initial = 1;
330            s.increase = -10;
331            s.leading = Leading::Spaces;
332        });
333        assert_eq!(spaced.render(2), [" 1", "-9"]);
334        // Repeat 0 counts as 1, and huge steps saturate.
335        let edge = seq(|s| {
336            s.initial = i64::MAX - 1;
337            s.repeat = 0;
338        });
339        assert_eq!(
340            edge.render(3),
341            [
342                (i64::MAX - 1).to_string(),
343                i64::MAX.to_string(),
344                i64::MAX.to_string()
345            ]
346        );
347    }
348}