1use std::{
26 any::TypeId,
27 io::Write,
28 sync::{Arc, LazyLock, Mutex, Once},
29};
30
31use duat_core::{
32 buffer::Buffer,
33 cmd,
34 context::{self, Handle},
35 data::Pass,
36 form, hook,
37 mode::{self, KeyEvent, event},
38 text::{Ghost, Searcher, Tagger, Text, txt},
39 ui::{PrintInfo, RwArea, Widget},
40};
41
42use super::IncSearcher;
43use crate::{
44 hooks::{SearchPerformed, SearchUpdated},
45 widgets::PromptLine,
46};
47
48static HISTORY: Mutex<Vec<(TypeId, Vec<String>)>> = Mutex::new(Vec::new());
49static PROMPT_TAGGER: LazyLock<Tagger> = LazyLock::new(Tagger::new);
50static TAGGER: LazyLock<Tagger> = LazyLock::new(Tagger::new);
51static PREVIEW_TAGGER: LazyLock<Tagger> = LazyLock::new(Tagger::new);
52
53pub struct Prompt {
77 mode: Box<dyn PromptMode>,
78 starting_text: String,
79 ty: TypeId,
80 clone_fn: Arc<Mutex<ModeCloneFn>>,
81 reset_fn: fn(),
82 history_index: Option<usize>,
83}
84
85impl Prompt {
86 pub fn new<M: PromptMode + Clone>(mode: M) -> Self {
92 let clone_fn = Arc::new(Mutex::new({
93 let mode = mode.clone();
94 move || -> Box<dyn PromptMode> { Box::new(mode.clone()) }
95 }));
96
97 Self {
98 mode: Box::new(mode),
99 starting_text: String::new(),
100 ty: TypeId::of::<M>(),
101 clone_fn,
102 reset_fn: mode::reset::<M::ExitWidget>,
103 history_index: None,
104 }
105 }
106
107 pub fn new_with<M: PromptMode + Clone>(mode: M, initial: impl ToString) -> Self {
114 let clone_fn = Arc::new(Mutex::new({
115 let mode = mode.clone();
116 move || -> Box<dyn PromptMode> { Box::new(mode.clone()) }
117 }));
118 Self {
119 mode: Box::new(mode),
120 starting_text: initial.to_string(),
121 ty: TypeId::of::<M>(),
122 clone_fn,
123 reset_fn: mode::reset::<M::ExitWidget>,
124 history_index: None,
125 }
126 }
127
128 fn show_preview(&mut self, pa: &mut Pass, handle: Handle<PromptLine>) {
130 let history = HISTORY.lock().unwrap();
131 if handle.text(pa).is_empty()
132 && let Some((_, ty_history)) = history.iter().find(|(ty, _)| *ty == self.ty)
133 {
134 handle.text_mut(pa).insert_tag_after(
135 *PREVIEW_TAGGER,
136 0,
137 Ghost(txt!("[prompt.preview]{}", ty_history.last().unwrap())),
138 );
139 }
140 }
141}
142
143impl mode::Mode for Prompt {
144 type Widget = PromptLine;
145
146 fn send_key(&mut self, pa: &mut Pass, key: KeyEvent, handle: Handle<Self::Widget>) {
147 use duat_core::mode::KeyCode::*;
148
149 let ty_eq = |&&(ty, _): &&(TypeId, _)| ty == self.ty;
150
151 let mut update = |pa: &mut Pass| {
152 let text = std::mem::take(handle.write(pa).text_mut());
153 let text = self.mode.update(pa, text, handle.area());
154 *handle.write(pa).text_mut() = text;
155 };
156
157 let reset = |prompt: &mut Self| {
158 if let Some(ret_handle) = prompt.mode.return_handle() {
159 mode::reset_to(ret_handle);
160 } else {
161 (prompt.reset_fn)();
162 }
163 };
164
165 handle.text_mut(pa).remove_tags(*PREVIEW_TAGGER, ..);
166
167 match key {
168 event!(Backspace) => {
169 if handle.read(pa).text().is_empty() {
170 handle.write(pa).text_mut().selections_mut().clear();
171
172 update(pa);
173
174 if let Some(ret_handle) = self.mode.return_handle() {
175 mode::reset_to(ret_handle);
176 } else {
177 (self.reset_fn)();
178 }
179 } else {
180 handle.edit_main(pa, |mut c| {
181 c.move_hor(-1);
182 c.set_anchor_if_needed();
183 c.replace("");
184 c.unset_anchor();
185 });
186 update(pa);
187 }
188 }
189 event!(Delete) => {
190 handle.edit_main(pa, |mut c| c.replace(""));
191 update(pa);
192 }
193
194 event!(Char(char)) => {
195 handle.edit_main(pa, |mut c| {
196 c.insert(char);
197 c.move_hor(1);
198 });
199 update(pa);
200 }
201 event!(Left) => {
202 handle.edit_main(pa, |mut c| c.move_hor(-1));
203 update(pa);
204 }
205 event!(Right) => {
206 handle.edit_main(pa, |mut c| c.move_hor(1));
207 update(pa);
208 }
209 event!(Up) => {
210 let history = HISTORY.lock().unwrap();
211 let Some((_, ty_history)) = history.iter().find(ty_eq) else {
212 return;
213 };
214
215 let index = if let Some(index) = &mut self.history_index {
216 *index = index.saturating_sub(1);
217 *index
218 } else {
219 self.history_index = Some(ty_history.len() - 1);
220 ty_history.len() - 1
221 };
222
223 handle.edit_main(pa, |mut c| {
224 c.move_to(..);
225 c.replace(ty_history[index].clone());
226 c.unset_anchor();
227 })
228 }
229 event!(Down) => {
230 let history = HISTORY.lock().unwrap();
231 let Some((_, ty_history)) = history.iter().find(ty_eq) else {
232 return;
233 };
234
235 if let Some(index) = &mut self.history_index {
236 if *index + 1 < ty_history.len() {
237 *index = (*index + 1).min(ty_history.len() - 1);
238
239 handle.edit_main(pa, |mut c| {
240 c.move_to(..);
241 c.replace(ty_history[*index].clone());
242 c.unset_anchor();
243 })
244 } else {
245 self.history_index = None;
246 handle.edit_main(pa, |mut c| {
247 c.move_to(..);
248 c.replace("");
249 c.unset_anchor();
250 })
251 }
252 };
253 }
254
255 event!(Esc) => {
256 handle.edit_main(pa, |mut c| {
257 c.move_to(..);
258 c.replace("");
259 });
260 handle.write(pa).text_mut().selections_mut().clear();
261 update(pa);
262 reset(self);
263 }
264 event!(Enter) => {
265 handle.write(pa).text_mut().selections_mut().clear();
266
267 if handle.text(pa).is_empty() {
268 let history = HISTORY.lock().unwrap();
269 if let Some((_, ty_history)) = history.iter().find(ty_eq) {
270 handle.edit_main(pa, |mut c| {
271 c.move_to(..);
272 c.replace(ty_history.last().unwrap());
273 });
274 }
275 }
276
277 update(pa);
278 reset(self);
279 }
280 _ => {}
281 }
282
283 self.show_preview(pa, handle);
284 }
285
286 fn on_switch(&mut self, pa: &mut Pass, handle: Handle<Self::Widget>) {
287 let text = {
288 let pl = handle.write(pa);
289 *pl.text_mut() = Text::with_default_main_selection();
290 pl.text_mut().replace_range(0..0, &self.starting_text);
291
292 let tag = Ghost(match pl.prompt_of_id(self.ty) {
293 Some(text) => txt!("{text}[prompt.colon]:"),
294 None => txt!("{}[prompt.colon]:", self.mode.prompt()),
295 });
296 pl.text_mut().insert_tag(*PROMPT_TAGGER, 0, tag);
297
298 std::mem::take(pl.text_mut())
299 };
300
301 let text = self.mode.on_switch(pa, text, handle.area());
302
303 *handle.write(pa).text_mut() = text;
304
305 self.show_preview(pa, handle);
306 }
307
308 fn before_exit(&mut self, pa: &mut Pass, handle: Handle<Self::Widget>) {
309 let text = std::mem::take(handle.write(pa).text_mut());
310 if !text.is_empty() {
311 let mut history = HISTORY.lock().unwrap();
312 if let Some((_, ty_history)) = history.iter_mut().find(|(ty, _)| *ty == self.ty) {
313 if ty_history.last().is_none_or(|last| last != text.bytes()) {
314 ty_history.push(text.to_string());
315 }
316 } else {
317 history.push((self.ty, vec![text.to_string()]));
318 }
319 }
320
321 self.mode.before_exit(pa, text, handle.area());
322 }
323}
324
325impl Clone for Prompt {
326 fn clone(&self) -> Self {
327 Self {
328 mode: self.clone_fn.lock().unwrap()(),
329 starting_text: self.starting_text.clone(),
330 ty: self.ty,
331 clone_fn: self.clone_fn.clone(),
332 reset_fn: self.reset_fn,
333 history_index: None,
334 }
335 }
336}
337
338#[allow(unused_variables)]
396pub trait PromptMode: Send + 'static {
397 type ExitWidget: Widget
400 = Buffer
401 where
402 Self: Sized;
403
404 fn update(&mut self, pa: &mut Pass, text: Text, area: &RwArea) -> Text;
409
410 fn on_switch(&mut self, pa: &mut Pass, text: Text, area: &RwArea) -> Text {
417 text
418 }
419
420 fn before_exit(&mut self, pa: &mut Pass, text: Text, area: &RwArea) {}
426
427 fn prompt(&self) -> Text;
430
431 fn return_handle(&self) -> Option<Handle<dyn Widget>> {
435 None
436 }
437}
438
439#[derive(Default, Clone)]
444pub struct RunCommands;
445
446impl RunCommands {
447 #[allow(clippy::new_ret_no_self)]
449 pub fn new() -> Prompt {
450 Self::call_once();
451 Prompt::new(Self)
452 }
453
454 pub fn new_with(initial: impl ToString) -> Prompt {
456 Self::call_once();
457 Prompt::new_with(Self, initial)
458 }
459
460 fn call_once() {
461 static ONCE: Once = Once::new();
462 ONCE.call_once(|| {
463 form::set_weak("caller.info", "accent.info");
464 form::set_weak("caller.error", "accent.error");
465 form::set_weak("parameter.info", "default.info");
466 form::set_weak("parameter.error", "default.error");
467 });
468 }
469}
470
471impl PromptMode for RunCommands {
472 fn update(&mut self, pa: &mut Pass, mut text: Text, _: &RwArea) -> Text {
473 text.remove_tags(*TAGGER, ..);
474
475 let command = text.to_string();
476 let caller = command.split_whitespace().next();
477 if let Some(caller) = caller {
478 if let Some((ok_ranges, err_range)) = cmd::check_args(pa, &command) {
479 let id = form::id_of!("caller.info");
480 text.insert_tag(*TAGGER, 0..caller.len(), id.to_tag(0));
481
482 let default_id = form::id_of!("parameter.info");
483 for (range, id) in ok_ranges {
484 text.insert_tag(*TAGGER, range, id.unwrap_or(default_id).to_tag(0));
485 }
486 if let Some((range, _)) = err_range {
487 let id = form::id_of!("parameter.error");
488 text.insert_tag(*TAGGER, range, id.to_tag(0));
489 }
490 } else {
491 let id = form::id_of!("caller.error");
492 text.insert_tag(*TAGGER, 0..caller.len(), id.to_tag(0));
493 }
494 }
495
496 text
497 }
498
499 fn before_exit(&mut self, _: &mut Pass, text: Text, _: &RwArea) {
500 let call = text.to_string();
501 if !call.is_empty() {
502 cmd::queue_notify(call);
503 }
504 }
505
506 fn prompt(&self) -> Text {
507 Text::default()
508 }
509}
510
511pub struct IncSearch<I: IncSearcher> {
538 inc: I,
539 orig: Option<(mode::Selections, PrintInfo)>,
540 prev: String,
541}
542
543impl<I: IncSearcher> Clone for IncSearch<I> {
544 fn clone(&self) -> Self {
545 Self {
546 inc: self.inc.clone(),
547 orig: self.orig.clone(),
548 prev: self.prev.clone(),
549 }
550 }
551}
552
553impl<I: IncSearcher> IncSearch<I> {
554 #[allow(clippy::new_ret_no_self)]
557 pub fn new(inc: I) -> Prompt {
558 static ONCE: Once = Once::new();
559 ONCE.call_once(|| {
560 form::set_weak("regex.error", "accent.error");
561 form::set_weak("regex.operator", "operator");
562 form::set_weak("regex.class", "constant");
563 form::set_weak("regex.bracket", "punctuation.bracket");
564 });
565 Prompt::new(Self { inc, orig: None, prev: String::new() })
566 }
567}
568
569impl<I: IncSearcher> PromptMode for IncSearch<I> {
570 fn update(&mut self, pa: &mut Pass, mut text: Text, _: &RwArea) -> Text {
571 let (orig_selections, orig_print_info) = self.orig.as_ref().unwrap();
572 text.remove_tags(*TAGGER, ..);
573
574 let handle = context::current_buffer(pa).clone();
575
576 if text == self.prev {
577 return text;
578 } else {
579 let prev = std::mem::replace(&mut self.prev, text.to_string());
580 hook::queue(SearchUpdated((prev, self.prev.clone())));
581 }
582
583 match Searcher::new(text.to_string()) {
584 Ok(searcher) => {
585 handle.area().set_print_info(pa, orig_print_info.clone());
586 let buffer = handle.write(pa);
587 *buffer.selections_mut() = orig_selections.clone();
588
589 let ast = regex_syntax::ast::parse::Parser::new()
590 .parse(&text.to_string())
591 .unwrap();
592
593 crate::tag_from_ast(*TAGGER, &mut text, &ast);
594
595 if !text.is_empty() {
596 self.inc.search(pa, handle.attach_searcher(searcher));
597 }
598 }
599 Err(err) => {
600 let regex_syntax::Error::Parse(err) = *err else {
601 unreachable!("As far as I can tell, regex_syntax has goofed up");
602 };
603
604 let span = err.span();
605 let id = form::id_of!("regex.error");
606
607 text.insert_tag(*TAGGER, span.start.offset..span.end.offset, id.to_tag(0));
608 }
609 }
610
611 text
612 }
613
614 fn on_switch(&mut self, pa: &mut Pass, text: Text, _: &RwArea) -> Text {
615 let handle = context::current_buffer(pa);
616
617 self.orig = Some((
618 handle.read(pa).selections().clone(),
619 handle.area().get_print_info(pa),
620 ));
621
622 text
623 }
624
625 fn before_exit(&mut self, _: &mut Pass, text: Text, _: &RwArea) {
626 if !text.is_empty() {
627 if let Err(err) = Searcher::new(text.to_string()) {
628 let regex_syntax::Error::Parse(err) = *err else {
629 unreachable!("As far as I can tell, regex_syntax has goofed up");
630 };
631
632 let range = err.span().start.offset..err.span().end.offset;
633 let err = txt!(
634 "[a]{:?}, \"{}\"[prompt.colon]:[] {}",
635 range,
636 text.strs(range).unwrap(),
637 err.kind()
638 );
639
640 context::error!("{err}")
641 } else {
642 hook::queue(SearchPerformed(text.to_string()));
643 }
644 }
645 }
646
647 fn prompt(&self) -> Text {
648 txt!("{}", self.inc.prompt())
649 }
650}
651
652#[derive(Clone, Copy)]
659pub struct PipeSelections;
660
661impl PipeSelections {
662 #[allow(clippy::new_ret_no_self)]
665 pub fn new() -> Prompt {
666 Prompt::new(Self)
667 }
668}
669
670impl PromptMode for PipeSelections {
671 fn update(&mut self, _: &mut Pass, mut text: Text, _: &RwArea) -> Text {
672 fn is_in_path(program: &str) -> bool {
673 if let Ok(path) = std::env::var("PATH") {
674 for p in path.split(":") {
675 let p_str = format!("{p}/{program}");
676 if let Ok(true) = std::fs::exists(p_str) {
677 return true;
678 }
679 }
680 }
681 false
682 }
683
684 text.remove_tags(*TAGGER, ..);
685
686 let command = text.to_string();
687 let Some(caller) = command.split_whitespace().next() else {
688 return text;
689 };
690
691 let args = cmd::args_iter(&command);
692
693 let (caller_id, args_id) = if is_in_path(caller) {
694 (form::id_of!("caller.info"), form::id_of!("parameter.indo"))
695 } else {
696 (
697 form::id_of!("caller.error"),
698 form::id_of!("parameter.error"),
699 )
700 };
701
702 let c_s = command.len() - command.trim_start().len();
703 text.insert_tag(*TAGGER, c_s..c_s + caller.len(), caller_id.to_tag(0));
704
705 for (_, range) in args {
706 text.insert_tag(*TAGGER, range, args_id.to_tag(0));
707 }
708
709 text
710 }
711
712 fn before_exit(&mut self, pa: &mut Pass, text: Text, _: &RwArea) {
713 use std::process::{Command, Stdio};
714
715 let command = text.to_string();
716 let Some(caller) = command.split_whitespace().next() else {
717 return;
718 };
719
720 let handle = context::current_buffer(pa).clone();
721 handle.edit_all(pa, |mut c| {
722 let Ok(mut child) = Command::new(caller)
723 .args(cmd::args_iter(&command).map(|(a, _)| a))
724 .stdin(Stdio::piped())
725 .stdout(Stdio::piped())
726 .spawn()
727 else {
728 return;
729 };
730
731 let input: String = c.selection().collect();
732 if let Some(mut stdin) = child.stdin.take() {
733 std::thread::spawn(move || {
734 stdin.write_all(input.as_bytes()).unwrap();
735 });
736 }
737 if let Ok(out) = child.wait_with_output() {
738 let out = String::from_utf8_lossy(&out.stdout);
739 c.set_anchor_if_needed();
740 c.replace(out);
741 }
742 });
743 }
744
745 fn prompt(&self) -> Text {
746 txt!("[prompt]pipe")
747 }
748}
749
750type ModeCloneFn = dyn Fn() -> Box<dyn PromptMode> + Send;