pepper 0.31.0

A simple and opinionated modal code editor for your terminal
Documentation
use crate::{
    buffer::BufferHandle,
    buffer_position::BufferPosition,
    buffer_view::{BufferView, BufferViewCollection},
    client::Client,
    cursor::Cursor,
    editor::Editor,
};

#[derive(Clone, Copy)]
pub enum NavigationMovement {
    Forward,
    Backward,
}

#[derive(Clone)]
struct NavigationHistorySnapshot {
    pub buffer_handle: BufferHandle,
    pub position: BufferPosition,
}

#[derive(Default)]
pub struct NavigationHistory {
    snapshots: Vec<NavigationHistorySnapshot>,
    current_snapshot_index: u32,
    on_previous_buffer: bool,
}

impl NavigationHistory {
    pub fn clear(&mut self) {
        self.snapshots.clear();
        self.current_snapshot_index = 0;
        self.on_previous_buffer = false;
    }

    pub fn save_snapshot(client: &mut Client, buffer_views: &BufferViewCollection) {
        let buffer_view_handle = match client.buffer_view_handle() {
            Some(handle) => handle,
            None => return,
        };
        let buffer_view = buffer_views.get(buffer_view_handle);

        let this = &mut client.navigation_history;
        if this.on_previous_buffer {
            this.current_snapshot_index = this.snapshots.len() as _;
        }
        this.snapshots.truncate(this.current_snapshot_index as _);
        this.on_previous_buffer = false;

        let buffer_handle = buffer_view.buffer_handle;
        let position = buffer_view.cursors.main_cursor().position;

        if this
            .snapshots
            .last()
            .map(|s| s.buffer_handle == buffer_handle && s.position == position)
            .unwrap_or(false)
        {
            return;
        }

        this.snapshots.push(NavigationHistorySnapshot {
            buffer_handle,
            position,
        });
        this.current_snapshot_index = this.snapshots.len() as _;
    }

    pub fn move_in_history(client: &mut Client, editor: &mut Editor, movement: NavigationMovement) {
        match movement {
            NavigationMovement::Forward => {
                if client.navigation_history.current_snapshot_index + 1
                    >= client.navigation_history.snapshots.len() as _
                {
                    return;
                }

                client.navigation_history.current_snapshot_index += 1;
            }
            NavigationMovement::Backward => {
                if client.navigation_history.current_snapshot_index == 0 {
                    return;
                }

                if client.navigation_history.current_snapshot_index
                    == client.navigation_history.snapshots.len() as _
                {
                    Self::save_snapshot(client, &editor.buffer_views);
                    if client.navigation_history.current_snapshot_index > 1 {
                        client.navigation_history.current_snapshot_index -= 1;
                    }
                }

                client.navigation_history.current_snapshot_index -= 1;
            }
        }

        let snapshot = &client.navigation_history.snapshots
            [client.navigation_history.current_snapshot_index as usize];

        let position = editor
            .buffers
            .get(snapshot.buffer_handle)
            .content()
            .saturate_position(snapshot.position);

        let buffer_view_handle = editor
            .buffer_views
            .buffer_view_handle_from_buffer_handle(client.handle(), snapshot.buffer_handle);

        let mut cursors = editor
            .buffer_views
            .get_mut(buffer_view_handle)
            .cursors
            .mut_guard();
        cursors.clear();
        cursors.add(Cursor {
            anchor: position,
            position,
        });

        client.set_buffer_view_handle_no_history(Some(buffer_view_handle));
        client.navigation_history.on_previous_buffer = false;
    }

    pub fn move_to_previous_buffer(client: &mut Client, editor: &mut Editor) {
        fn save_snapshot_if_current_buffer_is_different_from_last(
            client: &mut Client,
            buffer_view: &BufferView,
        ) {
            if let Some(current_snapshot) = client
                .navigation_history
                .snapshots
                .get(client.navigation_history.current_snapshot_index as usize)
            {
                if current_snapshot.buffer_handle == buffer_view.buffer_handle {
                    return;
                }
            }

            client
                .navigation_history
                .snapshots
                .push(NavigationHistorySnapshot {
                    buffer_handle: buffer_view.buffer_handle,
                    position: buffer_view.cursors.main_cursor().position,
                });
        }

        let current_buffer_view = client
            .buffer_view_handle()
            .map(|h| editor.buffer_views.get(h));

        if let Some(current_buffer_view) = current_buffer_view {
            save_snapshot_if_current_buffer_is_different_from_last(client, current_buffer_view);
        }

        let current_buffer_handle = current_buffer_view.map(|v| v.buffer_handle);

        for (i, snapshot) in client.navigation_history.snapshots.iter().enumerate().rev() {
            if current_buffer_handle != Some(snapshot.buffer_handle) {
                let buffer_view_handle = editor
                    .buffer_views
                    .buffer_view_handle_from_buffer_handle(client.handle(), snapshot.buffer_handle);
                client.set_buffer_view_handle_no_history(Some(buffer_view_handle));
                client.navigation_history.current_snapshot_index = i as _;
                client.navigation_history.on_previous_buffer = true;
                break;
            }
        }
    }

    pub fn remove_snapshots_with_buffer_handle(&mut self, buffer_handle: BufferHandle) {
        for i in (0..self.snapshots.len()).rev() {
            let snapshot = self.snapshots[i].clone();
            if snapshot.buffer_handle == buffer_handle {
                self.snapshots.remove(i);

                if self.current_snapshot_index > 0 && i <= self.current_snapshot_index as _ {
                    self.current_snapshot_index -= 1;
                }
            }
        }
    }
}

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

    use std::path::PathBuf;

    fn setup() -> (Editor, Client) {
        let mut client = Client::new();
        let mut editor = Editor::new(PathBuf::new(), String::new());

        let buffer_a = editor.buffers.add_new();
        assert_eq!(0, buffer_a.handle().0);
        let buffer_b = editor.buffers.add_new();
        assert_eq!(1, buffer_b.handle().0);
        let buffer_c = editor.buffers.add_new();
        assert_eq!(2, buffer_c.handle().0);

        let view_a = editor
            .buffer_views
            .add_new(client.handle(), BufferHandle(0));
        let view_b = editor
            .buffer_views
            .add_new(client.handle(), BufferHandle(1));
        let view_c = editor
            .buffer_views
            .add_new(client.handle(), BufferHandle(2));

        NavigationHistory::save_snapshot(&mut client, &editor.buffer_views);
        client.set_buffer_view_handle_no_history(Some(view_a));
        NavigationHistory::save_snapshot(&mut client, &editor.buffer_views);
        client.set_buffer_view_handle_no_history(Some(view_b));
        NavigationHistory::save_snapshot(&mut client, &editor.buffer_views);
        client.set_buffer_view_handle_no_history(Some(view_c));

        (editor, client)
    }

    fn buffer_index(client: &Client, editor: &Editor) -> usize {
        let buffer_view_handle = client.buffer_view_handle().unwrap();
        let buffer_view = editor.buffer_views.get(buffer_view_handle);
        buffer_view.buffer_handle.0 as _
    }

    #[test]
    fn move_back_and_forward_in_history() {
        let (mut editor, mut client) = setup();

        assert_eq!(2, client.navigation_history.current_snapshot_index);
        assert_eq!(2, buffer_index(&client, &editor));

        NavigationHistory::move_in_history(&mut client, &mut editor, NavigationMovement::Forward);
        assert_eq!(2, client.navigation_history.current_snapshot_index);
        assert_eq!(2, buffer_index(&client, &editor));

        NavigationHistory::move_in_history(&mut client, &mut editor, NavigationMovement::Backward);
        assert_eq!(1, client.navigation_history.current_snapshot_index);
        assert_eq!(1, buffer_index(&client, &editor));

        NavigationHistory::move_in_history(&mut client, &mut editor, NavigationMovement::Backward);
        assert_eq!(0, client.navigation_history.current_snapshot_index);
        assert_eq!(0, buffer_index(&client, &editor));

        NavigationHistory::move_in_history(&mut client, &mut editor, NavigationMovement::Backward);
        assert_eq!(0, client.navigation_history.current_snapshot_index);
        assert_eq!(0, buffer_index(&client, &editor));

        NavigationHistory::move_in_history(&mut client, &mut editor, NavigationMovement::Forward);
        assert_eq!(1, client.navigation_history.current_snapshot_index);
        assert_eq!(1, buffer_index(&client, &editor));

        assert_eq!(3, client.navigation_history.snapshots.len());
    }

    #[test]
    fn move_to_previous_buffer_three_times() {
        let (mut editor, mut client) = setup();

        assert_eq!(2, buffer_index(&client, &editor));

        NavigationHistory::move_to_previous_buffer(&mut client, &mut editor);
        assert_eq!(1, buffer_index(&client, &editor));

        NavigationHistory::move_to_previous_buffer(&mut client, &mut editor);
        assert_eq!(2, buffer_index(&client, &editor));

        NavigationHistory::move_to_previous_buffer(&mut client, &mut editor);
        assert_eq!(1, buffer_index(&client, &editor));
    }

    #[test]
    fn navigate_back_then_move_to_previous_buffer() {
        let (mut editor, mut client) = setup();

        NavigationHistory::move_in_history(&mut client, &mut editor, NavigationMovement::Backward);
        assert_eq!(1, client.navigation_history.current_snapshot_index);
        assert_eq!(1, buffer_index(&client, &editor));

        NavigationHistory::move_in_history(&mut client, &mut editor, NavigationMovement::Backward);
        assert_eq!(0, client.navigation_history.current_snapshot_index);
        assert_eq!(0, buffer_index(&client, &editor));

        NavigationHistory::move_in_history(&mut client, &mut editor, NavigationMovement::Forward);
        assert_eq!(1, client.navigation_history.current_snapshot_index);
        assert_eq!(1, buffer_index(&client, &editor));

        assert_eq!(3, client.navigation_history.snapshots.len());

        NavigationHistory::move_to_previous_buffer(&mut client, &mut editor);
        assert_eq!(2, client.navigation_history.current_snapshot_index);
        assert_eq!(2, buffer_index(&client, &editor));

        assert_eq!(3, client.navigation_history.snapshots.len());

        NavigationHistory::move_to_previous_buffer(&mut client, &mut editor);
        assert_eq!(1, client.navigation_history.current_snapshot_index);
        assert_eq!(1, buffer_index(&client, &editor));

        NavigationHistory::move_to_previous_buffer(&mut client, &mut editor);
        assert_eq!(2, client.navigation_history.current_snapshot_index);
        assert_eq!(2, buffer_index(&client, &editor));

        assert_eq!(3, client.navigation_history.snapshots.len());
    }
}