1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
//! In-memory position memory for recently visited notes.
//!
//! Remembers where the user was — both the scroll offset and the caret
//! position — in the last few notes they left, so navigating back to one — via
//! a link or the picker, not just the back/forward history — resumes where they
//! were instead of jumping to the top with the caret reset. This is
//! deliberately not persisted: it only needs to survive within a session.
use rutle::tree_path::DocumentPosition;
/// How many notes' positions are retained.
const CAPACITY: usize = 10;
/// Where the user was in a note: the scroll offset and, when known, the caret
/// position. `cursor` is `None` for a position captured from an editor without a
/// caret (e.g. a read-only plugin view); restoring it then leaves the caret at
/// the document start.
#[derive(Debug, Clone, Default, PartialEq)]
pub struct NotePosition {
pub scroll: i32,
pub cursor: Option<DocumentPosition>,
}
#[derive(Default)]
pub struct PositionMemory {
/// (note name, position), most-recently-remembered first.
entries: Vec<(String, NotePosition)>,
}
impl PositionMemory {
pub fn new() -> Self {
Self::default()
}
/// Record `pos` for `note`, promoting it to most-recent and evicting the
/// least-recently-remembered note once more than [`CAPACITY`] are tracked.
pub fn remember(&mut self, note: &str, pos: NotePosition) {
self.entries.retain(|(name, _)| name != note);
self.entries.insert(0, (note.to_string(), pos));
self.entries.truncate(CAPACITY);
}
/// The remembered position for `note`, if it is still tracked.
pub fn get(&self, note: &str) -> Option<NotePosition> {
self.entries
.iter()
.find(|(name, _)| name == note)
.map(|(_, pos)| pos.clone())
}
/// Rename a tracked note in place (used when a note is renamed), preserving
/// its remembered position and recency. No-op if `old` is not tracked.
pub fn rename(&mut self, old: &str, new: &str) {
if let Some((name, _)) = self.entries.iter_mut().find(|(name, _)| name == old) {
*name = new.to_string();
}
}
/// Stop tracking `note`'s position (used when a note is deleted). No-op if
/// it is not tracked.
pub fn remove(&mut self, note: &str) {
self.entries.retain(|(name, _)| name != note);
}
}
#[cfg(test)]
mod tests {
use super::*;
/// A scroll-only position, for the tests that only exercise recency/eviction.
fn at(scroll: i32) -> NotePosition {
NotePosition {
scroll,
cursor: None,
}
}
#[test]
fn remembers_and_returns_position() {
let mut m = PositionMemory::new();
assert_eq!(m.get("a"), None);
m.remember("a", at(42));
assert_eq!(m.get("a"), Some(at(42)));
}
#[test]
fn remembers_cursor_alongside_scroll() {
let mut m = PositionMemory::new();
let pos = NotePosition {
scroll: 10,
cursor: Some(DocumentPosition::new(2, 5)),
};
m.remember("a", pos.clone());
assert_eq!(m.get("a"), Some(pos));
}
#[test]
fn updates_existing_position() {
let mut m = PositionMemory::new();
m.remember("a", at(10));
m.remember("a", at(99));
assert_eq!(m.get("a"), Some(at(99)));
}
#[test]
fn evicts_least_recently_remembered_beyond_capacity() {
let mut m = PositionMemory::new();
for i in 0..CAPACITY {
m.remember(&format!("p{i}"), at(i as i32));
}
// All CAPACITY notes are still tracked.
assert_eq!(m.get("p0"), Some(at(0)));
// One more evicts the oldest ("p0"); the newest survives.
m.remember("new", at(123));
assert_eq!(m.get("p0"), None);
assert_eq!(m.get("new"), Some(at(123)));
assert_eq!(m.get("p1"), Some(at(1)));
}
#[test]
fn rename_preserves_position() {
let mut m = PositionMemory::new();
m.remember("old", at(42));
m.rename("old", "new");
assert_eq!(m.get("old"), None);
assert_eq!(m.get("new"), Some(at(42)));
}
#[test]
fn rename_unknown_note_is_noop() {
let mut m = PositionMemory::new();
m.remember("a", at(1));
m.rename("missing", "new");
assert_eq!(m.get("new"), None);
assert_eq!(m.get("a"), Some(at(1)));
}
#[test]
fn re_remembering_refreshes_recency() {
let mut m = PositionMemory::new();
for i in 0..CAPACITY {
m.remember(&format!("p{i}"), at(i as i32));
}
// Touch the oldest so it is no longer the eviction candidate.
m.remember("p0", at(7));
m.remember("new", at(1));
assert_eq!(m.get("p0"), Some(at(7))); // survived
assert_eq!(m.get("p1"), None); // evicted instead
}
}