Skip to main content

modalkit_ratatui/
list.rs

1//! # List
2//!
3//! ## Overview
4//!
5//! This renders a list of items that can be navigated using the [Editable] trait.
6//!
7//! ## Example
8//!
9//! ```
10//! use modalkit::{
11//!     actions::{Editable, EditAction, EditorActions},
12//!     editing::application::EmptyInfo,
13//!     editing::context::EditContext,
14//!     editing::store::Store,
15//!     prelude::*,
16//! };
17//! use modalkit_ratatui::list::ListState;
18//!
19//! let mut store = Store::default();
20//! let ctx = EditContext::default();
21//!
22//! // Create new list state.
23//! let items = vec!["Alice".into(), "Bob".into(), "Eve".into()];
24//! let mut list = ListState::<String, EmptyInfo>::new("People".into(), items);
25//!
26//! // Jump to end of the list.
27//! let op = EditAction::Motion;
28//! let mv = MoveType::BufferPos(MovePosition::End);
29//! let _ = list.edit(&op, &mv.into(), &ctx, &mut store).unwrap();
30//! ```
31use 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/// A position within a list.
72#[derive(Clone, Debug, Default, Eq, PartialEq)]
73pub struct ListCursor {
74    /// The position of the selected item within the list.
75    pub position: usize,
76
77    /// A row within the [Text] representation of the selected [ListItem].
78    pub text_row: usize,
79}
80
81impl ListCursor {
82    /// Create a new cursor for a list.
83    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
109/// Trait for items kept in a [ListState].
110pub trait ListItem<I>: Clone + ToString
111where
112    I: ApplicationInfo,
113{
114    /// Return a representation of this item to show in the terminal window.
115    fn show(
116        &self,
117        selected: bool,
118        viewport: &ViewportContext<ListCursor>,
119        store: &mut Store<I>,
120    ) -> Text;
121
122    /// Return a word that represents this list item.
123    ///
124    /// By default this is just the [ToString] value, but you can provide a different
125    /// implementation to get more useful [OpenTarget::Cursor] behaviour.
126    fn get_word(&self) -> Option<String> {
127        self.to_string().into()
128    }
129
130    /// Checks whether this list item contains a given regular expression.
131    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
152/// Persistent state for [List].
153pub 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    /// Tracks the jumplist for this window.
164    jumped: HistoryList<ListCursor>,
165}
166
167/// Widget for rendering a list of text items.
168pub 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    /// Create state for a list.
186    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    /// Get the content identifier for this list.
200    pub fn id(&self) -> I::ContentId {
201        self.id.clone()
202    }
203
204    /// Indicates whether or not this list contains any items.
205    pub fn is_empty(&self) -> bool {
206        self.items.is_empty()
207    }
208
209    /// Returns the number of items in this list.
210    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                        // We've moved back far enough.
248                        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                        // We've moved back far enough.
270                        self.viewctx.corner.position = idx;
271                        self.viewctx.corner.text_row = lines - target;
272                        break;
273                    }
274                }
275            },
276        }
277    }
278
279    /// Move cursor back inside viewport.
280    fn shift_cursor(&mut self, store: &mut Store<I>) {
281        if self.cursor < self.viewctx.corner {
282            // Cursor is above the viewport; move it inside.
283            self.cursor = self.viewctx.corner.position.into();
284            return;
285        }
286
287        // Check whether the cursor is below the viewport.
288        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                // Cursor is already within the viewport.
293                break;
294            }
295
296            lines += item.show(false, &self.viewctx, store).lines.len();
297
298            if lines >= self.viewctx.get_height() {
299                // We've reached the end of the viewport; move cursor into it.
300                self.cursor = idx.into();
301                break;
302            }
303        }
304    }
305
306    /// Replace the set of items with a new list.
307    pub fn set(&mut self, items: Vec<T>) {
308        self.items = items;
309        self._clamp();
310    }
311
312    /// Get a reference to the currently selected value.
313    pub fn get(&self) -> Option<&T> {
314        self.items.get(self.cursor.position)
315    }
316
317    /// Get a mutable reference to the currently selected value.
318    pub fn get_mut(&mut self) -> Option<&mut T> {
319        self.items.get_mut(self.cursor.position)
320    }
321
322    /// Set the dimensions and placement within the terminal window for this list.
323    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            // These movements don't map meaningfully onto a list.
353            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                // Calculate the new index as described in :help N%
381                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                // Need store to calculate an accurate middle position.
438                return None;
439            },
440            MoveType::ViewportPos(MovePosition::End) => {
441                // Need store to calculate an accurate end position.
442                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(&register, 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            // Everything else is a modifying action.
746            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                // Get saved group.
829                let ngroup = store.cursors.get_group(self.id.clone(), &reg)?;
830
831                // Lists don't have groups; override current position.
832                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                // Lists don't have groups; override any previously saved group.
844                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                            // Push current position if this is the first jump backwards.
911                            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        // We highlight the selected text, but don't show the cursor.
1141        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    /// Create a new widget.
1191    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    /// Set a message to display when the list is empty.
1202    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    /// Set the alignment of the displayed empty message.
1208    pub fn empty_alignment(mut self, alignment: Alignment) -> Self {
1209        self.empty_alignment = alignment;
1210        self
1211    }
1212
1213    /// Indicate whether the widget is currently focused.
1214    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        /*
1391         * This will render as:
1392         *
1393         * +------------------------------+
1394         * |The Wind-Up Bird Chronicle    |
1395         * |    by Haruki Murakami        |
1396         * |The Master and Margarita      |
1397         * |    by Mikhail Bulgakov       |
1398         * |The Left Hand of Darkness     |
1399         * |    by Ursula K. Le Guin      |
1400         * |2666                          |
1401         * |    by Roberto Bolaño         |
1402         * |Nevada                        |
1403         * |    by Imogen Binnie          |
1404         * |Annihilation                  |
1405         * |    by Jeff Vandermeer        |
1406         * |Foucault's Pendulum           |
1407         * |    by Umberto Eco            |
1408         * |Monday Starts on Saturday     |
1409         * |    by Arkady Strugatsky      |
1410         * +------------------------------+
1411         */
1412        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        // Start out w/ cursor at item 1 and corner at item 0.
1455        list.cursor = ListCursor::new(1, 0);
1456        list.viewctx.corner = ListCursor::new(0, 0);
1457
1458        // Rendering list when cursor is in view doesn't move corner.
1459        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        // Removing focus doesn't matter.
1464        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        // Move cursor out of view.
1469        list.cursor = ListCursor::new(4, 0);
1470
1471        // Rendering list when cursor is out of view moves corner.
1472        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        // Reset corner.
1477        list.viewctx.corner = ListCursor::new(0, 0);
1478
1479        // Corner still moves when unfocused.
1480        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        // Cursor is above viewport after scrolling, and gets placed at top.
1750        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        // Cursor is below viewport after scrolling, and gets placed at bottom.
1755        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        // Cursor is above viewport after scrolling, and gets placed at top.
1760        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        // Cursor stays inside viewport.
1765        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}