strop_engine/editor/normal/
search.rs1use strop_core::Range;
6use strop_grammar::{self as grammar, Command};
7
8use crate::editor::{Editor, FindPending, LastSearch};
9
10impl Editor {
11 pub(crate) fn note_search(&mut self, cmd: &Command) {
12 match &cmd.target {
13 grammar::Target::Motion(grammar::Motion::Search(query)) => {
14 self.last_search = Some(LastSearch {
15 query: query.clone(),
16 backward: false,
17 });
18 }
19 grammar::Target::Motion(grammar::Motion::SearchBackward(query)) => {
20 self.last_search = Some(LastSearch {
21 query: query.clone(),
22 backward: true,
23 });
24 }
25 grammar::Target::Motion(grammar::Motion::FindChar { ch, till, backward }) => {
26 self.last_find = Some((*ch, *backward, *till));
27 }
28 _ => {}
29 }
30 }
31
32 pub(crate) fn repeat_find(&mut self, reverse: bool) {
35 let Some((ch, backward, till)) = self.last_find else {
36 self.message = "no previous find".into();
37 return;
38 };
39 let backward = backward ^ reverse;
40 let seek = |buf: &strop_core::Buffer, cursor: usize| -> Option<usize> {
42 let landing = |target: usize| {
43 if !till {
44 target
45 } else if backward {
46 buf.ceil_boundary(target + 1)
47 } else {
48 buf.clamp_boundary(target.saturating_sub(1))
49 }
50 };
51 let mut target = grammar::find_character(buf, cursor.into(), ch, backward, 1)?;
52 while landing(target.get()) == cursor {
53 target = grammar::find_character(buf, target, ch, backward, 1)?;
54 }
55 Some(landing(target.get()))
56 };
57 let extras: Vec<usize> = self
58 .extra_selections()
59 .iter()
60 .map(|s| seek(self.buf(), s.head).unwrap_or(s.head))
61 .collect();
62 self.sels_mut().set_extras(extras);
63 match seek(self.buf(), self.head()) {
64 Some(h) => {
65 self.set_head(h);
66 self.flash(Range::charwise(self.head(), self.head()));
67 }
68 None => self.message = "find: no more matches".into(),
69 }
70 self.normalize_cursors();
71 }
72
73 pub(crate) fn repeat_search(&mut self, invert: bool) {
78 let Some(search) = self.last_search.clone() else {
79 self.message = "no previous search".into();
80 return;
81 };
82 let motion = if search.backward ^ invert {
83 grammar::Motion::SearchBackward(search.query.clone())
84 } else {
85 grammar::Motion::Search(search.query.clone())
86 };
87 let command = grammar::Command {
88 op: None,
89 register: None,
90 count: None,
91 target: grammar::Target::Motion(motion),
92 keys: if invert { "N" } else { "n" }.into(),
93 };
94 if self.defer_resolution(
95 &command,
96 self.all_cursors(),
97 super::super::resolution::ResolutionPurpose::RepeatSearch(invert),
98 ) {
99 return;
100 }
101 self.move_cursor(&command);
102 self.last_search = Some(search);
104 }
105
106 pub(crate) fn search_word_under_cursor(&mut self, backward: bool) {
109 let word_char = |c: char| c.is_alphanumeric() || c == '_';
112 let buf_len = self.buf().len_bytes();
113 let head = self.buf().clamp_boundary(self.head());
114 if head >= buf_len {
115 self.message = "no word under cursor".into();
116 return;
117 }
118 let char_at = |position: usize| -> Option<char> {
119 (position < buf_len).then(|| {
120 self.buf()
121 .text()
122 .char(self.buf().text().byte_to_char(position))
123 })
124 };
125 if !char_at(head).is_some_and(word_char) {
126 self.message = "no word under cursor".into();
127 return;
128 }
129 let mut start = head;
130 while start > 0 {
131 let prev = self.buf().clamp_boundary(start - 1);
132 if char_at(prev).is_some_and(word_char) {
133 start = prev;
134 } else {
135 break;
136 }
137 }
138 let mut end = head;
139 while end < buf_len {
140 match char_at(end) {
141 Some(ch) if word_char(ch) => end += ch.len_utf8(),
142 _ => break,
143 }
144 }
145 let pattern = self.buf().text().byte_slice(start..end).to_string();
146 let query = match grammar::CompiledQuery::compile(&pattern, true) {
147 Ok(query) => query,
148 Err(error) => {
149 self.message = error.to_string();
150 return;
151 }
152 };
153 self.last_search = Some(LastSearch { query, backward });
154 if backward {
157 self.set_head(start);
158 }
159 self.repeat_search(false);
160 }
161
162 pub fn find_candidates(&self) -> Option<FindPending> {
167 let m = self.walker.pending_motion();
168 let ch = m.chars().next()?;
169 (m.chars().count() == 1 && matches!(ch, 'f' | 'F' | 't' | 'T')).then_some(FindPending {
170 ch,
171 backward: matches!(ch, 'F' | 'T'),
172 })
173 }
174
175 pub fn current_search_query(
178 &self,
179 ) -> Result<Option<grammar::CompiledQuery>, grammar::QueryError> {
180 if let Some(pattern) = self.search_pattern() {
181 grammar::CompiledQuery::compile(pattern, false).map(Some)
182 } else {
183 Ok(self.last_search.as_ref().map(|search| search.query.clone()))
184 }
185 }
186}