pepper 0.31.0

A simple and opinionated modal code editor for your terminal
Documentation
use std::{fmt, path::Path};

use crate::{
    buffer::{BufferCollection, BufferHandle, BufferProperties, CharDisplayDistances},
    buffer_position::BufferPositionIndex,
    buffer_view::{BufferView, BufferViewCollection, BufferViewHandle},
    editor::Editor,
    editor_utils::ResidualStrBytes,
    navigation_history::{NavigationHistory, NavigationMovement},
    serialization::{DeserializeError, Deserializer, Serialize, Serializer},
};

#[derive(Clone, Copy, Eq, PartialEq)]
pub struct ClientHandle(pub u8);

impl<'de> Serialize<'de> for ClientHandle {
    fn serialize(&self, serializer: &mut dyn Serializer) {
        self.0.serialize(serializer);
    }

    fn deserialize(deserializer: &mut dyn Deserializer<'de>) -> Result<Self, DeserializeError> {
        Ok(Self(u8::deserialize(deserializer)?))
    }
}

pub enum ViewAnchor {
    Top,
    Center,
    Bottom,
}

pub struct Client {
    active: bool,
    handle: ClientHandle,

    pub viewport_size: (u16, u16),

    pub(crate) navigation_history: NavigationHistory,

    buffer_view_handle: Option<BufferViewHandle>,
    stdin_buffer_handle: Option<BufferHandle>,
    stdin_residual_bytes: ResidualStrBytes,
}

impl Client {
    pub(crate) fn new() -> Self {
        Self {
            active: false,
            handle: ClientHandle(0),

            viewport_size: (0, 0),

            navigation_history: NavigationHistory::default(),

            buffer_view_handle: None,
            stdin_buffer_handle: None,
            stdin_residual_bytes: ResidualStrBytes::default(),
        }
    }

    fn dispose(&mut self) {
        self.active = false;

        self.viewport_size = (0, 0);

        self.navigation_history.clear();

        self.buffer_view_handle = None;
        self.stdin_buffer_handle = None;
        self.stdin_residual_bytes = ResidualStrBytes::default();
    }

    pub fn handle(&self) -> ClientHandle {
        self.handle
    }

    pub fn buffer_view_handle(&self) -> Option<BufferViewHandle> {
        self.buffer_view_handle
    }

    pub fn stdin_buffer_handle(&self) -> Option<BufferHandle> {
        self.stdin_buffer_handle
    }

    pub fn set_buffer_view_handle(
        &mut self,
        handle: Option<BufferViewHandle>,
        buffer_views: &BufferViewCollection,
    ) {
        NavigationHistory::save_snapshot(self, buffer_views);
        self.set_buffer_view_handle_no_history(handle);
    }

    pub(crate) fn set_buffer_view_handle_no_history(&mut self, handle: Option<BufferViewHandle>) {
        self.buffer_view_handle = handle;
    }

    pub fn has_ui(&self) -> bool {
        self.viewport_size.0 != 0 && self.viewport_size.1 != 0
    }

    pub fn set_view_anchor(&self, editor: &mut Editor, anchor: ViewAnchor) {
        if !self.has_ui() {
            return;
        }

        if let Some(buffer_view_handle) = self.buffer_view_handle {
            let height = self.viewport_size.1.saturating_sub(1) as usize;
            let height_offset = match anchor {
                ViewAnchor::Top => 0,
                ViewAnchor::Center => height / 2,
                ViewAnchor::Bottom => height.saturating_sub(1),
            };

            let buffer_view = editor.buffer_views.get_mut(buffer_view_handle);
            let main_cursor_padding_top = self.find_main_cursor_padding_top(
                buffer_view,
                &editor.buffers,
                editor.config.tab_size,
            );
            buffer_view.scroll = main_cursor_padding_top.saturating_sub(height_offset) as _;
        }
    }

    pub(crate) fn scroll_to_main_cursor(
        &self,
        buffer_views: &mut BufferViewCollection,
        buffers: &BufferCollection,
        tab_size: u8,
        margin_bottom: usize,
    ) -> BufferPositionIndex {
        if !self.has_ui() {
            return 0;
        }

        let height = self.viewport_size.1.saturating_sub(1) as usize;
        let height = height.saturating_sub(margin_bottom);
        let half_height = height / 2;

        match self.buffer_view_handle {
            Some(buffer_view_handle) => {
                let buffer_view = buffer_views.get_mut(buffer_view_handle);
                let main_cursor_padding_top =
                    self.find_main_cursor_padding_top(buffer_view, buffers, tab_size);

                let mut scroll = buffer_view.scroll as usize;
                if main_cursor_padding_top < scroll.saturating_sub(half_height) {
                    scroll = main_cursor_padding_top.saturating_sub(half_height) as _;
                } else if main_cursor_padding_top < scroll {
                    scroll = main_cursor_padding_top as _;
                } else if main_cursor_padding_top >= scroll + height + half_height {
                    scroll = (main_cursor_padding_top + 1 - half_height) as _;
                } else if main_cursor_padding_top >= scroll + height {
                    scroll = (main_cursor_padding_top + 1 - height) as _;
                }
                let scroll = scroll as _;
                buffer_view.scroll = scroll;
                scroll
            }
            None => 0,
        }
    }

    pub(crate) fn on_stdin_input(&mut self, editor: &mut Editor, bytes: &[u8]) {
        let mut buf = Default::default();
        let texts = self.stdin_residual_bytes.receive_bytes(&mut buf, bytes);

        let buffer_handle = match self.stdin_buffer_handle() {
            Some(handle) => handle,
            None => {
                use fmt::Write;

                let buffer = editor.buffers.add_new();

                let mut path = editor.string_pool.acquire_with("pipe.");
                let _ = write!(path, "{}", self.handle().0);
                buffer.set_path(Path::new(&path));
                editor.string_pool.release(path);

                buffer.properties = BufferProperties::text();
                buffer.properties.file_backed_enabled = false;

                let buffer_view_handle =
                    editor.buffer_views.add_new(self.handle(), buffer.handle());
                self.set_buffer_view_handle(Some(buffer_view_handle), &editor.buffer_views);

                self.stdin_buffer_handle = Some(buffer.handle());
                buffer.handle()
            }
        };

        let buffer = editor.buffers.get_mut(buffer_handle);
        let mut events = editor
            .events
            .writer()
            .buffer_text_inserts_mut_guard(buffer_handle);
        for text in texts {
            let position = buffer.content().end();
            buffer.insert_text(&mut editor.word_database, position, text, &mut events);
        }
    }

    pub(crate) fn on_buffer_close(&mut self, editor: &mut Editor, buffer_handle: BufferHandle) {
        self.navigation_history
            .remove_snapshots_with_buffer_handle(buffer_handle);

        if let Some(handle) = self.buffer_view_handle {
            let buffer_view = editor.buffer_views.get(handle);
            if buffer_view.buffer_handle == buffer_handle {
                self.buffer_view_handle = None;
                NavigationHistory::move_in_history(self, editor, NavigationMovement::Backward);
                NavigationHistory::move_in_history(self, editor, NavigationMovement::Forward);
            }
        }

        if self.stdin_buffer_handle == Some(buffer_handle) {
            self.stdin_buffer_handle = None;
        }
    }

    fn find_main_cursor_padding_top(
        &self,
        buffer_view: &BufferView,
        buffers: &BufferCollection,
        tab_size: u8,
    ) -> usize {
        let width = self.viewport_size.0 as usize;

        let buffer = buffers.get(buffer_view.buffer_handle).content();
        let position = buffer_view.cursors.main_cursor().position;

        let mut height = position.line_index as usize;
        for display_len in &buffer.line_display_lens()[..position.line_index as usize] {
            height += display_len.total_len(tab_size) / width;
        }

        let cursor_line = buffer.lines()[position.line_index as usize].as_str();
        let cursor_line = &cursor_line[..position.column_byte_index as usize];
        if let Some(d) = CharDisplayDistances::new(cursor_line, tab_size).last() {
            height += d.distance as usize / width;
        }

        height
    }
}

#[derive(Default)]
pub struct ClientManager {
    focused_client: Option<ClientHandle>,
    previous_focused_client: Option<ClientHandle>,
    clients: Vec<Client>,
}

impl ClientManager {
    pub fn focused_client(&self) -> Option<ClientHandle> {
        self.focused_client
    }

    pub fn previous_focused_client(&self) -> Option<ClientHandle> {
        self.previous_focused_client
    }

    pub fn focus_client(&mut self, handle: ClientHandle) -> bool {
        let client = self.get(handle);
        if !client.has_ui() {
            return false;
        }

        let changed = Some(handle) != self.focused_client;
        if changed {
            self.previous_focused_client = self.focused_client;
        }
        self.focused_client = Some(handle);
        changed
    }

    pub fn get(&self, handle: ClientHandle) -> &Client {
        &self.clients[handle.0 as usize]
    }

    pub fn get_mut(&mut self, handle: ClientHandle) -> &mut Client {
        &mut self.clients[handle.0 as usize]
    }

    pub fn iter(&self) -> impl Clone + Iterator<Item = &Client> {
        self.clients.iter().filter(|c| c.active)
    }

    pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut Client> {
        self.clients.iter_mut().filter(|c| c.active)
    }

    pub(crate) fn on_client_joined(&mut self, handle: ClientHandle) {
        let min_len = handle.0 as usize + 1;
        if min_len > self.clients.len() {
            self.clients.resize_with(min_len, Client::new);
        }

        let client = &mut self.clients[handle.0 as usize];
        client.active = true;
        client.handle = handle;
    }

    pub(crate) fn on_client_left(&mut self, handle: ClientHandle) {
        self.clients[handle.0 as usize].dispose();
        if self.focused_client == Some(handle) {
            self.focused_client = None;
        }
    }
}