Skip to main content

ochrepad_core/
bookmarks.rs

1//! Bookmarks: marked lines, as in Notepad++ (Search > Bookmark). They
2//! belong to the document, so every view of it shows the same ones, and
3//! changing them is not an undo step. They follow edits the way Scintilla's
4//! line markers do: text inserted at the start of a marked line pushes the
5//! mark down with the line's text, a mark whose line break before it is
6//! deleted joins the line before, and the marks in a block of replaced
7//! lines stay on the same lines as far as the new text reaches (sorting
8//! lines keeps marks where they were).
9
10use crate::action::{Action, ActionError};
11use crate::changeset::{ChangeSet, Edit};
12use crate::history::EditKind;
13use crate::resolve::Resolved;
14use crate::selection::SelectionSet;
15use crate::text::Document;
16
17/// The marked lines of one document, sorted and without repeats.
18#[derive(Clone, Debug, Default, PartialEq, Eq)]
19pub struct Bookmarks {
20    lines: Vec<usize>,
21}
22
23/// How one edit of a change set changed the lines, in the coordinates of
24/// the document before the change: lines `start..=old_end` became
25/// `start..=start + new_lines`.
26#[derive(Clone, Copy, Debug)]
27pub(crate) struct EditLines {
28    pub start: usize,
29    pub old_end: usize,
30    /// Line breaks in the new text.
31    pub new_lines: usize,
32    /// The edit begins at the start of line `start`.
33    pub at_line_start: bool,
34    /// The edit only inserts.
35    pub insertion: bool,
36}
37
38impl Bookmarks {
39    /// The marked lines, in order.
40    pub fn lines(&self) -> &[usize] {
41        &self.lines
42    }
43
44    pub fn is_empty(&self) -> bool {
45        self.lines.is_empty()
46    }
47
48    pub fn len(&self) -> usize {
49        self.lines.len()
50    }
51
52    pub fn contains(&self, line: usize) -> bool {
53        self.lines.binary_search(&line).is_ok()
54    }
55
56    /// Replaces the marks with `lines`; lines past the last of `line_count`
57    /// are dropped.
58    pub fn set(&mut self, lines: impl IntoIterator<Item = usize>, line_count: usize) {
59        let mut lines: Vec<usize> = lines.into_iter().filter(|&l| l < line_count).collect();
60        lines.sort_unstable();
61        lines.dedup();
62        self.lines = lines;
63    }
64
65    /// Marks `line`; returns whether it was unmarked.
66    pub fn insert(&mut self, line: usize) -> bool {
67        match self.lines.binary_search(&line) {
68            Ok(_) => false,
69            Err(i) => {
70                self.lines.insert(i, line);
71                true
72            }
73        }
74    }
75
76    /// Unmarks `line`; returns whether it was marked.
77    pub fn remove(&mut self, line: usize) -> bool {
78        match self.lines.binary_search(&line) {
79            Ok(i) => {
80                self.lines.remove(i);
81                true
82            }
83            Err(_) => false,
84        }
85    }
86
87    /// Marks `line` if it is unmarked, else unmarks it; returns whether it
88    /// is marked now.
89    pub fn toggle(&mut self, line: usize) -> bool {
90        !self.remove(line) && self.insert(line)
91    }
92
93    pub fn clear(&mut self) {
94        self.lines.clear();
95    }
96
97    /// The first marked line after `line`, else the first one (wrapping
98    /// around, as F2 does).
99    pub fn next_after(&self, line: usize) -> Option<usize> {
100        let i = self.lines.partition_point(|&l| l <= line);
101        self.lines.get(i).or(self.lines.first()).copied()
102    }
103
104    /// The last marked line before `line`, else the last one.
105    pub fn prev_before(&self, line: usize) -> Option<usize> {
106        let i = self.lines.partition_point(|&l| l < line);
107        i.checked_sub(1)
108            .and_then(|i| self.lines.get(i))
109            .or(self.lines.last())
110            .copied()
111    }
112
113    /// Marks every unmarked line of a document of `line_count` lines and
114    /// unmarks every marked one.
115    pub fn invert(&mut self, line_count: usize) {
116        let mut marked = self.lines.iter().copied().peekable();
117        let mut inverted = Vec::with_capacity(line_count.saturating_sub(self.lines.len()));
118        for line in 0..line_count {
119            if marked.peek() == Some(&line) {
120                marked.next();
121            } else {
122                inverted.push(line);
123            }
124        }
125        self.lines = inverted;
126    }
127
128    /// Moves the marks through a change set's `edits` (sorted, in the old
129    /// document's lines); `line_count` is the new document's.
130    pub(crate) fn map(&mut self, edits: &[EditLines], line_count: usize) {
131        if self.lines.is_empty() || edits.is_empty() {
132            return;
133        }
134        let mut out = Vec::with_capacity(self.lines.len());
135        // Lines gained by the edits wholly before the current mark.
136        let mut delta: isize = 0;
137        let mut i = 0;
138        for &line in &self.lines {
139            while let Some(e) = edits.get(i).filter(|e| e.old_end < line) {
140                delta += e.new_lines as isize - (e.old_end - e.start) as isize;
141                i += 1;
142            }
143            let new = match edits.get(i) {
144                // Inside a replaced block: the same line of the new text, or
145                // its last line when the new text is shorter.
146                Some(e) if e.start < line => {
147                    e.start as isize + delta + (line - e.start).min(e.new_lines) as isize
148                }
149                // Text inserted at the start of the line goes before it.
150                Some(e) if e.start == line && e.at_line_start && e.insertion => {
151                    (line + e.new_lines) as isize + delta
152                }
153                _ => line as isize + delta,
154            };
155            out.push(new.clamp(0, line_count.saturating_sub(1) as isize) as usize);
156        }
157        out.sort_unstable();
158        out.dedup();
159        self.lines = out;
160    }
161}
162
163/// The marked lines' text, each with its line break (the last line of the
164/// document has none): what Copy Bookmarked Lines puts on the clipboard.
165pub fn marked_text(doc: &Document, marks: &Bookmarks) -> String {
166    let last = doc.line_count() - 1;
167    let mut text = String::new();
168    for &line in marks.lines() {
169        text.push_str(&doc.line_text(line));
170        if line < last {
171            text.push('\n');
172        }
173    }
174    text
175}
176
177/// The lines that hold a selection's head (a caret's line), in order.
178pub(crate) fn caret_lines(doc: &Document, sels: &SelectionSet) -> Vec<usize> {
179    let mut lines: Vec<usize> = sels.ranges().iter().map(|s| doc.line_of(s.head)).collect();
180    lines.dedup();
181    lines
182}
183
184/// Resolves the bookmark actions that move the caret or edit the text.
185pub(crate) fn resolve(
186    action: &Action,
187    doc: &Document,
188    sels: &SelectionSet,
189    marks: &Bookmarks,
190) -> Result<Resolved, ActionError> {
191    if marks.is_empty() {
192        return Err(ActionError::NoBookmarks);
193    }
194    let caret = doc.line_of(sels.primary().head);
195    let go = |line: Option<usize>| {
196        let line = line.ok_or(ActionError::NoBookmarks)?;
197        Ok(Resolved::Selections(SelectionSet::caret(
198            doc.line_start(line),
199        )))
200    };
201    match action {
202        Action::NextBookmark => go(marks.next_after(caret)),
203        Action::PrevBookmark => go(marks.prev_before(caret)),
204        Action::RemoveBookmarkedLines => {
205            Ok(crate::transform::remove_lines(doc, sels, marks.lines()))
206        }
207        Action::RemoveUnbookmarkedLines => {
208            // The empty line after a final line break is not a line of its
209            // own: removing it would take the file's last line break.
210            let last = doc.line_count() - 1;
211            let count = if last > 0 && doc.line_start(last) == doc.len() {
212                last
213            } else {
214                last + 1
215            };
216            let mut unmarked = marks.clone();
217            unmarked.invert(count);
218            Ok(crate::transform::remove_lines(doc, sels, unmarked.lines()))
219        }
220        Action::ReplaceBookmarkedLines(text) => {
221            let edits: Vec<Edit> = marks
222                .lines()
223                .iter()
224                .map(|&l| Edit::new(doc.line_start(l), doc.line_end(l), text.as_str()))
225                .collect();
226            let changes = ChangeSet::new(doc.len(), edits);
227            let after = SelectionSet::caret(
228                changes.map_position(sels.primary().head, crate::changeset::Bias::Before),
229            );
230            Ok(Resolved::Edit {
231                changes,
232                after,
233                kind: EditKind::Other,
234            })
235        }
236        _ => Ok(Resolved::Selections(sels.clone())),
237    }
238}
239
240/// The marks after Paste to (Replace) Bookmarked Lines put `text` on each
241/// of `before`: the first line of each replacement.
242pub(crate) fn after_replace(before: &[usize], text: &str) -> Vec<usize> {
243    let breaks = text.bytes().filter(|&b| b == b'\n').count();
244    before
245        .iter()
246        .enumerate()
247        .map(|(i, &l)| l + i * breaks)
248        .collect()
249}
250
251#[cfg(test)]
252mod tests {
253    use super::*;
254
255    fn marks(lines: &[usize]) -> Bookmarks {
256        let mut b = Bookmarks::default();
257        b.set(lines.iter().copied(), usize::MAX);
258        b
259    }
260
261    #[test]
262    fn toggle_insert_remove_and_set() {
263        let mut b = Bookmarks::default();
264        assert!(b.toggle(3));
265        assert!(b.toggle(1));
266        assert_eq!(b.lines(), &[1, 3]);
267        assert!(!b.toggle(3));
268        assert_eq!(b.lines(), &[1]);
269        assert!(!b.insert(1));
270        assert!(b.remove(1));
271        assert!(b.is_empty());
272        b.set([5, 2, 2, 9], 6);
273        assert_eq!(b.lines(), &[2, 5]);
274    }
275
276    #[test]
277    fn next_and_previous_wrap_around() {
278        let b = marks(&[2, 5, 9]);
279        assert_eq!(b.next_after(0), Some(2));
280        assert_eq!(b.next_after(2), Some(5));
281        assert_eq!(b.next_after(9), Some(2));
282        assert_eq!(b.prev_before(5), Some(2));
283        assert_eq!(b.prev_before(2), Some(9));
284        assert_eq!(b.prev_before(100), Some(9));
285        assert_eq!(Bookmarks::default().next_after(0), None);
286    }
287
288    #[test]
289    fn invert_marks_the_other_lines() {
290        let mut b = marks(&[0, 2]);
291        b.invert(5);
292        assert_eq!(b.lines(), &[1, 3, 4]);
293        b.invert(5);
294        assert_eq!(b.lines(), &[0, 2]);
295    }
296
297    #[test]
298    fn marked_text_keeps_line_breaks_except_at_the_end() {
299        let doc = Document::new("a\nb\nc");
300        assert_eq!(marked_text(&doc, &marks(&[0, 2])), "a\nc");
301        assert_eq!(marked_text(&doc, &marks(&[1])), "b\n");
302    }
303
304    #[test]
305    fn after_replace_counts_the_inserted_lines() {
306        assert_eq!(after_replace(&[1, 4], "x\ny"), vec![1, 5]);
307        assert_eq!(after_replace(&[1, 4], "x"), vec![1, 4]);
308    }
309}