ted 0.7.0

Core text editor functionality.
Documentation
pub mod jump;

use frappe::Signal;
use ropey::Rope;
use Ropey;
use {Buffer, buffer};

/// Imagine a cursor as the primary actor through which to edit a buffer.
/// A cursor can have multiple positions, aka multi-cursor.
pub struct Cursor {
    positions: Vec<PositionSignal>
    // TODO Insert/overwrite mode?
}

type PositionSignal = (Signal<usize>, Option<Signal<usize>>);

/// The char index of the cursor and the selection start char index.
pub type Position = (usize, Option<usize>);

impl Cursor {
    pub fn new<'a, I>(buf: &Buffer, positions: I) -> Self
    where I: IntoIterator<Item=&'a Position> {
        let mut cur = Self {
            positions: Vec::new()
        };
        cur.set_positions(buf, positions);
        cur
    }

    pub fn positions(&self) -> Positions {
        Positions::new(self.positions.as_slice())
    }

    /// Handles a mutation of the cursor, leaving the buffer unchanged.
    pub fn handle_cur(&mut self, buf: &Buffer, event: CursorEvent) {
        match event {
            CursorEvent::Jump(mut positions) => {
                positions.sort();
                positions.dedup();

                self.set_positions(buf, positions.iter());
            }
        }
    }

    /// Handles a mutation of the buffer.
    pub fn handle_buf(&mut self, buf: &mut Buffer, event: BufferEvent) {
        let pos_lines = |text: &Rope, pos: Position| -> ::std::ops::Range<usize> {
            let (char_idx, sel_start) = pos;

            if let Some(sel_start) = sel_start {
                let sel = self::select((sel_start, char_idx));

                text.char_to_line(sel.start)..
                (text.char_to_line(sel.end) + 1)
                    .min(text.len_lines())
            } else {
                let line = text.char_to_line(char_idx)
                    .min(text.len_lines().saturating_sub(1));
                line..line + 1
            }
        };

        match event {
            BufferEvent::Insert(ref text) => {
                for &mut (ref char_idx_signal, ref mut sel_start) in &mut self.positions {
                    if sel_start.is_some() { // delete and clear selection
                        let start_idx = sel_start.as_ref().unwrap().sample();
                        let end_idx = char_idx_signal.sample();
                        *sel_start = None;

                        buf.handle(buffer::Event::Remove {
                            char_idx_range: select((start_idx, end_idx))
                        });
                    }

                    buf.handle(buffer::Event::Insert {
                        char_idx: char_idx_signal.sample(),
                        text: text.clone() // XXX
                    });
                }
            }
            BufferEvent::Backspace => {
                for &mut (ref char_idx_signal, ref mut sel_start) in &mut self.positions {
                    let char_idx = char_idx_signal.sample();
                    if sel_start.is_some() {
                        let start_idx = sel_start.as_ref().unwrap().sample();
                        *sel_start = None;

                        buf.handle(buffer::Event::Remove {
                            char_idx_range: select((start_idx, char_idx))
                        });
                    } else if let (char_idx, false) = char_idx.overflowing_sub(1) {
                        buf.handle(buffer::Event::Remove {
                            char_idx_range: char_idx..char_idx + 1
                        });
                    }
                }
            }
            BufferEvent::Delete => {
                for &mut (ref char_idx_signal, ref mut sel_start) in &mut self.positions {
                    let char_idx = char_idx_signal.sample();
                    if sel_start.is_some() {
                        let start_idx = sel_start.as_ref().unwrap().sample();
                        *sel_start = None;

                        buf.handle(buffer::Event::Remove {
                            char_idx_range: select((start_idx, char_idx))
                        });
                    } else if char_idx < buf.text().len_chars() {
                        buf.handle(buffer::Event::Remove {
                            char_idx_range: char_idx..char_idx + 1
                        });
                    }
                }
            }
            BufferEvent::Indent(indent) => indent.sample_with(|indent| {
                if buf.text().len_chars() == 0 {
                    /* An empty Rope causes weird math afterwards
                     * (see docs of `char_to_line()` and `line_to_char()`)
                     * so we just handle this simple case here. */
                    buf.handle(buffer::Event::Insert {
                        char_idx: 0,
                        text: (*indent).clone() // XXX
                    })
                } else {
                    for pos in self.positions() {
                        for line_idx in pos_lines(buf.text(), pos) {
                            let line_start_idx = buf.text().line_to_char(line_idx);
                            buf.handle(buffer::Event::Insert {
                                char_idx: line_start_idx,
                                text: (*indent).clone() // XXX
                            })
                        }
                    }
                }
            }),
            BufferEvent::Dedent(indent) => indent.sample_with(|indent| {
                if buf.text().len_chars() == 0 { return } // nothing to dedent

                for pos in self.positions() {
                    for line_idx in pos_lines(buf.text(), pos) {
                        if {
                            let line = buf.text().line(line_idx);
                            line.starts_with(&indent)
                        } {
                            let line_start_idx = buf.text().line_to_char(line_idx);
                            buf.handle(buffer::Event::Remove {
                                char_idx_range:
                                    line_start_idx..
                                    line_start_idx + indent.chars().count()
                            });
                        }
                    }
                }
            })
        }
    }

    fn set_positions<'a, I>(&mut self, buf: &Buffer, positions: I)
    where I: IntoIterator<Item=&'a Position> {
        let positions = positions.into_iter()
            .map(|&(char_idx, sel_start)| (
                buf.signal_char_idx(char_idx),
                sel_start.map(|sel_start| buf.signal_char_idx(sel_start))
            ));

        self.positions.clear();
        self.positions.extend(positions);
    }
}

pub fn select(range: (usize, usize)) -> ::std::ops::Range<usize> {
    let (start_idx, end_idx) = range;
    if start_idx < end_idx {
        start_idx..end_idx
    } else {
        end_idx..start_idx
    }
}

/// Unused in this module but provided for completeness.
pub enum Event {
    Cur(CursorEvent),
    Buf(BufferEvent)
}

/// An event that mutates the cursor.
pub enum CursorEvent {
    /// Positions will be deduped for you.
    Jump(Vec<Position>)
}

/// An event that mutates the buffer.
pub enum BufferEvent {
    Insert(String),
    Backspace,
    Delete,
    Indent(Signal<String>), Dedent(Signal<String>)
}

pub struct Positions<'a> {
    items: &'a [PositionSignal],
    idx: usize,
    idx_back: usize
}

impl<'a> Positions<'a> {
    pub fn new(positions: &'a [PositionSignal]) -> Self {
        Positions {
            items: positions,
            idx: 0,
            idx_back: 0
        }
    }
}

impl<'a> Iterator for Positions<'a> {
    type Item = Position;

    fn next(&mut self) -> Option<Self::Item> {
        if self.idx < self.items.len() - self.idx_back {
            let item = &self.items[self.idx];
            self.idx += 1;
            Some((
                item.0.sample(),
                match item.1 {
                    Some(ref signal) => Some(signal.sample()),
                    None             => None
                }
            ))
        } else {
            None
        }
    }
}

impl<'a> DoubleEndedIterator for Positions<'a> {
    fn next_back(&mut self) -> Option<Self::Item> {
        let len = self.items.len();

        if self.idx_back < len - self.idx {
            let item = &self.items[len - 1 - self.idx_back];
            self.idx_back += 1;
            Some((
                item.0.sample(),
                match item.1 {
                    Some(ref signal) => Some(signal.sample()),
                    None             => None
                }
            ))
        } else {
            None
        }
    }
}

impl<'a> ExactSizeIterator for Positions<'a> {
    fn len(&self) -> usize {
        self.items.len()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    const TEXT: &str = concat!(
        "This is line number 1\n",
        "Soon follows line number 2\n",
        "Which, in turn, is followed by line number 3\n",
        "In close call to line number 4\n",
        "Within proximity of line number 5\n",
        "Right beside line number 6\n"
    );

    #[test]
    fn new() {
        let mut buf = Buffer::from_str(TEXT);
        let positions: Vec<Position> = TEXT.lines().enumerate()
            .map(|(line_idx, _)| (buf.text().line_to_char(line_idx), None))
            .collect();
        let cur = Cursor::new(&mut buf, positions.iter());

        assert_eq!(positions, cur.positions().collect::<Vec<_>>());
    }

    #[test]
    fn insert() {
        let mut buf = Buffer::from_str(TEXT);
        let mut cur = Cursor::new(&mut buf, &[(0, None)]);

        const CHARS: [char; 17] = [
            'L', 'i', 'n', 'e', 's', ' ',
            'b', 'e', 'g', 'i', 'n', ' ',
            'a', 't', ' ', '0', '\n'
        ];
        for char in &CHARS {
            cur.handle_buf(&mut buf, BufferEvent::Insert(char.to_string()));
        }
        assert_eq!(CHARS.iter().collect::<String>(), buf.text().line(0).to_string());

        const LINE_IDC: [usize; 2] = [0, 1];
        let positions = LINE_IDC.iter()
            .map(|line_idx| (buf.text().line_to_char(*line_idx), None))
            .collect::<Vec<_>>();
        cur.set_positions(&mut buf, &positions);
        const PREFIX: &str = "->";
        cur.handle_buf(&mut buf, BufferEvent::Insert(PREFIX.to_owned()));
        for line_idx in &LINE_IDC {
            assert!(buf.text().line(*line_idx).to_string().starts_with(PREFIX));
        }
    }

    #[test]
    fn backspace() {
        let mut buf = Buffer::from_str(TEXT);
        let mut cur = Cursor::new(&mut buf, &[(0, None)]);

        cur.handle_buf(&mut buf, BufferEvent::Backspace);
        assert_eq!("This is line number 1\n", buf.text().line(0).to_string());

        let positions = [(buf.text().len_chars(), None)];
        cur.set_positions(&mut buf, &positions);
        cur.handle_buf(&mut buf, BufferEvent::Backspace);
        let line_idx = buf.text().len_lines() - 1;
        assert_eq!("Right beside line number 6", buf.text().line(line_idx).to_string());

        for _ in 0..9 {
            cur.handle_buf(&mut buf, BufferEvent::Backspace);
        }
        assert_eq!("Right beside line", buf.text().line(line_idx).to_string());

        const LINE_IDC: [usize; 2] = [2, 3];
        let positions = LINE_IDC.iter()
            // set cursor to end of line
            .map(|line_idx| (buf.text().line_to_char(*line_idx + 1) - 1, None))
            .collect::<Vec<_>>();
        cur.set_positions(&mut buf, &positions);
        cur.handle_buf(&mut buf, BufferEvent::Backspace);
        cur.handle_buf(&mut buf, BufferEvent::Backspace);
        for line_idx in &LINE_IDC {
            assert!(buf.text().line(*line_idx).to_string().ends_with("line number\n"));
        }
    }
}