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)]
398pub trait PromptMode: Send + 'static {
399 type ExitWidget: Widget
404 where
405 Self: Sized;
406
407 fn update(&mut self, pa: &mut Pass, text: Text, area: &RwArea) -> Text;
412
413 fn on_switch(&mut self, pa: &mut Pass, text: Text, area: &RwArea) -> Text {
420 text
421 }
422
423 fn before_exit(&mut self, pa: &mut Pass, text: Text, area: &RwArea) {}
429
430 fn prompt(&self) -> Text;
433
434 fn return_handle(&self) -> Option<Handle<dyn Widget>> {
438 None
439 }
440}
441
442#[derive(Default, Clone)]
447pub struct RunCommands;
448
449impl RunCommands {
450 #[allow(clippy::new_ret_no_self)]
452 pub fn new() -> Prompt {
453 Self::call_once();
454 Prompt::new(Self)
455 }
456
457 pub fn new_with(initial: impl ToString) -> Prompt {
459 Self::call_once();
460 Prompt::new_with(Self, initial)
461 }
462
463 fn call_once() {
464 static ONCE: Once = Once::new();
465 ONCE.call_once(|| {
466 form::set_weak("caller.info", "accent.info");
467 form::set_weak("caller.error", "accent.error");
468 form::set_weak("parameter.info", "default.info");
469 form::set_weak("parameter.error", "default.error");
470 });
471 }
472}
473
474impl PromptMode for RunCommands {
475 type ExitWidget = Buffer;
476
477 fn update(&mut self, pa: &mut Pass, mut text: Text, _: &RwArea) -> Text {
478 text.remove_tags(*TAGGER, ..);
479
480 let command = text.to_string();
481 let caller = command.split_whitespace().next();
482 if let Some(caller) = caller {
483 if let Some((ok_ranges, err_range)) = cmd::check_args(pa, &command) {
484 let id = form::id_of!("caller.info");
485 text.insert_tag(*TAGGER, 0..caller.len(), id.to_tag(0));
486
487 let default_id = form::id_of!("parameter.info");
488 for (range, id) in ok_ranges {
489 text.insert_tag(*TAGGER, range, id.unwrap_or(default_id).to_tag(0));
490 }
491 if let Some((range, _)) = err_range {
492 let id = form::id_of!("parameter.error");
493 text.insert_tag(*TAGGER, range, id.to_tag(0));
494 }
495 } else {
496 let id = form::id_of!("caller.error");
497 text.insert_tag(*TAGGER, 0..caller.len(), id.to_tag(0));
498 }
499 }
500
501 text
502 }
503
504 fn before_exit(&mut self, _: &mut Pass, text: Text, _: &RwArea) {
505 let call = text.to_string();
506 if !call.is_empty() {
507 cmd::queue_notify(call);
508 }
509 }
510
511 fn prompt(&self) -> Text {
512 Text::default()
513 }
514}
515
516pub struct IncSearch<I: IncSearcher> {
543 inc: I,
544 orig: Option<(mode::Selections, PrintInfo)>,
545 prev: String,
546}
547
548impl<I: IncSearcher> Clone for IncSearch<I> {
549 fn clone(&self) -> Self {
550 Self {
551 inc: self.inc.clone(),
552 orig: self.orig.clone(),
553 prev: self.prev.clone(),
554 }
555 }
556}
557
558impl<I: IncSearcher> IncSearch<I> {
559 #[allow(clippy::new_ret_no_self)]
562 pub fn new(inc: I) -> Prompt {
563 static ONCE: Once = Once::new();
564 ONCE.call_once(|| {
565 form::set_weak("regex.error", "accent.error");
566 form::set_weak("regex.operator", "operator");
567 form::set_weak("regex.class", "constant");
568 form::set_weak("regex.bracket", "punctuation.bracket");
569 });
570 Prompt::new(Self { inc, orig: None, prev: String::new() })
571 }
572}
573
574impl<I: IncSearcher> PromptMode for IncSearch<I> {
575 type ExitWidget = Buffer;
576
577 fn update(&mut self, pa: &mut Pass, mut text: Text, _: &RwArea) -> Text {
578 let (orig_selections, orig_print_info) = self.orig.as_ref().unwrap();
579 text.remove_tags(*TAGGER, ..);
580
581 let handle = context::current_buffer(pa).clone();
582
583 if text == self.prev {
584 return text;
585 } else {
586 let prev = std::mem::replace(&mut self.prev, text.to_string());
587 hook::queue(SearchUpdated((prev, self.prev.clone())));
588 }
589
590 match Searcher::new(text.to_string()) {
591 Ok(searcher) => {
592 handle.area().set_print_info(pa, orig_print_info.clone());
593 let buffer = handle.write(pa);
594 *buffer.selections_mut() = orig_selections.clone();
595
596 let ast = regex_syntax::ast::parse::Parser::new()
597 .parse(&text.to_string())
598 .unwrap();
599
600 crate::tag_from_ast(*TAGGER, &mut text, &ast);
601
602 if !text.is_empty() {
603 self.inc.search(pa, handle.attach_searcher(searcher));
604 }
605 }
606 Err(err) => {
607 let regex_syntax::Error::Parse(err) = *err else {
608 unreachable!("As far as I can tell, regex_syntax has goofed up");
609 };
610
611 let span = err.span();
612 let id = form::id_of!("regex.error");
613
614 text.insert_tag(*TAGGER, span.start.offset..span.end.offset, id.to_tag(0));
615 }
616 }
617
618 text
619 }
620
621 fn on_switch(&mut self, pa: &mut Pass, text: Text, _: &RwArea) -> Text {
622 let handle = context::current_buffer(pa);
623
624 self.orig = Some((
625 handle.read(pa).selections().clone(),
626 handle.area().get_print_info(pa),
627 ));
628
629 text
630 }
631
632 fn before_exit(&mut self, _: &mut Pass, text: Text, _: &RwArea) {
633 if !text.is_empty() {
634 if let Err(err) = Searcher::new(text.to_string()) {
635 let regex_syntax::Error::Parse(err) = *err else {
636 unreachable!("As far as I can tell, regex_syntax has goofed up");
637 };
638
639 let range = err.span().start.offset..err.span().end.offset;
640 let err = txt!(
641 "[a]{:?}, \"{}\"[prompt.colon]:[] {}",
642 range,
643 text.strs(range).unwrap(),
644 err.kind()
645 );
646
647 context::error!("{err}")
648 } else {
649 hook::queue(SearchPerformed(text.to_string()));
650 }
651 }
652 }
653
654 fn prompt(&self) -> Text {
655 txt!("{}", self.inc.prompt())
656 }
657}
658
659#[derive(Clone, Copy)]
666pub struct PipeSelections;
667
668impl PipeSelections {
669 #[allow(clippy::new_ret_no_self)]
672 pub fn new() -> Prompt {
673 Prompt::new(Self)
674 }
675}
676
677impl PromptMode for PipeSelections {
678 type ExitWidget = Buffer;
679
680 fn update(&mut self, _: &mut Pass, mut text: Text, _: &RwArea) -> Text {
681 fn is_in_path(program: &str) -> bool {
682 if let Ok(path) = std::env::var("PATH") {
683 for p in path.split(":") {
684 let p_str = format!("{p}/{program}");
685 if let Ok(true) = std::fs::exists(p_str) {
686 return true;
687 }
688 }
689 }
690 false
691 }
692
693 text.remove_tags(*TAGGER, ..);
694
695 let command = text.to_string();
696 let Some(caller) = command.split_whitespace().next() else {
697 return text;
698 };
699
700 let args = cmd::ArgsIter::new(&command);
701
702 let (caller_id, args_id) = if is_in_path(caller) {
703 (form::id_of!("caller.info"), form::id_of!("parameter.indo"))
704 } else {
705 (
706 form::id_of!("caller.error"),
707 form::id_of!("parameter.error"),
708 )
709 };
710
711 let c_s = command.len() - command.trim_start().len();
712 text.insert_tag(*TAGGER, c_s..c_s + caller.len(), caller_id.to_tag(0));
713
714 for (_, range) in args {
715 text.insert_tag(*TAGGER, range, args_id.to_tag(0));
716 }
717
718 text
719 }
720
721 fn before_exit(&mut self, pa: &mut Pass, text: Text, _: &RwArea) {
722 use std::process::{Command, Stdio};
723
724 let command = text.to_string();
725 let Some(caller) = command.split_whitespace().next() else {
726 return;
727 };
728
729 let handle = context::current_buffer(pa).clone();
730 handle.edit_all(pa, |mut c| {
731 let Ok(mut child) = Command::new(caller)
732 .args(cmd::ArgsIter::new(&command).map(|(a, _)| a))
733 .stdin(Stdio::piped())
734 .stdout(Stdio::piped())
735 .spawn()
736 else {
737 return;
738 };
739
740 let input: String = c.selection().collect();
741 if let Some(mut stdin) = child.stdin.take() {
742 std::thread::spawn(move || {
743 stdin.write_all(input.as_bytes()).unwrap();
744 });
745 }
746 if let Ok(out) = child.wait_with_output() {
747 let out = String::from_utf8_lossy(&out.stdout);
748 c.set_anchor_if_needed();
749 c.replace(out);
750 }
751 });
752 }
753
754 fn prompt(&self) -> Text {
755 txt!("[prompt]pipe")
756 }
757}
758
759type ModeCloneFn = dyn Fn() -> Box<dyn PromptMode> + Send;