1use std::cmp::{Ord, Ordering, PartialOrd};
32use std::marker::PhantomData;
33
34use regex::Regex;
35
36use ratatui::{
37 buffer::Buffer,
38 layout::{Alignment, Rect},
39 style::{Modifier as StyleModifier, Style},
40 text::Text,
41 widgets::{Paragraph, StatefulWidget, Widget},
42};
43
44use modalkit::actions::*;
45use modalkit::editing::{
46 application::ApplicationInfo,
47 completion::CompletionList,
48 context::{EditContext, Resolve},
49 cursor::{Cursor, CursorGroup, CursorState},
50 history::HistoryList,
51 rope::EditRope,
52 store::{RegisterCell, RegisterPutFlags, Store},
53};
54use modalkit::errors::{EditError, EditResult, UIError, UIResult};
55use modalkit::prelude::*;
56use modalkit::ui::idx_offset;
57
58use super::{ScrollActions, TerminalCursor, WindowOps};
59
60fn _clamp_cursor(cursor: &mut ListCursor, len: usize) {
61 let max = len.saturating_sub(1);
62
63 if cursor.position <= max {
64 return;
65 }
66
67 cursor.position = max;
68 cursor.text_row = 0;
69}
70
71#[derive(Clone, Debug, Default, Eq, PartialEq)]
73pub struct ListCursor {
74 pub position: usize,
76
77 pub text_row: usize,
79}
80
81impl ListCursor {
82 pub fn new(position: usize, text_row: usize) -> Self {
84 ListCursor { position, text_row }
85 }
86}
87
88impl Ord for ListCursor {
89 fn cmp(&self, other: &Self) -> Ordering {
90 let pcmp = self.position.cmp(&other.position);
91 let tcmp = self.text_row.cmp(&other.text_row);
92
93 pcmp.then(tcmp)
94 }
95}
96
97impl PartialOrd for ListCursor {
98 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
99 Some(self.cmp(other))
100 }
101}
102
103impl From<usize> for ListCursor {
104 fn from(position: usize) -> Self {
105 ListCursor { position, text_row: 0 }
106 }
107}
108
109pub trait ListItem<I>: Clone + ToString
111where
112 I: ApplicationInfo,
113{
114 fn show(
116 &self,
117 selected: bool,
118 viewport: &ViewportContext<ListCursor>,
119 store: &mut Store<I>,
120 ) -> Text;
121
122 fn get_word(&self) -> Option<String> {
127 self.to_string().into()
128 }
129
130 fn matches(&self, needle: &Regex) -> bool {
132 let s = self.to_string();
133 needle.is_match(s.as_str())
134 }
135}
136
137impl<I> ListItem<I> for String
138where
139 I: ApplicationInfo,
140{
141 fn show(&self, selected: bool, _: &ViewportContext<ListCursor>, _: &mut Store<I>) -> Text {
142 if selected {
143 let hl = Style::default().add_modifier(StyleModifier::REVERSED);
144
145 Text::styled(self.as_str(), hl)
146 } else {
147 Text::raw(self.as_str())
148 }
149 }
150}
151
152pub struct ListState<T, I>
154where
155 T: ListItem<I>,
156 I: ApplicationInfo,
157{
158 id: I::ContentId,
159 items: Vec<T>,
160 cursor: ListCursor,
161 viewctx: ViewportContext<ListCursor>,
162
163 jumped: HistoryList<ListCursor>,
165}
166
167pub struct List<'a, T, I>
169where
170 T: ListItem<I>,
171 I: ApplicationInfo,
172{
173 focused: bool,
174 empty_message: Option<Text<'a>>,
175 empty_alignment: Alignment,
176 store: &'a mut Store<I>,
177 _p: PhantomData<T>,
178}
179
180impl<T, I> ListState<T, I>
181where
182 T: ListItem<I>,
183 I: ApplicationInfo,
184{
185 pub fn new(id: I::ContentId, items: Vec<T>) -> Self {
187 let mut viewctx = ViewportContext::default();
188 viewctx.wrap = true;
189
190 ListState {
191 id,
192 items,
193 cursor: 0.into(),
194 viewctx,
195 jumped: HistoryList::new(0.into(), 100),
196 }
197 }
198
199 pub fn id(&self) -> I::ContentId {
201 self.id.clone()
202 }
203
204 pub fn is_empty(&self) -> bool {
206 self.items.is_empty()
207 }
208
209 pub fn len(&self) -> usize {
211 self.items.len()
212 }
213
214 fn _clamp(&mut self) {
215 _clamp_cursor(&mut self.cursor, self.items.len());
216 }
217
218 fn _range_to(&self, pos: ListCursor) -> EditRange<ListCursor> {
219 EditRange::inclusive(self.cursor.clone(), pos, TargetShape::LineWise)
220 }
221
222 fn scrollview(&mut self, idx: usize, pos: MovePosition, store: &mut Store<I>) {
223 match pos {
224 MovePosition::Beginning => {
225 self.viewctx.corner = idx.into();
226 },
227 MovePosition::Middle => {
228 let mut lines = 0;
229 let target = self.viewctx.get_height() / 2;
230 let selidx = self.cursor.position;
231 let posidx = idx;
232
233 self.viewctx.corner.position = 0;
234 self.viewctx.corner.text_row = 0;
235
236 for (idx, item) in self.items.iter().enumerate().take(idx + 1).rev() {
237 let sel = selidx == idx;
238 let len = item.show(sel, &self.viewctx, store).lines.len();
239
240 if posidx == idx {
241 lines += len / 2;
242 } else {
243 lines += len;
244 }
245
246 if lines >= target {
247 self.viewctx.corner.position = idx;
249 self.viewctx.corner.text_row = lines - target;
250 break;
251 }
252 }
253 },
254 MovePosition::End => {
255 let mut lines = 0;
256 let target = self.viewctx.get_height();
257 let pos = self.cursor.position;
258
259 self.viewctx.corner.position = 0;
260 self.viewctx.corner.text_row = 0;
261
262 for (idx, item) in self.items.iter().enumerate().take(idx + 1).rev() {
263 let sel = idx == pos;
264 let len = item.show(sel, &self.viewctx, store).lines.len();
265
266 lines += len;
267
268 if lines >= target {
269 self.viewctx.corner.position = idx;
271 self.viewctx.corner.text_row = lines - target;
272 break;
273 }
274 }
275 },
276 }
277 }
278
279 fn shift_cursor(&mut self, store: &mut Store<I>) {
281 if self.cursor < self.viewctx.corner {
282 self.cursor = self.viewctx.corner.position.into();
284 return;
285 }
286
287 let mut lines = 0;
289
290 for (idx, item) in self.items.iter().enumerate().skip(self.viewctx.corner.position) {
291 if idx == self.cursor.position {
292 break;
294 }
295
296 lines += item.show(false, &self.viewctx, store).lines.len();
297
298 if lines >= self.viewctx.get_height() {
299 self.cursor = idx.into();
301 break;
302 }
303 }
304 }
305
306 pub fn set(&mut self, items: Vec<T>) {
308 self.items = items;
309 self._clamp();
310 }
311
312 pub fn get(&self) -> Option<&T> {
314 self.items.get(self.cursor.position)
315 }
316
317 pub fn get_mut(&mut self) -> Option<&mut T> {
319 self.items.get_mut(self.cursor.position)
320 }
321
322 pub fn set_term_info(&mut self, area: Rect) {
324 self.viewctx.dimensions = (area.width as usize, area.height as usize);
325 }
326}
327
328impl<T, I> CursorMovements<ListCursor> for ListState<T, I>
329where
330 T: ListItem<I>,
331 I: ApplicationInfo,
332{
333 fn first_word(
334 &self,
335 pos: &ListCursor,
336 _: &CursorMovementsContext<'_, ListCursor>,
337 ) -> ListCursor {
338 pos.clone()
339 }
340
341 fn movement(
342 &self,
343 pos: &ListCursor,
344 movement: &MoveType,
345 count: &Count,
346 ctx: &CursorMovementsContext<'_, ListCursor>,
347 ) -> Option<ListCursor> {
348 let len = self.items.len();
349 let count = ctx.context.resolve(count);
350
351 match movement {
352 MoveType::BufferByteOffset => None,
354 MoveType::Column(_, _) => None,
355 MoveType::ItemMatch => None,
356 MoveType::LineColumnOffset => None,
357 MoveType::LinePercent => None,
358 MoveType::LinePos(_) => None,
359 MoveType::SentenceBegin(_) => None,
360 MoveType::ScreenFirstWord(_) => None,
361 MoveType::ScreenLinePos(_) => None,
362 MoveType::WordBegin(_, _) => None,
363 MoveType::WordEnd(_, _) => None,
364
365 MoveType::BufferLineOffset => {
366 let max = len.saturating_sub(1);
367 let off = count.saturating_sub(1).min(max);
368
369 if off < len {
370 return Some(off.into());
371 } else {
372 return None;
373 }
374 },
375 MoveType::BufferLinePercent => {
376 if count > 100 {
377 return None;
378 }
379
380 let off = len.saturating_mul(count).saturating_add(99) / 100;
382 let off = off.saturating_sub(1);
383
384 if off < len {
385 return Some(off.into());
386 } else {
387 return None;
388 }
389 },
390 MoveType::BufferPos(MovePosition::Beginning) => {
391 if len > 0 {
392 return Some(0.into());
393 } else {
394 return None;
395 }
396 },
397 MoveType::BufferPos(MovePosition::Middle) => {
398 let off = len / 2;
399
400 if off < len {
401 return Some(off.into());
402 } else {
403 return None;
404 }
405 },
406 MoveType::BufferPos(MovePosition::End) => {
407 if len > 0 {
408 return Some(len.saturating_sub(1).into());
409 } else {
410 return None;
411 }
412 },
413 MoveType::FinalNonBlank(dir) |
414 MoveType::FirstWord(dir) |
415 MoveType::Line(dir) |
416 MoveType::ScreenLine(dir) |
417 MoveType::ParagraphBegin(dir) |
418 MoveType::SectionBegin(dir) |
419 MoveType::SectionEnd(dir) => {
420 let pos = pos.position;
421
422 match dir {
423 MoveDir1D::Previous => {
424 return Some(pos.saturating_sub(count).into());
425 },
426 MoveDir1D::Next => {
427 let max = len.saturating_sub(1);
428
429 return Some(pos.saturating_add(count).min(max).into());
430 },
431 };
432 },
433 MoveType::ViewportPos(MovePosition::Beginning) => {
434 return Some(self.viewctx.corner.position.into());
435 },
436 MoveType::ViewportPos(MovePosition::Middle) => {
437 return None;
439 },
440 MoveType::ViewportPos(MovePosition::End) => {
441 return None;
443 },
444 _ => return None,
445 }
446 }
447
448 fn range_of_movement(
449 &self,
450 pos: &ListCursor,
451 movement: &MoveType,
452 count: &Count,
453 ctx: &CursorMovementsContext<'_, ListCursor>,
454 ) -> Option<EditRange<ListCursor>> {
455 let other = self.movement(pos, movement, count, ctx)?;
456
457 Some(EditRange::inclusive(pos.clone(), other, TargetShape::LineWise))
458 }
459
460 fn range(
461 &self,
462 pos: &ListCursor,
463 range: &RangeType,
464 _: bool,
465 count: &Count,
466 ctx: &CursorMovementsContext<'_, ListCursor>,
467 ) -> Option<EditRange<ListCursor>> {
468 let len = self.items.len();
469 let max = len.saturating_sub(1);
470
471 match range {
472 RangeType::Bracketed(_, _) => None,
473 RangeType::Item => None,
474 RangeType::Quote(_) => None,
475 RangeType::Word(_) => None,
476 RangeType::XmlTag => None,
477
478 RangeType::Buffer => {
479 if len > 0 {
480 Some(EditRange::inclusive(0.into(), max.into(), TargetShape::LineWise))
481 } else {
482 None
483 }
484 },
485 RangeType::Line | RangeType::Paragraph | RangeType::Sentence => {
486 let count = ctx.context.resolve(count);
487 let end = count.saturating_sub(1).saturating_add(pos.position).min(max);
488
489 if len > 0 {
490 Some(EditRange::inclusive(pos.clone(), end.into(), TargetShape::LineWise))
491 } else {
492 None
493 }
494 },
495 _ => None,
496 }
497 }
498}
499
500impl<T, I> CursorSearch<ListCursor> for ListState<T, I>
501where
502 T: ListItem<I>,
503 I: ApplicationInfo,
504{
505 fn find_char(
506 &self,
507 _: &ListCursor,
508 _: bool,
509 _: MoveDir1D,
510 _: bool,
511 _: char,
512 _: usize,
513 ) -> Option<ListCursor> {
514 return None;
515 }
516
517 fn find_matches(
518 &self,
519 _: &ListCursor,
520 _: &ListCursor,
521 _: &Regex,
522 ) -> Vec<EditRange<ListCursor>> {
523 return vec![];
524 }
525
526 fn find_regex(
527 &self,
528 pos: &ListCursor,
529 dir: MoveDir1D,
530 needle: &Regex,
531 count: usize,
532 ) -> Option<EditRange<ListCursor>> {
533 let mut matches = vec![];
534
535 for (idx, item) in self.items.iter().enumerate() {
536 if item.matches(needle) {
537 matches.push(idx);
538 }
539 }
540
541 let modulus = matches.len();
542
543 if modulus == 0 {
544 return None;
545 }
546
547 let i = match dir {
548 MoveDir1D::Previous => matches.iter().position(|&idx| idx >= pos.position).unwrap_or(0),
549 MoveDir1D::Next => {
550 let max = modulus.saturating_sub(1);
551
552 matches.iter().rposition(|&idx| idx <= pos.position).unwrap_or(max)
553 },
554 };
555
556 idx_offset(i, count, &dir, modulus, true).map(|i| {
557 let cursor = ListCursor::from(matches[i]);
558 let lw = TargetShape::LineWise;
559
560 EditRange::inclusive(cursor.clone(), cursor, lw)
561 })
562 }
563}
564
565impl<T, I> EditorActions<EditContext, Store<I>, I> for ListState<T, I>
566where
567 T: ListItem<I>,
568 I: ApplicationInfo,
569{
570 fn edit(
571 &mut self,
572 operation: &EditAction,
573 motion: &EditTarget,
574 ctx: &EditContext,
575 store: &mut Store<I>,
576 ) -> EditResult<EditInfo, I> {
577 match operation {
578 EditAction::Motion => {
579 if motion.is_jumping() {
580 self.jumped.push(self.cursor.clone());
581 }
582
583 let pos = match motion {
584 EditTarget::CurrentPosition | EditTarget::Selection => {
585 return Ok(None);
586 },
587 EditTarget::Boundary(rt, inc, term, count) => {
588 let ctx = CursorMovementsContext {
589 action: operation,
590 view: &self.viewctx,
591 context: ctx,
592 };
593
594 self.range(&self.cursor, rt, *inc, count, &ctx).map(|r| {
595 match term {
596 MoveTerminus::Beginning => r.start,
597 MoveTerminus::End => r.end,
598 }
599 })
600 },
601 EditTarget::CharJump(mark) | EditTarget::LineJump(mark) => {
602 let mark = ctx.resolve(mark);
603 let cursor = store.cursors.get_mark(self.id.clone(), mark)?;
604
605 Some(cursor.y.into())
606 },
607 EditTarget::Motion(mt, count) => {
608 let ctx = CursorMovementsContext {
609 action: operation,
610 view: &self.viewctx,
611 context: ctx,
612 };
613
614 self.movement(&self.cursor, mt, count, &ctx)
615 },
616 EditTarget::Range(_, _, _) => {
617 return Err(EditError::Failure("Cannot use ranges in a list".to_string()));
618 },
619 EditTarget::Search(SearchType::Char(_), _, _) => {
620 let msg = "Cannot perform character search in a list";
621 let err = EditError::Failure(msg.into());
622
623 return Err(err);
624 },
625 EditTarget::Search(SearchType::Regex, flip, count) => {
626 let count = ctx.resolve(count);
627
628 let dir = ctx.get_search_regex_dir();
629 let dir = flip.resolve(&dir);
630
631 let lsearch = store.registers.get_last_search();
632 let lsearch = lsearch.to_string();
633 let needle = Regex::new(lsearch.as_ref())?;
634
635 self.find_regex(&self.cursor, dir, &needle, count).map(|r| r.start)
636 },
637 EditTarget::Search(SearchType::Word(_, _), _, _) => {
638 let msg = "Cannot perform word search in a list";
639 let err = EditError::Failure(msg.into());
640
641 return Err(err);
642 },
643 _ => return Ok(None),
644 };
645
646 if let Some(pos) = pos {
647 self.cursor = pos;
648 }
649
650 return Ok(None);
651 },
652 EditAction::Yank => {
653 let mut info = None;
654
655 let cmc = CursorMovementsContext {
656 action: operation,
657 view: &self.viewctx,
658 context: ctx,
659 };
660
661 let range = match motion {
662 EditTarget::CurrentPosition | EditTarget::Selection => {
663 Some(self._range_to(self.cursor.clone()))
664 },
665 EditTarget::Boundary(rt, inc, term, count) => {
666 self.range(&self.cursor, rt, *inc, count, &cmc).map(|r| {
667 self._range_to(match term {
668 MoveTerminus::Beginning => r.start,
669 MoveTerminus::End => r.end,
670 })
671 })
672 },
673 EditTarget::CharJump(mark) | EditTarget::LineJump(mark) => {
674 let mark = ctx.resolve(mark);
675 let cursor = store.cursors.get_mark(self.id.clone(), mark)?;
676
677 Some(self._range_to(cursor.y.into()))
678 },
679 EditTarget::Motion(mt, count) => {
680 self.range_of_movement(&self.cursor, mt, count, &cmc)
681 },
682 EditTarget::Range(rt, inc, count) => {
683 self.range(&self.cursor, rt, *inc, count, &cmc)
684 },
685 EditTarget::Search(SearchType::Char(_), _, _) => {
686 let msg = "Cannot perform character search in a list";
687 let err = EditError::Failure(msg.into());
688
689 return Err(err);
690 },
691 EditTarget::Search(SearchType::Regex, flip, count) => {
692 let count = ctx.resolve(count);
693
694 let dir = ctx.get_search_regex_dir();
695 let dir = flip.resolve(&dir);
696
697 let lsearch = store.registers.get_last_search();
698 let lsearch = lsearch.to_string();
699 let needle = Regex::new(lsearch.as_ref())?;
700
701 self.find_regex(&self.cursor, dir, &needle, count)
702 },
703 EditTarget::Search(SearchType::Word(_, _), _, _) => {
704 let msg = "Cannot perform word search in a list";
705 let err = EditError::Failure(msg.into());
706
707 return Err(err);
708 },
709 _ => return Ok(None),
710 };
711
712 if let Some(range) = range {
713 let mut items = 0;
714 let mut yanked = EditRope::from("");
715
716 for pos in range.start.position..=range.end.position {
717 if let Some(item) = self.items.get(pos) {
718 yanked += EditRope::from(item.to_string());
719 yanked += EditRope::from('\n');
720
721 items += 1;
722 } else {
723 break;
724 }
725 }
726
727 let cell = RegisterCell::new(TargetShape::LineWise, yanked);
728 let register = ctx.get_register().unwrap_or(Register::Unnamed);
729 let mut flags = RegisterPutFlags::NONE;
730
731 if ctx.get_register_append() {
732 flags |= RegisterPutFlags::APPEND;
733 }
734
735 store.registers.put(®ister, cell, flags)?;
736
737 if items > 1 {
738 info = Some(InfoMessage::Message(format!("{items} items yanked")));
739 }
740 }
741
742 return Ok(info);
743 },
744
745 EditAction::ChangeCase(_) => Err(EditError::ReadOnly),
747 EditAction::ChangeNumber(_, _) => Err(EditError::ReadOnly),
748 EditAction::Delete => Err(EditError::ReadOnly),
749 EditAction::Format => Err(EditError::ReadOnly),
750 EditAction::Indent(_) => Err(EditError::ReadOnly),
751 EditAction::Join(_) => Err(EditError::ReadOnly),
752 EditAction::Replace(_) => Err(EditError::ReadOnly),
753 }
754 }
755
756 fn mark(
757 &mut self,
758 name: Mark,
759 _: &EditContext,
760 store: &mut Store<I>,
761 ) -> EditResult<EditInfo, I> {
762 let cursor = Cursor::new(self.cursor.position, 0);
763
764 store.cursors.set_mark(self.id.clone(), name, cursor);
765
766 Ok(None)
767 }
768
769 fn complete(
770 &mut self,
771 _: &CompletionStyle,
772 _: &CompletionType,
773 _: &CompletionDisplay,
774 _: &EditContext,
775 _: &mut Store<I>,
776 ) -> EditResult<EditInfo, I> {
777 let msg = "Cannot complete any text inside a list";
778 let err = EditError::Failure(msg.into());
779
780 Err(err)
781 }
782
783 fn insert_text(
784 &mut self,
785 _: &InsertTextAction,
786 _: &EditContext,
787 _: &mut Store<I>,
788 ) -> EditResult<EditInfo, I> {
789 Err(EditError::ReadOnly)
790 }
791
792 fn selection_command(
793 &mut self,
794 _: &SelectionAction,
795 _: &EditContext,
796 _: &mut Store<I>,
797 ) -> EditResult<EditInfo, I> {
798 Err(EditError::Failure("Cannot perform selection actions in a list".into()))
799 }
800
801 fn history_command(
802 &mut self,
803 act: &HistoryAction,
804 _: &EditContext,
805 _: &mut Store<I>,
806 ) -> EditResult<EditInfo, I> {
807 match act {
808 HistoryAction::Checkpoint => Ok(None),
809 HistoryAction::Undo(_) => Err(EditError::Failure("Nothing to undo".into())),
810 HistoryAction::Redo(_) => Err(EditError::Failure("Nothing to redo".into())),
811 }
812 }
813
814 fn cursor_command(
815 &mut self,
816 act: &CursorAction,
817 ctx: &EditContext,
818 store: &mut Store<I>,
819 ) -> EditResult<EditInfo, I> {
820 match act {
821 CursorAction::Close(_) => Ok(None),
822 CursorAction::Rotate(_, _) => Ok(None),
823 CursorAction::Split(_) => Ok(None),
824
825 CursorAction::Restore(_) => {
826 let reg = ctx.get_register().unwrap_or(Register::UnnamedCursorGroup);
827
828 let ngroup = store.cursors.get_group(self.id.clone(), ®)?;
830
831 if self.jumped.current() != &self.cursor {
833 self.jumped.push(self.cursor.clone());
834 }
835
836 self.cursor = ngroup.leader.cursor().y.into();
837
838 Ok(None)
839 },
840 CursorAction::Save(_) => {
841 let reg = ctx.get_register().unwrap_or(Register::UnnamedCursorGroup);
842
843 let cursor = Cursor::new(self.cursor.position, 0);
845 let state = CursorState::Location(cursor);
846 let group = CursorGroup::new(state, vec![]);
847
848 store.cursors.set_group(self.id.clone(), reg, group)?;
849
850 Ok(None)
851 },
852 act => {
853 let msg = format!("unknown cursor action: {act:?}");
854 return Err(EditError::Unimplemented(msg));
855 },
856 }
857 }
858}
859
860impl<T, I> Editable<EditContext, Store<I>, I> for ListState<T, I>
861where
862 T: ListItem<I>,
863 I: ApplicationInfo,
864{
865 fn editor_command(
866 &mut self,
867 act: &EditorAction,
868 ctx: &EditContext,
869 store: &mut Store<I>,
870 ) -> EditResult<EditInfo, I> {
871 match act {
872 EditorAction::Cursor(act) => self.cursor_command(act, ctx, store),
873 EditorAction::Edit(ea, et) => self.edit(&ctx.resolve(ea), et, ctx, store),
874 EditorAction::History(act) => self.history_command(act, ctx, store),
875 EditorAction::InsertText(act) => self.insert_text(act, ctx, store),
876 EditorAction::Mark(name) => self.mark(ctx.resolve(name), ctx, store),
877 EditorAction::Selection(act) => self.selection_command(act, ctx, store),
878 EditorAction::Complete(sel, ct, disp) => self.complete(sel, ct, disp, ctx, store),
879 act => {
880 let msg = format!("unknown editor action: {act:?}");
881 return Err(EditError::Unimplemented(msg));
882 },
883 }
884 }
885}
886
887impl<T, I> Jumpable<EditContext, I> for ListState<T, I>
888where
889 T: ListItem<I>,
890 I: ApplicationInfo,
891{
892 fn jump(
893 &mut self,
894 list: PositionList,
895 dir: MoveDir1D,
896 count: usize,
897 _: &EditContext,
898 ) -> UIResult<usize, I> {
899 match list {
900 PositionList::ChangeList => {
901 let msg = "No changes to jump to within the list";
902 let err = UIError::Failure(msg.into());
903
904 return Err(err);
905 },
906 PositionList::JumpList => {
907 let (len, pos) = match dir {
908 MoveDir1D::Previous => {
909 if self.jumped.future_len() == 0 && *self.jumped.current() != self.cursor {
910 self.jumped.push(self.cursor.clone());
912 }
913
914 let plen = self.jumped.past_len();
915 let pos = self.jumped.prev(count);
916
917 (plen, pos)
918 },
919 MoveDir1D::Next => {
920 let flen = self.jumped.future_len();
921 let pos = self.jumped.next(count);
922
923 (flen, pos)
924 },
925 };
926
927 if len > 0 {
928 self.cursor = pos.clone();
929 }
930
931 return Ok(count.saturating_sub(len));
932 },
933 }
934 }
935}
936
937impl<C, I, T> Promptable<C, Store<I>, I> for ListState<T, I>
938where
939 I: ApplicationInfo,
940 T: ListItem<I> + Promptable<C, Store<I>, I>,
941{
942 fn prompt(
943 &mut self,
944 act: &PromptAction,
945 ctx: &C,
946 store: &mut Store<I>,
947 ) -> EditResult<Vec<(Action<I>, C)>, I> {
948 if let Some(item) = self.get_mut() {
949 return item.prompt(act, ctx, store);
950 } else {
951 let msg = "No item currently selected";
952 let err = EditError::Failure(msg.into());
953
954 return Err(err);
955 }
956 }
957}
958
959impl<I, T> Searchable<EditContext, Store<I>, I> for ListState<T, I>
960where
961 I: ApplicationInfo,
962 T: ListItem<I>,
963{
964 fn search(
965 &mut self,
966 dir: MoveDirMod,
967 count: Count,
968 ctx: &EditContext,
969 store: &mut Store<I>,
970 ) -> UIResult<EditInfo, I> {
971 let search = EditTarget::Search(SearchType::Regex, dir, count);
972
973 Ok(self.edit(&EditAction::Motion, &search, ctx, store)?)
974 }
975}
976
977impl<I, T> ScrollActions<EditContext, Store<I>, I> for ListState<T, I>
978where
979 I: ApplicationInfo,
980 T: ListItem<I>,
981{
982 fn dirscroll(
983 &mut self,
984 dir: MoveDir2D,
985 size: ScrollSize,
986 count: &Count,
987 ctx: &EditContext,
988 store: &mut Store<I>,
989 ) -> EditResult<EditInfo, I> {
990 if self.items.is_empty() {
991 return Ok(None);
992 }
993
994 let count = ctx.resolve(count);
995 let height = self.viewctx.get_height();
996 let mut corner = self.viewctx.corner.clone();
997
998 let mut rows = match size {
999 ScrollSize::Cell => count,
1000 ScrollSize::HalfPage => count.saturating_mul(height) / 2,
1001 ScrollSize::Page => count.saturating_mul(height),
1002 };
1003
1004 _clamp_cursor(&mut corner, self.items.len());
1005
1006 match dir {
1007 MoveDir2D::Up => {
1008 while rows > 0 {
1009 if corner.text_row >= rows {
1010 corner.text_row -= rows;
1011 break;
1012 } else if corner.position == 0 {
1013 corner.text_row = 0;
1014 break;
1015 }
1016
1017 rows -= corner.text_row.saturating_add(1);
1018
1019 let pos = corner.position.saturating_sub(1);
1020 let sel = pos == self.cursor.position;
1021 let txt = self.items[pos].show(sel, &self.viewctx, store);
1022
1023 corner.position = pos;
1024 corner.text_row = txt.height().saturating_sub(1);
1025 }
1026 },
1027 MoveDir2D::Down => {
1028 let last = self.items.len().saturating_sub(1);
1029
1030 while rows > 0 {
1031 let pos = corner.position;
1032 let sel = pos == self.cursor.position;
1033 let txt = self.items[pos].show(sel, &self.viewctx, store);
1034 let len = txt.height();
1035 let max = len.saturating_sub(1);
1036
1037 if pos == last {
1038 corner.text_row = corner.text_row.saturating_add(rows).min(max);
1039 break;
1040 } else if corner.text_row >= max {
1041 corner.position = pos.saturating_add(1);
1042 corner.text_row = 0;
1043
1044 rows -= 1;
1045 } else if corner.text_row + rows <= max {
1046 corner.text_row += rows;
1047 break;
1048 } else {
1049 corner.position = pos.saturating_add(1);
1050 corner.text_row = 0;
1051
1052 rows -= len - corner.text_row;
1053 }
1054 }
1055 },
1056 MoveDir2D::Left | MoveDir2D::Right => {
1057 let msg = "Cannot scroll horizontally in a list";
1058 let err = EditError::Failure(msg.into());
1059
1060 return Err(err);
1061 },
1062 };
1063
1064 self.viewctx.corner = corner;
1065 self.shift_cursor(store);
1066
1067 Ok(None)
1068 }
1069
1070 fn cursorpos(
1071 &mut self,
1072 pos: MovePosition,
1073 axis: Axis,
1074 _: &EditContext,
1075 store: &mut Store<I>,
1076 ) -> EditResult<EditInfo, I> {
1077 match axis {
1078 Axis::Horizontal => {
1079 let msg = "Cannot scroll horizontally in a list";
1080 let err = EditError::Failure(msg.into());
1081
1082 return Err(err);
1083 },
1084 Axis::Vertical => {
1085 self.scrollview(self.cursor.position, pos, store);
1086
1087 return Ok(None);
1088 },
1089 }
1090 }
1091
1092 fn linepos(
1093 &mut self,
1094 pos: MovePosition,
1095 count: &Count,
1096 ctx: &EditContext,
1097 store: &mut Store<I>,
1098 ) -> EditResult<EditInfo, I> {
1099 let len = self.items.len();
1100 let index = ctx.resolve(count).min(len).saturating_sub(1);
1101
1102 self.scrollview(index, pos, store);
1103 self.shift_cursor(store);
1104
1105 Ok(None)
1106 }
1107}
1108
1109impl<I, T> Scrollable<EditContext, Store<I>, I> for ListState<T, I>
1110where
1111 I: ApplicationInfo,
1112 T: ListItem<I>,
1113{
1114 fn scroll(
1115 &mut self,
1116 style: &ScrollStyle,
1117 ctx: &EditContext,
1118 store: &mut Store<I>,
1119 ) -> EditResult<EditInfo, I> {
1120 match style {
1121 ScrollStyle::Direction2D(dir, size, count) => {
1122 return self.dirscroll(*dir, *size, count, ctx, store);
1123 },
1124 ScrollStyle::CursorPos(pos, axis) => {
1125 return self.cursorpos(*pos, *axis, ctx, store);
1126 },
1127 ScrollStyle::LinePos(pos, count) => {
1128 return self.linepos(*pos, count, ctx, store);
1129 },
1130 }
1131 }
1132}
1133
1134impl<T, I> TerminalCursor for ListState<T, I>
1135where
1136 T: ListItem<I>,
1137 I: ApplicationInfo,
1138{
1139 fn get_term_cursor(&self) -> Option<(u16, u16)> {
1140 return None;
1142 }
1143}
1144
1145impl<I, T> WindowOps<I> for ListState<T, I>
1146where
1147 T: ListItem<I>,
1148 I: ApplicationInfo,
1149{
1150 fn dup(&self, _: &mut Store<I>) -> Self {
1151 ListState {
1152 id: self.id.clone(),
1153 items: self.items.clone(),
1154 cursor: self.cursor.clone(),
1155 viewctx: self.viewctx.clone(),
1156 jumped: self.jumped.clone(),
1157 }
1158 }
1159
1160 fn close(&mut self, _: CloseFlags, _: &mut Store<I>) -> bool {
1161 true
1162 }
1163
1164 fn write(&mut self, _: Option<&str>, _: WriteFlags, _: &mut Store<I>) -> UIResult<EditInfo, I> {
1165 Err(UIError::Failure("Cannot write list".into()))
1166 }
1167
1168 fn draw(&mut self, area: Rect, buf: &mut Buffer, focused: bool, store: &mut Store<I>) {
1169 List::new(store).focus(focused).render(area, buf, self);
1170 }
1171
1172 fn get_completions(&self) -> Option<CompletionList> {
1173 None
1174 }
1175
1176 fn get_cursor_word(&self, _: &WordStyle) -> Option<String> {
1177 self.items.get(self.cursor.position).and_then(ListItem::get_word)
1178 }
1179
1180 fn get_selected_word(&self) -> Option<String> {
1181 self.items.get(self.cursor.position).and_then(ListItem::get_word)
1182 }
1183}
1184
1185impl<'a, T, I> List<'a, T, I>
1186where
1187 T: ListItem<I>,
1188 I: ApplicationInfo,
1189{
1190 pub fn new(store: &'a mut Store<I>) -> Self {
1192 List {
1193 focused: false,
1194 empty_message: None,
1195 empty_alignment: Alignment::Left,
1196 store,
1197 _p: PhantomData,
1198 }
1199 }
1200
1201 pub fn empty_message<X: Into<Text<'a>>>(mut self, text: X) -> Self {
1203 self.empty_message = Some(text.into());
1204 self
1205 }
1206
1207 pub fn empty_alignment(mut self, alignment: Alignment) -> Self {
1209 self.empty_alignment = alignment;
1210 self
1211 }
1212
1213 pub fn focus(mut self, focused: bool) -> Self {
1215 self.focused = focused;
1216 self
1217 }
1218}
1219
1220impl<T, I> StatefulWidget for List<'_, T, I>
1221where
1222 T: ListItem<I>,
1223 I: ApplicationInfo,
1224{
1225 type State = ListState<T, I>;
1226
1227 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
1228 state.set_term_info(area);
1229
1230 let height = state.viewctx.get_height();
1231
1232 if height == 0 {
1233 return;
1234 }
1235
1236 if state.is_empty() {
1237 if let Some(msg) = self.empty_message {
1238 Paragraph::new(msg).alignment(self.empty_alignment).render(area, buf);
1239 return;
1240 }
1241 }
1242
1243 if state.cursor < state.viewctx.corner {
1244 state.viewctx.corner = state.cursor.clone();
1245 }
1246
1247 state._clamp();
1248
1249 let corner = &state.viewctx.corner;
1250 let mut lines = vec![];
1251 let mut sawit = false;
1252
1253 for (idx, item) in state.items.iter().enumerate().skip(corner.position) {
1254 let sel = idx == state.cursor.position;
1255 let txt = item.show(self.focused && sel, &state.viewctx, self.store);
1256
1257 if sel && txt.lines.len() >= height {
1258 lines = txt
1259 .lines
1260 .into_iter()
1261 .take(height)
1262 .enumerate()
1263 .map(|(row, line)| (idx, row, line))
1264 .collect();
1265 break;
1266 }
1267
1268 for (row, line) in txt.lines.into_iter().enumerate() {
1269 if idx == corner.position && row < corner.text_row {
1270 continue;
1271 }
1272
1273 if sawit && lines.len() >= height {
1274 break;
1275 }
1276
1277 lines.push((idx, row, line));
1278 }
1279
1280 if sel {
1281 sawit = true;
1282 }
1283
1284 if sawit && lines.len() >= height {
1285 break;
1286 }
1287 }
1288
1289 if lines.len() > height {
1290 let n = lines.len() - height;
1291 let _ = lines.drain(..n);
1292 }
1293
1294 if let Some((idx, row, _)) = lines.first() {
1295 state.viewctx.corner.position = *idx;
1296 state.viewctx.corner.text_row = *row;
1297 }
1298
1299 let mut y = area.top();
1300 let x = area.left();
1301
1302 for (_, _, txt) in lines.into_iter() {
1303 let _ = buf.set_line(x, y, &txt, area.width);
1304
1305 y += 1;
1306 }
1307 }
1308}
1309
1310#[cfg(test)]
1311mod tests {
1312 use std::fmt::Display;
1313
1314 use super::*;
1315 use ratatui::text::{Line, Span};
1316
1317 use modalkit::actions::WindowAction;
1318 use modalkit::editing::application::EmptyInfo;
1319 use modalkit::editing::context::EditContextBuilder;
1320
1321 #[derive(Clone)]
1322 struct TestItem {
1323 book: String,
1324 author: String,
1325 }
1326
1327 impl TestItem {
1328 fn new(book: &str, author: &str) -> Self {
1329 let book = book.to_owned();
1330 let author = author.to_owned();
1331
1332 TestItem { book, author }
1333 }
1334 }
1335
1336 impl Display for TestItem {
1337 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1338 f.write_str(&self.book)
1339 }
1340 }
1341
1342 impl<I> ListItem<I> for TestItem
1343 where
1344 I: ApplicationInfo,
1345 {
1346 fn show(&self, selected: bool, _: &ViewportContext<ListCursor>, _: &mut Store<I>) -> Text {
1347 let style = if selected {
1348 Style::default().add_modifier(StyleModifier::REVERSED)
1349 } else {
1350 Style::default()
1351 };
1352
1353 let line1 = Line::from(Span::styled(self.book.as_str(), style));
1354 let line2 = Line::from(vec![Span::from(" by "), Span::from(self.author.as_str())]);
1355
1356 Text::from(vec![line1, line2])
1357 }
1358 }
1359
1360 impl<I> Promptable<EditContext, Store<I>, I> for TestItem
1361 where
1362 I: ApplicationInfo,
1363 {
1364 fn prompt(
1365 &mut self,
1366 act: &PromptAction,
1367 ctx: &EditContext,
1368 _: &mut Store<I>,
1369 ) -> EditResult<Vec<(Action<I>, EditContext)>, I> {
1370 match act {
1371 PromptAction::Submit => {
1372 let target = OpenTarget::Name(self.author.clone());
1373 let act = WindowAction::Switch(target);
1374
1375 return Ok(vec![(act.into(), ctx.clone())]);
1376 },
1377 PromptAction::Abort(_) | PromptAction::Recall(_, _, _) => {
1378 let msg = "";
1379 let err = EditError::Unimplemented(msg.into());
1380
1381 Err(err)
1382 },
1383 }
1384 }
1385 }
1386
1387 type TestListState = ListState<TestItem, EmptyInfo>;
1388
1389 fn mklist() -> (TestListState, EditContext, Store<EmptyInfo>) {
1390 let mut list = ListState::new("".to_string(), vec![
1413 TestItem::new("The Wind-Up Bird Chronicle", "Haruki Murakami"),
1414 TestItem::new("The Master and Margarita", "Mikhail Bulgakov"),
1415 TestItem::new("The Left Hand of Darkness", "Ursula K. Le Guin"),
1416 TestItem::new("2666", "Roberto Bolaño"),
1417 TestItem::new("Nevada", "Imogen Binnie"),
1418 TestItem::new("Annihilation", "Jeff Vandermeer"),
1419 TestItem::new("Foucault's Pendulum", "Umberto Eco"),
1420 TestItem::new("Monday Starts on Saturday", "Arkady Strugatsky"),
1421 ]);
1422
1423 list.viewctx.dimensions.0 = 30;
1424 list.viewctx.dimensions.1 = 5;
1425
1426 (list, EditContext::default(), Store::default())
1427 }
1428
1429 #[test]
1430 fn test_list_length() {
1431 let list = TestListState::new("".to_string(), vec![]);
1432 assert_eq!(list.is_empty(), true);
1433 assert_eq!(list.len(), 0);
1434
1435 let list = TestListState::new("".to_string(), vec![TestItem::new("Dune", "Frank Herbert")]);
1436 assert_eq!(list.is_empty(), false);
1437 assert_eq!(list.len(), 1);
1438
1439 let list = TestListState::new("".to_string(), vec![
1440 TestItem::new("The Name of the Wind", "Patrick Rothfuss"),
1441 TestItem::new("Sabriel", "Garth Nix"),
1442 TestItem::new("The Three-Body Problem", "Cixin Liu"),
1443 ]);
1444 assert_eq!(list.is_empty(), false);
1445 assert_eq!(list.len(), 3);
1446 }
1447
1448 #[test]
1449 fn test_render_cursor() {
1450 let (mut list, _, mut store) = mklist();
1451 let area = Rect::new(0, 0, 30, 5);
1452 let mut buffer = Buffer::empty(area);
1453
1454 list.cursor = ListCursor::new(1, 0);
1456 list.viewctx.corner = ListCursor::new(0, 0);
1457
1458 list.draw(area, &mut buffer, true, &mut store);
1460 assert_eq!(list.cursor, ListCursor::new(1, 0));
1461 assert_eq!(list.viewctx.corner, ListCursor::new(0, 0));
1462
1463 list.draw(area, &mut buffer, false, &mut store);
1465 assert_eq!(list.cursor, ListCursor::new(1, 0));
1466 assert_eq!(list.viewctx.corner, ListCursor::new(0, 0));
1467
1468 list.cursor = ListCursor::new(4, 0);
1470
1471 list.draw(area, &mut buffer, true, &mut store);
1473 assert_eq!(list.cursor, ListCursor::new(4, 0));
1474 assert_eq!(list.viewctx.corner, ListCursor::new(2, 1));
1475
1476 list.viewctx.corner = ListCursor::new(0, 0);
1478
1479 list.draw(area, &mut buffer, true, &mut store);
1481 assert_eq!(list.cursor, ListCursor::new(4, 0));
1482 assert_eq!(list.viewctx.corner, ListCursor::new(2, 1));
1483 }
1484
1485 #[test]
1486 fn test_motion_line() {
1487 let (mut list, ctx, mut store) = mklist();
1488 let op = EditAction::Motion;
1489 let prev = MoveType::Line(MoveDir1D::Previous);
1490 let next = MoveType::Line(MoveDir1D::Next);
1491
1492 assert_eq!(list.cursor.position, 0);
1493
1494 list.edit(&op, &EditTarget::Motion(next.clone(), 1.into()), &ctx, &mut store)
1495 .unwrap();
1496 assert_eq!(list.cursor.position, 1);
1497
1498 list.edit(&op, &EditTarget::Motion(next.clone(), 3.into()), &ctx, &mut store)
1499 .unwrap();
1500 assert_eq!(list.cursor.position, 4);
1501
1502 list.edit(&op, &EditTarget::Motion(next.clone(), 5.into()), &ctx, &mut store)
1503 .unwrap();
1504 assert_eq!(list.cursor.position, 7);
1505
1506 list.edit(&op, &EditTarget::Motion(prev.clone(), 10.into()), &ctx, &mut store)
1507 .unwrap();
1508 assert_eq!(list.cursor.position, 0);
1509 }
1510
1511 #[test]
1512 fn test_motion_buffer_line_offset() {
1513 let (mut list, ctx, mut store) = mklist();
1514 let op = EditAction::Motion;
1515 let mv = MoveType::BufferLineOffset;
1516
1517 list.edit(&op, &EditTarget::Motion(mv.clone(), 8.into()), &ctx, &mut store)
1518 .unwrap();
1519 assert_eq!(list.cursor.position, 7);
1520
1521 list.edit(&op, &EditTarget::Motion(mv.clone(), 5.into()), &ctx, &mut store)
1522 .unwrap();
1523 assert_eq!(list.cursor.position, 4);
1524
1525 list.edit(&op, &EditTarget::Motion(mv.clone(), 1.into()), &ctx, &mut store)
1526 .unwrap();
1527 assert_eq!(list.cursor.position, 0);
1528
1529 list.edit(&op, &EditTarget::Motion(mv.clone(), 10.into()), &ctx, &mut store)
1530 .unwrap();
1531 assert_eq!(list.cursor.position, 7);
1532 }
1533
1534 #[test]
1535 fn test_motion_buffer_line_percent() {
1536 let (mut list, ctx, mut store) = mklist();
1537 let op = EditAction::Motion;
1538 let mv = MoveType::BufferLinePercent;
1539
1540 list.edit(&op, &EditTarget::Motion(mv.clone(), 100.into()), &ctx, &mut store)
1541 .unwrap();
1542 assert_eq!(list.cursor.position, 7);
1543
1544 list.edit(&op, &EditTarget::Motion(mv.clone(), 50.into()), &ctx, &mut store)
1545 .unwrap();
1546 assert_eq!(list.cursor.position, 3);
1547
1548 list.edit(&op, &EditTarget::Motion(mv.clone(), 25.into()), &ctx, &mut store)
1549 .unwrap();
1550 assert_eq!(list.cursor.position, 1);
1551
1552 list.edit(&op, &EditTarget::Motion(mv.clone(), 0.into()), &ctx, &mut store)
1553 .unwrap();
1554 assert_eq!(list.cursor.position, 0);
1555 }
1556
1557 #[test]
1558 fn test_motion_buffer_pos() {
1559 let (mut list, ctx, mut store) = mklist();
1560 let op = EditAction::Motion;
1561 let beg = MoveType::BufferPos(MovePosition::Beginning);
1562 let mid = MoveType::BufferPos(MovePosition::Middle);
1563 let end = MoveType::BufferPos(MovePosition::End);
1564
1565 assert_eq!(list.cursor.position, 0);
1566
1567 list.edit(&op, &end.into(), &ctx, &mut store).unwrap();
1568 assert_eq!(list.cursor.position, 7);
1569
1570 list.edit(&op, &beg.into(), &ctx, &mut store).unwrap();
1571 assert_eq!(list.cursor.position, 0);
1572
1573 list.edit(&op, &mid.into(), &ctx, &mut store).unwrap();
1574 assert_eq!(list.cursor.position, 4);
1575 }
1576
1577 #[test]
1578 fn test_motion_viewport() {
1579 let (mut list, ctx, mut store) = mklist();
1580 let op = EditAction::Motion;
1581 let beg = MoveType::ViewportPos(MovePosition::Beginning);
1582
1583 assert_eq!(list.cursor.position, 0);
1584
1585 list.viewctx.corner.position = 3;
1586
1587 list.edit(&op, &beg.clone().into(), &ctx, &mut store).unwrap();
1588 assert_eq!(list.cursor.position, 3);
1589
1590 list.viewctx.corner.position = 6;
1591
1592 list.edit(&op, &beg.clone().into(), &ctx, &mut store).unwrap();
1593 assert_eq!(list.cursor.position, 6);
1594 }
1595
1596 #[test]
1597 fn test_yank() {
1598 let (mut list, mut ctx, mut store) = mklist();
1599 let op = EditAction::Yank;
1600 let end = EditTarget::Motion(MoveType::BufferPos(MovePosition::End), 1.into());
1601 list.cursor.position = 4;
1602 ctx = EditContextBuilder::from(ctx).register(Some(Register::Named('c'))).build();
1603
1604 list.edit(&op, &end, &ctx, &mut store).unwrap();
1605
1606 let cell = store.registers.get(&Register::Named('c')).unwrap();
1607 let rope = cell.value;
1608
1609 assert_eq!(
1610 rope.to_string(),
1611 "Nevada\nAnnihilation\nFoucault's Pendulum\nMonday Starts on Saturday\n"
1612 );
1613 }
1614
1615 #[test]
1616 fn test_search() {
1617 let (mut list, ctx, mut store) = mklist();
1618
1619 store.registers.set_last_search("on");
1620
1621 assert_eq!(list.cursor.position, 0);
1622
1623 list.search(MoveDir1D::Next.into(), 1.into(), &ctx, &mut store).unwrap();
1624 assert_eq!(list.cursor.position, 5);
1625
1626 list.search(MoveDir1D::Next.into(), 1.into(), &ctx, &mut store).unwrap();
1627 assert_eq!(list.cursor.position, 7);
1628
1629 list.search(MoveDir1D::Next.into(), 1.into(), &ctx, &mut store).unwrap();
1630 assert_eq!(list.cursor.position, 0);
1631
1632 list.search(MoveDir1D::Previous.into(), 3.into(), &ctx, &mut store)
1633 .unwrap();
1634 assert_eq!(list.cursor.position, 0);
1635
1636 list.search(MoveDir1D::Previous.into(), 2.into(), &ctx, &mut store)
1637 .unwrap();
1638 assert_eq!(list.cursor.position, 5);
1639
1640 list.search(MoveDir1D::Previous.into(), 2.into(), &ctx, &mut store)
1641 .unwrap();
1642 assert_eq!(list.cursor.position, 7);
1643 }
1644
1645 #[test]
1646 fn test_mark_and_jumps() {
1647 let (mut list, ctx, mut store) = mklist();
1648
1649 let a = Mark::BufferNamed('a');
1650 let b = Mark::BufferNamed('b');
1651 let c = Mark::BufferNamed('c');
1652 let d = Mark::BufferNamed('d');
1653
1654 assert_eq!(list.cursor.position, 0);
1655 list.mark(a, &ctx, &mut store).unwrap();
1656
1657 list.cursor.position = 2;
1658 list.mark(b, &ctx, &mut store).unwrap();
1659
1660 list.cursor.position = 4;
1661 list.mark(c, &ctx, &mut store).unwrap();
1662
1663 list.cursor.position = 7;
1664 list.mark(d, &ctx, &mut store).unwrap();
1665
1666 let op = EditAction::Motion;
1667
1668 list.edit(&op, &EditTarget::LineJump(a.into()), &ctx, &mut store).unwrap();
1669 assert_eq!(list.cursor.position, 0);
1670
1671 list.edit(&op, &EditTarget::LineJump(b.into()), &ctx, &mut store).unwrap();
1672 assert_eq!(list.cursor.position, 2);
1673
1674 list.edit(&op, &EditTarget::LineJump(c.into()), &ctx, &mut store).unwrap();
1675 assert_eq!(list.cursor.position, 4);
1676
1677 list.edit(&op, &EditTarget::LineJump(d.into()), &ctx, &mut store).unwrap();
1678 assert_eq!(list.cursor.position, 7);
1679
1680 let res = list.jump(PositionList::JumpList, MoveDir1D::Previous, 1, &ctx).unwrap();
1681 assert_eq!(res, 0);
1682 assert_eq!(list.cursor.position, 4);
1683
1684 let res = list.jump(PositionList::JumpList, MoveDir1D::Previous, 1, &ctx).unwrap();
1685 assert_eq!(res, 0);
1686 assert_eq!(list.cursor.position, 2);
1687
1688 let res = list.jump(PositionList::JumpList, MoveDir1D::Previous, 1, &ctx).unwrap();
1689 assert_eq!(res, 0);
1690 assert_eq!(list.cursor.position, 0);
1691
1692 let res = list.jump(PositionList::JumpList, MoveDir1D::Next, 3, &ctx).unwrap();
1693 assert_eq!(res, 0);
1694 assert_eq!(list.cursor.position, 7);
1695 }
1696
1697 #[test]
1698 fn test_scroll_dirscroll() {
1699 let (mut list, ctx, mut store) = mklist();
1700
1701 assert_eq!(list.cursor.position, 0);
1702 assert_eq!(list.viewctx.corner.position, 0);
1703
1704 list.dirscroll(MoveDir2D::Down, ScrollSize::Page, &2.into(), &ctx, &mut store)
1705 .unwrap();
1706 assert_eq!(list.viewctx.corner, ListCursor::new(5, 0));
1707 assert_eq!(list.cursor.position, 5);
1708
1709 list.dirscroll(MoveDir2D::Up, ScrollSize::Page, &1.into(), &ctx, &mut store)
1710 .unwrap();
1711 assert_eq!(list.viewctx.corner, ListCursor::new(2, 1));
1712 assert_eq!(list.cursor.position, 4);
1713
1714 list.dirscroll(MoveDir2D::Down, ScrollSize::Cell, &1.into(), &ctx, &mut store)
1715 .unwrap();
1716 assert_eq!(list.viewctx.corner, ListCursor::new(3, 0));
1717 assert_eq!(list.cursor.position, 4);
1718 }
1719
1720 #[test]
1721 fn test_scroll_cursorpos() {
1722 let (mut list, ctx, mut store) = mklist();
1723
1724 list.cursor.position = 3;
1725
1726 list.cursorpos(MovePosition::Beginning, Axis::Vertical, &ctx, &mut store)
1727 .unwrap();
1728 assert_eq!(list.cursor.position, 3);
1729 assert_eq!(list.viewctx.corner, ListCursor::new(3, 0));
1730
1731 list.cursorpos(MovePosition::Middle, Axis::Vertical, &ctx, &mut store)
1732 .unwrap();
1733 assert_eq!(list.cursor.position, 3);
1734 assert_eq!(list.viewctx.corner, ListCursor::new(2, 1));
1735
1736 list.cursorpos(MovePosition::End, Axis::Vertical, &ctx, &mut store)
1737 .unwrap();
1738 assert_eq!(list.cursor.position, 3);
1739 assert_eq!(list.viewctx.corner, ListCursor::new(1, 1));
1740 }
1741
1742 #[test]
1743 fn test_scroll_linepos() {
1744 let (mut list, ctx, mut store) = mklist();
1745
1746 assert_eq!(list.cursor.position, 0);
1747 assert_eq!(list.viewctx.corner.position, 0);
1748
1749 list.linepos(MovePosition::Beginning, &5.into(), &ctx, &mut store).unwrap();
1751 assert_eq!(list.viewctx.corner, ListCursor::new(4, 0));
1752 assert_eq!(list.cursor.position, 4);
1753
1754 list.linepos(MovePosition::Middle, &1.into(), &ctx, &mut store).unwrap();
1756 assert_eq!(list.viewctx.corner, ListCursor::new(0, 0));
1757 assert_eq!(list.cursor.position, 2);
1758
1759 list.linepos(MovePosition::Middle, &6.into(), &ctx, &mut store).unwrap();
1761 assert_eq!(list.viewctx.corner, ListCursor::new(4, 1));
1762 assert_eq!(list.cursor.position, 4);
1763
1764 list.linepos(MovePosition::End, &5.into(), &ctx, &mut store).unwrap();
1766 assert_eq!(list.viewctx.corner, ListCursor::new(2, 1));
1767 assert_eq!(list.cursor.position, 4);
1768 }
1769
1770 #[test]
1771 fn test_submit() {
1772 let (mut list, ctx, mut store) = mklist();
1773
1774 assert_eq!(list.cursor.position, 0);
1775
1776 let acts = list.prompt(&PromptAction::Submit, &ctx, &mut store).unwrap();
1777 let target = OpenTarget::Name("Haruki Murakami".into());
1778 assert_eq!(acts.len(), 1);
1779 assert_eq!(acts[0].0, WindowAction::Switch(target).into());
1780
1781 list.cursor.position = 4;
1782
1783 let acts = list.prompt(&PromptAction::Submit, &ctx, &mut store).unwrap();
1784 let target = OpenTarget::Name("Imogen Binnie".into());
1785 assert_eq!(acts.len(), 1);
1786 assert_eq!(acts[0].0, WindowAction::Switch(target).into());
1787 }
1788}