logisheets_controller 0.7.0

the core of LogiSheets
Documentation
use std::collections::HashSet;

use logisheets_base::async_func::{AsyncCalcResult, Task};
use logisheets_base::{BlockRange, CellId, NormalRange, Range, SheetId};

use logisheets_workbook::prelude::{read, write};
pub mod display;
mod executor;
pub mod status;
pub mod style;
mod viewer;
use crate::edit_action::{
    ActionEffect, CreateSheet, EditAction, PayloadsAction, RecalcCell, StatusCode,
    WorkbookUpdateType,
};
use crate::errors::{Error, Result};
use crate::file_loader2::load;
use crate::file_saver::save_file;
use crate::formula_manager::Vertex;
use crate::settings::Settings;
use crate::version_manager::VersionManager;
use executor::Executor;
use status::Status;
use viewer::SheetViewer;

use self::display::{DisplayResponse, DisplaySheetRequest, SheetInfo};
use crate::async_func_manager::AsyncFuncManager;

pub struct Controller {
    pub status: Status,
    pub async_func_manager: AsyncFuncManager,
    pub curr_book_name: String,
    pub settings: Settings,
    pub version_manager: VersionManager,
}

impl Default for Controller {
    fn default() -> Self {
        let mut empty = Controller {
            status: Status::default(),
            curr_book_name: String::from("Book1"),
            settings: Settings::default(),
            version_manager: VersionManager::default(),
            async_func_manager: AsyncFuncManager::default(),
        };
        let add_sheet = PayloadsAction::new(false).add_payload(CreateSheet {
            idx: 0,
            new_name: String::from("Sheet1"),
        });
        empty.handle_action(EditAction::Payloads(add_sheet));
        empty
    }
}

impl Controller {
    // TODO: Due to the UUID generating, we can't just `assert_eq!(file1, file2)` where
    // `file1` and `file2` are binary got from saving of the same file. Fix it.
    pub fn save(&self) -> Result<Vec<u8>> {
        let workbook = save_file(self)?;
        write(workbook).map_err(|e| Error::Serde(e.into()))
    }

    pub fn from(status: Status, book_name: String, settings: Settings) -> Self {
        Controller {
            curr_book_name: book_name,
            settings,
            status,
            version_manager: VersionManager::default(),
            async_func_manager: AsyncFuncManager::default(),
        }
    }

    pub fn version(&self) -> u32 {
        self.version_manager.version()
    }

    pub fn from_file(name: String, f: &[u8]) -> Result<Self> {
        let res = read(f)?;
        Ok(load(res, name))
    }

    pub fn get_sheet_id_by_idx(&self, idx: usize) -> Option<SheetId> {
        self.status.sheet_pos_manager.get_sheet_id(idx)
    }

    pub fn get_sheet_id_by_name(&self, name: &str) -> Option<SheetId> {
        self.status.sheet_id_manager.has(name)
    }

    pub fn get_all_sheet_info(&self) -> Vec<SheetInfo> {
        let id_manager = &self.status.sheet_id_manager;
        let pos_manager = &self.status.sheet_pos_manager;
        pos_manager
            .pos
            .iter()
            .map(|id| SheetInfo {
                name: id_manager.get_string(id).unwrap_or(String::new()),
                id: *id,
                hidden: pos_manager.is_hidden(id),
                tab_color: String::from(""),
            })
            .collect()
    }

    // Handle an action and get the affected sheet indices.
    pub fn handle_action(&mut self, action: EditAction) -> ActionEffect {
        match action {
            EditAction::Undo => {
                let c = if self.undo() {
                    WorkbookUpdateType::Undo
                } else {
                    WorkbookUpdateType::UndoNothing
                };
                ActionEffect::from(0, vec![], c)
            }
            EditAction::Redo => {
                let c = if self.redo() {
                    WorkbookUpdateType::Redo
                } else {
                    WorkbookUpdateType::RedoNothing
                };
                ActionEffect::from(0, vec![], c)
            }
            EditAction::Payloads(payloads_action) => {
                let executor = Executor {
                    status: self.status.clone(),
                    version_manager: &mut self.version_manager,
                    async_func_manager: &mut self.async_func_manager,
                    book_name: &self.curr_book_name,
                    calc_config: self.settings.calc_config,
                    async_funcs: &self.settings.async_funcs,
                    updated_cells: HashSet::new(),
                    dirty_vertices: HashSet::new(),
                    sheet_updated: false,
                    cell_updated: false,
                };

                let result = executor.execute_and_calc(payloads_action);
                match result {
                    Ok(result) => {
                        let c = if result.cell_updated && result.sheet_updated {
                            WorkbookUpdateType::SheetAndCell
                        } else if result.cell_updated {
                            WorkbookUpdateType::Cell
                        } else if result.sheet_updated {
                            WorkbookUpdateType::Sheet
                        } else {
                            WorkbookUpdateType::DoNothing
                        };
                        self.status = result.status;
                        ActionEffect {
                            version: result.version_manager.version(),
                            async_tasks: result.async_func_manager.get_calc_tasks(),
                            status: StatusCode::Ok(c),
                        }
                    }
                    Err(e) => {
                        println!("{:?}", e.to_string());
                        ActionEffect::from_err(1) // todo
                    }
                }
            }
            EditAction::Recalc(cells) => {
                let dirty_vertices = cells
                    .into_iter()
                    .map(|recalc_sheet| {
                        let sheet_id = recalc_sheet.sheet_id;
                        let cell_id = recalc_sheet.cell_id;
                        let range = match cell_id {
                            CellId::NormalCell(c) => Range::Normal(NormalRange::Single(c)),
                            CellId::BlockCell(b) => Range::Block(BlockRange::Single(b)),
                        };
                        let range_id = self.status.range_manager.get_range_id(&sheet_id, &range);
                        Vertex::Range(sheet_id, range_id)
                    })
                    .collect::<HashSet<_>>();
                let executor = Executor {
                    status: self.status.clone(),
                    version_manager: &mut self.version_manager,
                    async_func_manager: &mut self.async_func_manager,
                    book_name: &self.curr_book_name,
                    calc_config: self.settings.calc_config,
                    async_funcs: &self.settings.async_funcs,
                    updated_cells: HashSet::new(),
                    dirty_vertices,
                    sheet_updated: false,
                    cell_updated: false,
                };
                if let Ok(result) = executor.calc() {
                    ActionEffect {
                        version: result.version_manager.version(),
                        async_tasks: vec![],
                        status: StatusCode::Ok(WorkbookUpdateType::Cell),
                    }
                } else {
                    ActionEffect::from_err(1)
                }
            }
        }
    }

    pub fn handle_async_calc_results(
        &mut self,
        tasks: Vec<Task>,
        res: Vec<AsyncCalcResult>,
    ) -> ActionEffect {
        let mut pending_cells = vec![];
        tasks.into_iter().zip(res.into_iter()).for_each(|(t, r)| {
            let cells = self
                .async_func_manager
                .commit_value(t, r)
                .into_iter()
                .map(|(s, c)| RecalcCell {
                    sheet_id: s,
                    cell_id: c,
                });
            pending_cells.extend(cells);
        });
        self.handle_action(EditAction::Recalc(pending_cells))
    }

    pub fn get_display_sheet_response(&self, req: DisplaySheetRequest) -> Result<DisplayResponse> {
        let viewer = SheetViewer::default();
        if req.version == 0 {
            return Ok(viewer.display_with_idx(self, req.sheet_idx));
        }

        let sheet_id = self
            .get_sheet_id_by_idx(req.sheet_idx)
            .ok_or(Error::UnavailableSheetIdx(req.sheet_idx))?;
        if let Some(diff) = self
            .version_manager
            .get_sheet_diffs_from_version(sheet_id, req.version)
        {
            Ok(viewer.display_with_diff(self, sheet_id, diff))
        } else {
            Ok(viewer.display_with_idx(self, req.sheet_idx))
        }
    }

    pub fn undo(&mut self) -> bool {
        match self.version_manager.undo() {
            Some(mut last_status) => {
                std::mem::swap(&mut self.status, &mut last_status);
                true
            }
            None => false,
        }
    }

    pub fn redo(&mut self) -> bool {
        match self.version_manager.redo() {
            Some(mut next_status) => {
                std::mem::swap(&mut self.status, &mut next_status);
                true
            }
            None => false,
        }
    }
}

#[cfg(test)]
mod tests {
    use crate::edit_action::{CellInput, EditAction, EditPayload, PayloadsAction};

    use super::Controller;

    #[test]
    fn controller_default_test() {
        let wb = Controller::default();
        println!("{:?}", wb.status);
    }

    #[test]
    fn controller_input_formula() {
        let mut wb = Controller::default();
        let payloads_action = PayloadsAction {
            payloads: vec![EditPayload::CellInput(CellInput {
                sheet_idx: 0,
                row: 0,
                col: 0,
                content: String::from("=ABS(1)"),
            })],
            undoable: true,
        };
        wb.handle_action(EditAction::Payloads(payloads_action));
        let len = wb.status.formula_manager.formulas.len();
        assert_eq!(len, 1);
    }

    #[test]
    fn from_file_test() {
        use std::fs;
        let buf = fs::read("../../tests/6.xlsx").unwrap();
        let controller = Controller::from_file(String::from("6"), &buf);
        match controller {
            Ok(_) => {}
            Err(_) => {
                panic!()
            }
        }
    }
}