strop_engine/editor/normal/
pending.rs1use strop_core::Range;
7use strop_grammar::{self as grammar, Command, Parse};
8
9use crate::editor::pending::{
10 PendingEffect, PendingEvent, PromptContext, SearchOrigin, TextPrompt,
11};
12use crate::editor::{input::ParserState, Editor, Key, Mode};
13
14impl Editor {
15 pub(crate) fn begin_text_line(&mut self, sigil: char, state: ParserState) {
18 self.cancel_pending();
19 let origin = SearchOrigin {
20 pane_index: self.active_pane,
21 pane: self.view().clone(),
22 revision: self.buf().revision(),
23 };
24 let context = match sigil {
25 ':' => PromptContext::Ex(origin),
26 '/' | '?' => PromptContext::Search {
27 origin,
28 state,
29 backward: sigil == '?',
30 },
31 '|' => {
32 if self.buf().readonly {
33 self.message = "readonly buffer".into();
34 return;
35 }
36 let visual = matches!(
37 self.mode,
38 Mode::Visual | Mode::VisualLine | Mode::VisualBlock
39 );
40 let range = if visual {
41 let Some(range) = self.visual_range() else {
42 return;
43 };
44 range
45 } else {
46 let line = self.buf().line_of(self.head());
47 Range::charwise(
48 self.buf().line_start(line),
49 self.buf().line_start(line + 1).min(self.buf().len_bytes()),
50 )
51 };
52 PromptContext::Pipe {
53 origin,
54 range,
55 visual,
56 }
57 }
58 _ => unreachable!("Walker only emits text-line sigils"),
59 };
60 self.pending.open(TextPrompt::new(context));
61 }
62
63 pub(crate) fn pending_origin_valid(&self, origin: &SearchOrigin) -> bool {
67 self.active_pane == origin.pane_index
68 && self
69 .panes
70 .get(origin.pane_index)
71 .is_some_and(|p| p.doc == origin.pane.doc)
72 && self
73 .docs
74 .get(origin.pane.doc)
75 .is_some_and(|d| d.buf.revision() == origin.revision)
76 }
77
78 fn restore_prompt_origin(&mut self, origin: &SearchOrigin) -> bool {
79 if !self.pending_origin_valid(origin) {
80 return false;
81 }
82 self.panes[origin.pane_index] = origin.pane.clone();
85 true
86 }
87
88 pub(crate) fn cancel_pending(&mut self) {
92 if let PendingEffect::Aborted(prompt) = self.pending.reduce(PendingEvent::Cancel) {
93 self.resolution.cancel_preview();
94 self.restore_prompt_origin(prompt.origin());
95 }
96 }
97
98 pub(crate) fn feed_pending(&mut self, key: Key) {
99 self.feed_pending_event(PendingEvent::Key(key));
100 }
101
102 pub(crate) fn feed_pending_event(&mut self, event: PendingEvent) {
105 if self
106 .pending
107 .prompt()
108 .is_some_and(|p| !self.pending_origin_valid(p.origin()))
109 {
110 self.cancel_pending();
111 self.message = "input cancelled: document or pane changed".into();
112 return;
113 }
114 match self.pending.reduce(event) {
115 PendingEffect::None | PendingEffect::ModeChanged => {}
116 PendingEffect::Edited => self.incsearch_jump(),
117 PendingEffect::CompleteEx => self.ex_tab_complete(),
118 PendingEffect::Repaint => self.needs_repaint = true,
119 PendingEffect::Rejected(error) => self.message = error.into(),
120 PendingEffect::Aborted(prompt) => {
121 self.restore_prompt_origin(prompt.origin());
122 }
123 PendingEffect::Accepted(prompt) => self.accept_prompt(prompt),
124 }
125 }
126
127 fn accept_prompt(&mut self, prompt: TextPrompt) {
128 if !self.restore_prompt_origin(prompt.origin()) {
129 self.message = "input cancelled: document or pane changed".into();
130 return;
131 }
132 match prompt.context() {
133 PromptContext::Ex(_) => self.run_ex(prompt.body()),
134 PromptContext::Pipe { range, visual, .. } => {
135 if self.buf().readonly {
136 self.message = "readonly buffer".into();
137 return;
138 }
139 self.pipe_run(range.start.get(), range.end.get(), prompt.body());
140 if *visual {
141 self.mode = Mode::Normal;
142 }
143 }
144 PromptContext::Search { .. } => {
145 let command = match self.search_prompt_command(&prompt, true) {
146 Ok(Some(command)) => command,
147 Ok(None) => return,
148 Err(error) => {
149 self.message = error;
150 return;
151 }
152 };
153 if self.defer_resolution(
154 &command,
155 self.all_cursors(),
156 super::super::resolution::ResolutionPurpose::Execute,
157 ) {
158 return;
159 }
160 if let Err(error) = self.search_prompt_heads(&prompt, &command) {
164 self.message = error;
165 return;
166 }
167 self.dispatch_grammar(&command);
168 }
169 }
170 }
171
172 pub(crate) fn search_prompt_command(
177 &self,
178 prompt: &TextPrompt,
179 repeat_empty: bool,
180 ) -> Result<Option<Command>, String> {
181 let Some((_, state)) = prompt.search() else {
182 return Ok(None);
183 };
184 let query = if prompt.body().is_empty() {
185 if !repeat_empty {
186 return Ok(None);
187 }
188 self.last_search
189 .as_ref()
190 .ok_or_else(|| "no previous search".to_string())?
191 .query
192 .clone()
193 } else {
194 grammar::CompiledQuery::compile(prompt.body(), false).map_err(|e| e.to_string())?
195 };
196 let target = if prompt.backward() == Some(true) {
197 grammar::Motion::SearchBackward(query)
198 } else {
199 grammar::Motion::Search(query)
200 };
201 Ok(Some(Command {
202 op: state.op,
203 register: state.register,
204 count: state.count(),
205 target: grammar::Target::Motion(target),
206 keys: String::new(),
208 }))
209 }
210
211 fn search_prompt_heads(
215 &self,
216 prompt: &TextPrompt,
217 command: &Command,
218 ) -> Result<Vec<usize>, String> {
219 let (origin, _) = prompt.search().expect("search context");
220 let heads = origin.pane.sels.heads();
221 let resolved = self.resolved_many(command, &heads)?;
222 Ok(heads
223 .into_iter()
224 .zip(resolved)
225 .map(|(head, hit)| {
226 hit.map_or(head, |hit| {
227 self.clamp_pos(grammar::cursor_after(self.buf(), head, command, &hit))
228 })
229 })
230 .collect())
231 }
232
233 pub(crate) fn incsearch_jump(&mut self) {
238 let Some(prompt) = self.pending.prompt().cloned() else {
239 return;
240 };
241 let Some((origin, _)) = prompt.search() else {
242 return;
243 };
244 if !self.pending_origin_valid(origin) {
245 return;
246 }
247 let command = self.search_prompt_command(&prompt, false);
248 self.restore_prompt_origin(origin);
249 let command = match command {
250 Ok(Some(command)) => command,
251 Ok(None) => {
252 self.resolution.cancel_preview();
253 return;
254 }
255 Err(error) => {
256 self.resolution.cancel_preview();
257 self.message = error;
258 return;
259 }
260 };
261 if self.defer_resolution(
262 &command,
263 origin.pane.sels.heads(),
264 super::super::resolution::ResolutionPurpose::IncSearch,
265 ) {
266 return;
267 }
268 match self.search_prompt_heads(&prompt, &command) {
269 Ok(heads) => {
270 let mut heads = heads.into_iter();
271 self.set_head(heads.next().expect("primary selection"));
272 self.sels_mut().set_extras(heads);
273 self.clamp_cursor();
274 }
275 Err(error) => self.message = error,
276 }
277 }
278
279 pub fn search_pattern(&self) -> Option<&str> {
282 let prompt = self.pending.prompt()?;
283 prompt.search()?;
284 (!prompt.body().is_empty()).then_some(prompt.body())
285 }
286
287 pub fn enter_pub(&mut self) {
290 if self.dive_from_blame() {
291 return;
292 }
293 let n = self.walker.state.count1.unwrap_or(1);
294 let line = (self.buf().line_of(self.head()) + n).min(self.buf().last_content_line());
295 let s = self.buf().line_start(line);
296 let e = self.buf().line_end(line);
297 let mut p = s;
298 while p < e
299 && self
300 .buf()
301 .byte_at(p)
302 .is_some_and(|b| b == b' ' || b == b'\t')
303 {
304 p += 1;
305 }
306 self.set_head(p.min(e));
307 self.clamp_cursor();
308 }
309
310 pub fn run_motion(&mut self, keys: &str) {
311 match grammar::parse(keys) {
312 Parse::Complete(cmd) => self.move_cursor(&cmd),
313 Parse::QueryError(error) => self.message = error.to_string(),
314 Parse::Incomplete | Parse::Invalid => {}
315 }
316 }
317}