strop_engine/editor/
occurrence.rs1use strop_core::id::{BufferRevision, DocumentId};
11use strop_core::selection::Selection;
12use strop_core::Range;
13
14use super::{Editor, Mode};
15
16#[derive(Debug, Clone)]
21pub(crate) struct OccurrenceState {
22 document: DocumentId,
23 revision: BufferRevision,
24 needle: String,
25 added: Vec<(usize, usize)>,
26 skipped: Vec<(usize, usize)>,
28 next_from: usize,
30 wrapped: bool,
31}
32
33enum Candidate {
35 Found((usize, usize), usize),
37 Exhausted,
38}
39
40fn quoted(needle: &str) -> String {
42 format!("\"{}\"", strop_core::layout::printable_text(needle))
43}
44
45impl Editor {
46 pub fn occurrence_next_pub(&mut self) {
50 if self.buf().readonly {
51 self.message = "readonly buffer".into();
52 return;
53 }
54 let Some(mut state) = self.occurrence_session() else {
55 self.occurrence_seed();
57 return;
58 };
59 match self.occurrence_candidate(&mut state) {
60 Candidate::Found(range, next_from) => {
61 state.next_from = next_from;
62 state.added.push(range);
63 let head = self.buf().clamp_boundary(range.1 - 1);
64 self.sels_mut().plant_extra_selection(range.0, head);
65 self.occurrence = Some(state);
66 self.occurrence_message();
67 }
68 Candidate::Exhausted => {
69 let message = format!("no more occurrences of {}", quoted(&state.needle));
70 self.occurrence = Some(state);
71 self.message = message;
72 }
73 }
74 }
75
76 pub fn occurrence_all_pub(&mut self) {
80 if self.buf().readonly {
81 self.message = "readonly buffer".into();
82 return;
83 }
84 let mut state = match self.occurrence_session() {
85 Some(state) => state,
86 None => {
87 self.occurrence_seed();
90 let Some(state) = self.occurrence.take() else {
91 return;
92 };
93 state
94 }
95 };
96 let mut all: Vec<(usize, usize)> = Vec::new();
99 let mut from = 0;
100 while let Some((start, end)) = self.occurrence_literal_from(from, state.needle.as_bytes()) {
101 all.push((start, end));
102 from = end;
103 }
104 let seed = state.added[0];
105 state.added = std::iter::once(seed)
106 .chain(
107 all.iter()
108 .copied()
109 .filter(|range| *range != seed && !state.skipped.contains(range)),
110 )
111 .collect();
112 state.next_from = self.buf().len_bytes();
113 state.wrapped = true;
114 self.sels_mut().collapse_extras();
115 let extras: Vec<(usize, usize)> = state.added.iter().skip(1).copied().collect();
116 for (start, end) in extras {
117 let head = self.buf().clamp_boundary(end - 1);
118 self.sels_mut().plant_extra_selection(start, head);
119 }
120 self.occurrence = Some(state);
121 self.occurrence_message();
122 }
123
124 pub fn occurrence_skip_pub(&mut self) {
127 if self.buf().readonly {
128 self.message = "readonly buffer".into();
129 return;
130 }
131 let Some(mut state) = self.occurrence_session() else {
132 self.occurrence_seed();
134 return;
135 };
136 match self.occurrence_candidate(&mut state) {
137 Candidate::Found(range, next_from) => {
138 state.next_from = next_from;
139 state.skipped.push(range);
140 let selected = state.added.len();
141 self.occurrence = Some(state);
142 self.message = format!("occurrence skipped ({selected} selected)");
143 }
144 Candidate::Exhausted => {
145 let message = format!("no more occurrences of {}", quoted(&state.needle));
146 self.occurrence = Some(state);
147 self.message = message;
148 }
149 }
150 }
151
152 pub fn occurrence_pop_pub(&mut self) {
155 if self
156 .occurrence
157 .as_ref()
158 .is_some_and(|state| !self.occurrence_fresh(state))
159 {
160 self.occurrence = None;
161 }
162 let Some(mut state) = self.occurrence.take() else {
163 self.message = "no occurrence selection".into();
164 return;
165 };
166 let popped = state.added[state.added.len() - 1];
167 if state.added.len() == 1 {
168 self.sels_mut().collapse_primary(popped.0);
170 self.mode = Mode::Normal;
171 self.message = "occurrence selection cleared".into();
172 return;
173 }
174 state.added.pop();
175 state.next_from = popped.0;
176 state.wrapped = false;
177 let head = self.buf().clamp_boundary(popped.1 - 1);
179 self.sels_mut().remove_extra(Selection {
180 anchor: popped.0,
181 head,
182 });
183 self.occurrence = Some(state);
184 self.occurrence_message();
185 }
186
187 fn occurrence_session(&mut self) -> Option<OccurrenceState> {
191 if self
192 .occurrence
193 .as_ref()
194 .is_some_and(|state| self.occurrence_fresh(state))
195 {
196 return self.occurrence.take();
197 }
198 self.occurrence = None;
199 None
200 }
201
202 fn occurrence_fresh(&self, state: &OccurrenceState) -> bool {
206 if state.document != self.current() || state.revision != self.buf().revision() {
207 return false;
208 }
209 let mut current: Vec<(usize, usize)> = std::iter::once(self.sels().primary())
210 .chain(self.extra_selections().iter().copied())
211 .map(|selection| self.selection_span(selection))
212 .collect();
213 current.sort_unstable();
214 let mut added = state.added.clone();
215 added.sort_unstable();
216 current == added
217 }
218
219 fn occurrence_seed(&mut self) {
223 let seed = if self.mode == Mode::Visual && self.anchor() != self.head() {
224 self.visual_range()
226 .map(|range| (self.buf().slice_string(range), range, false))
227 } else {
228 strop_grammar::word_run(self.buf(), self.head()).map(|(start, end)| {
229 (
230 self.buf().slice_string(Range::charwise(start, end)),
231 Range::charwise(start, end),
232 true,
233 )
234 })
235 };
236 let Some((needle, range, restretch)) = seed else {
237 self.message = "no word under cursor".into();
238 return;
239 };
240 if needle.is_empty() {
241 self.message = "empty selection — nothing to match".into();
242 return;
243 }
244 let range = (range.start.get(), range.end.get());
245 if let Some(collection) = self.collections.get(&self.current()) {
248 let inside = collection
249 .excerpts
250 .iter()
251 .any(|e| range.0 >= e.view_start && range.1 <= e.view_end);
252 if !inside {
253 self.message = "not on an editable excerpt".into();
254 return;
255 }
256 }
257 if restretch {
258 let head = self.buf().clamp_boundary(range.1 - 1);
259 self.sels_mut().stretch_primary(range.0, head);
260 }
261 self.sels_mut().collapse_extras();
263 self.mode = Mode::Visual;
264 self.occurrence = Some(OccurrenceState {
265 document: self.current(),
266 revision: self.buf().revision(),
267 needle: needle.clone(),
268 added: vec![range],
269 skipped: Vec::new(),
270 next_from: range.1,
271 wrapped: false,
272 });
273 self.message = format!("1 occurrence of {}", quoted(&needle));
274 }
275
276 fn occurrence_candidate(&self, state: &mut OccurrenceState) -> Candidate {
280 loop {
281 match self.occurrence_literal_from(state.next_from, state.needle.as_bytes()) {
282 Some((start, end)) => {
283 state.next_from = end;
284 if state.added.contains(&(start, end)) || state.skipped.contains(&(start, end))
285 {
286 continue;
287 }
288 return Candidate::Found((start, end), end);
289 }
290 None if !state.wrapped => {
291 state.wrapped = true;
292 state.next_from = 0;
293 }
294 None => return Candidate::Exhausted,
295 }
296 }
297 }
298
299 fn occurrence_message(&mut self) {
301 let Some(state) = &self.occurrence else {
302 return;
303 };
304 let n = state.added.len();
305 self.message = format!(
306 "{n} occurrence{} of {}",
307 if n == 1 { "" } else { "s" },
308 quoted(&state.needle)
309 );
310 }
311}
312
313impl Editor {
314 fn occurrence_literal_from(&self, from: usize, needle: &[u8]) -> Option<(usize, usize)> {
318 let id = self.current();
319 let Some(collection) = self.collections.get(&id) else {
320 return strop_grammar::literal_from(self.buf(), from, needle);
321 };
322 let mut from = from;
323 loop {
324 let (start, end) = strop_grammar::literal_from(self.buf(), from, needle)?;
325 let inside = collection
326 .excerpts
327 .iter()
328 .any(|e| start >= e.view_start && end <= e.view_end);
329 if inside {
330 return Some((start, end));
331 }
332 if end <= from {
333 return None; }
335 from = end;
336 }
337 }
338}