Skip to main content

collection_selection_confection/
lib.rs

1//! A sketch of a _selection_ API for collections that could live alongside _cursors_.
2//!
3//! The Rust standard library includes unstable cursor APIs for `LinkedList` ([RFC](https://github.com/rust-lang/rfcs/pull/2570), [tracking issue](https://github.com/rust-lang/rust/issues/58533)) and `BTreeMap`/`BTreeSet` ([API change proposal](https://github.com/rust-lang/libs-team/issues/141), [tracking issue](https://github.com/rust-lang/rust/issues/107540)).
4//! There has been a lot of bikeshedding over the years about what exactly these interfaces should look like.
5//! One point of contention has been whether a cursor should point to _elements_ in a collection, or _gaps_ between those elements.
6//! The current APIs in `std` have gone in opposite directions here: `LinkedList` cursors point at elements, whereas `BTreeMap` cursors were [rewritten](https://github.com/rust-lang/rust/pull/118208) to point at gaps instead.
7//!
8//! I've personally leaned toward the "gaps" formulation if we can only have one.
9//! At some point I also proposed having two separate cursor types and converting between them as you navigate, e.g., calling `next_gap(self)` on an `ElemCursor` would always successfully return a `GapCursor`.
10//! But nobody liked that idea. :)
11//!
12//! More recently I've been thinking about how useful it could be to instead have a separate concept of collection _selections_, analogous to selections in a text editor.
13//! A selection is:
14//!
15//! - Like a cursor in that you can move its start and end, and insert, modify, and delete elements around those endpoints.
16//! - Like a range iterator in that it allows you to iterate over some subset of a collection.
17//! - Like `extract_if` (formerly `drain_filter`), `split_off`, etc. in that it could allow efficiently removing or splitting off part(s) of a collection.
18//!
19//! A cursor can be turned into an empty selection, and a selection can be turned into a cursor at either of its endpoints (for a mutable selection you have to pick one, since it consumes the selection).
20//!
21//! I can imagine it being particularly useful for `BTreeMap`, but the sketch in this crate uses a fake (`Vec`-backed) linked list for simplicity, to focus on the API shape rather than implementation details.
22//!
23//! # Examples
24//!
25//! Moving a paragraph down past the next one, like a text editor's "move block down" command:
26//!
27//! ```
28//! # use collection_selection_confection::LinkedList;
29//! # let mut lines = LinkedList::new();
30//! # let mut cursor = lines.cursor_mut_at_start();
31//! # for line in ["Roses are red,", "violets are blue.", "", "Sugar is sweet,", "and so are you."] {
32//! #     cursor.insert_before(line.to_string());
33//! # }
34//! // lines:
35//! //     Roses are red,
36//! //     violets are blue.
37//! //
38//! //     Sugar is sweet,
39//! //     and so are you.
40//!
41//! // Select the first paragraph and cut it, along with the blank line after it.
42//! let mut selection = lines.cursor_mut_at_start().into_selection();
43//! while selection.peek_after_end().is_some_and(|line| !line.is_empty()) {
44//!     selection.move_end_next();
45//! }
46//! let paragraph = selection.take();
47//! selection.remove_after_end();
48//!
49//! // Move past the next paragraph and paste it there.
50//! let mut cursor = selection.into_cursor_at_end();
51//! while cursor.peek_after().is_some_and(|line| !line.is_empty()) {
52//!     cursor.move_next();
53//! }
54//! cursor.insert_before(String::new());
55//! cursor.splice_before(paragraph);
56//!
57//! // lines:
58//! //     Sugar is sweet,
59//! //     and so are you.
60//! //
61//! //     Roses are red,
62//! //     violets are blue.
63//! # assert_eq!(
64//! #     lines.iter().map(String::as_str).collect::<Vec<_>>(),
65//! #     ["Sugar is sweet,", "and so are you.", "", "Roses are red,", "violets are blue."],
66//! # );
67//! ```
68//!
69//! Unwrapping hard-wrapped paragraphs into one line each, switching back and forth between a cursor and a selection:
70//!
71//! ```
72//! # use collection_selection_confection::LinkedList;
73//! # let mut lines = LinkedList::new();
74//! # let mut cursor = lines.cursor_mut_at_start();
75//! # for line in ["Roses are red,", "violets are blue.", "", "Sugar is sweet,", "and so are you."] {
76//! #     cursor.insert_before(line.to_string());
77//! # }
78//! // lines:
79//! //     Roses are red,
80//! //     violets are blue.
81//! //
82//! //     Sugar is sweet,
83//! //     and so are you.
84//!
85//! let mut cursor = lines.cursor_mut_at_start();
86//! loop {
87//!     // Select the next paragraph.
88//!     let mut selection = cursor.into_selection();
89//!     while selection.peek_after_end().is_some_and(|line| !line.is_empty()) {
90//!         selection.move_end_next();
91//!     }
92//!
93//!     // Replace it with a single line.
94//!     let paragraph = selection.take();
95//!     if !paragraph.is_empty() {
96//!         let joined = paragraph.iter().map(String::as_str).collect::<Vec<_>>().join(" ");
97//!         selection.insert_at_start(joined);
98//!     }
99//!
100//!     // Step over the blank line, or stop if we're at the end.
101//!     cursor = selection.into_cursor_at_end();
102//!     if !cursor.move_next() {
103//!         break;
104//!     }
105//! }
106//!
107//! // lines:
108//! //     Roses are red, violets are blue.
109//! //
110//! //     Sugar is sweet, and so are you.
111//! # assert_eq!(
112//! #     lines.iter().map(String::as_str).collect::<Vec<_>>(),
113//! #     ["Roses are red, violets are blue.", "", "Sugar is sweet, and so are you."],
114//! # );
115//! ```
116mod list;
117
118pub use list::{CursorMut, LinkedList, SelectionMut};