use super::{NotebookCell, CellId, CellType, NotebookError, NotebookResult};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use std::time::SystemTime;
use uuid::Uuid;
pub type NotebookId = Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NotebookMetadata {
pub title: String,
pub author: Option<String>,
pub description: Option<String>,
pub created_at: SystemTime,
pub modified_at: SystemTime,
pub last_saved: Option<SystemTime>,
pub version: String,
pub language: String,
pub properties: HashMap<String, String>,
pub tags: Vec<String>,
}
impl Default for NotebookMetadata {
fn default() -> Self {
let now = SystemTime::now();
Self {
title: "未命名笔记本".to_string(),
author: None,
description: None,
created_at: now,
modified_at: now,
last_saved: None,
version: "1.0".to_string(),
language: "zh-CN".to_string(),
properties: HashMap::new(),
tags: Vec::new(),
}
}
}
impl NotebookMetadata {
pub fn mark_modified(&mut self) {
self.modified_at = SystemTime::now();
}
pub fn mark_saved(&mut self) {
self.last_saved = Some(SystemTime::now());
}
pub fn needs_save(&self) -> bool {
match self.last_saved {
Some(saved_time) => self.modified_at > saved_time,
None => true,
}
}
pub fn add_tag(&mut self, tag: String) {
if !self.tags.contains(&tag) {
self.tags.push(tag);
self.mark_modified();
}
}
pub fn remove_tag(&mut self, tag: &str) {
if let Some(pos) = self.tags.iter().position(|t| t == tag) {
self.tags.remove(pos);
self.mark_modified();
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Notebook {
pub id: NotebookId,
pub metadata: NotebookMetadata,
pub cells: Vec<NotebookCell>,
pub file_path: Option<PathBuf>,
}
impl Notebook {
pub fn new() -> Self {
Self {
id: Uuid::new_v4(),
metadata: NotebookMetadata::default(),
cells: Vec::new(),
file_path: None,
}
}
pub fn with_title(title: String) -> Self {
let mut notebook = Self::new();
notebook.metadata.title = title;
notebook
}
pub fn cell_count(&self) -> usize {
self.cells.len()
}
pub fn is_empty(&self) -> bool {
self.cells.is_empty()
}
pub fn add_cell(&mut self, cell: NotebookCell) {
self.cells.push(cell);
self.metadata.mark_modified();
}
pub fn insert_cell(&mut self, index: usize, cell: NotebookCell) -> NotebookResult<()> {
if index > self.cells.len() {
return Err(NotebookError::Cell(format!("索引 {} 超出范围", index)));
}
self.cells.insert(index, cell);
self.metadata.mark_modified();
Ok(())
}
pub fn remove_cell(&mut self, index: usize) -> NotebookResult<NotebookCell> {
if index >= self.cells.len() {
return Err(NotebookError::Cell(format!("索引 {} 超出范围", index)));
}
let cell = self.cells.remove(index);
self.metadata.mark_modified();
Ok(cell)
}
pub fn find_cell(&self, cell_id: &CellId) -> Option<(usize, &NotebookCell)> {
self.cells
.iter()
.enumerate()
.find(|(_, cell)| &cell.id == cell_id)
}
pub fn find_cell_mut(&mut self, cell_id: &CellId) -> Option<(usize, &mut NotebookCell)> {
self.cells
.iter_mut()
.enumerate()
.find(|(_, cell)| &cell.id == cell_id)
}
pub fn get_cell(&self, index: usize) -> Option<&NotebookCell> {
self.cells.get(index)
}
pub fn get_cell_mut(&mut self, index: usize) -> Option<&mut NotebookCell> {
if index < self.cells.len() {
self.metadata.mark_modified();
}
self.cells.get_mut(index)
}
pub fn get_cells(&self) -> &[NotebookCell] {
&self.cells
}
pub fn move_cell(&mut self, from: usize, to: usize) -> NotebookResult<()> {
if from >= self.cells.len() || to >= self.cells.len() {
return Err(NotebookError::Cell("索引超出范围".to_string()));
}
if from != to {
let cell = self.cells.remove(from);
self.cells.insert(to, cell);
self.metadata.mark_modified();
}
Ok(())
}
pub fn duplicate_cell(&mut self, index: usize) -> NotebookResult<()> {
if index >= self.cells.len() {
return Err(NotebookError::Cell(format!("索引 {} 超出范围", index)));
}
let duplicated = self.cells[index].duplicate();
self.cells.insert(index + 1, duplicated);
self.metadata.mark_modified();
Ok(())
}
pub fn get_code_cells(&self) -> Vec<(usize, &NotebookCell)> {
self.cells
.iter()
.enumerate()
.filter(|(_, cell)| cell.cell_type == CellType::Code)
.collect()
}
pub fn get_dirty_cells(&self) -> Vec<(usize, &NotebookCell)> {
self.cells
.iter()
.enumerate()
.filter(|(_, cell)| cell.needs_execution())
.collect()
}
pub fn clear_all_outputs(&mut self) {
for cell in &mut self.cells {
if cell.is_executable() {
cell.clear_output();
}
}
self.metadata.mark_modified();
}
pub fn statistics(&self) -> NotebookStatistics {
let mut stats = NotebookStatistics::default();
for cell in &self.cells {
match cell.cell_type {
CellType::Code => {
stats.code_cells += 1;
if cell.needs_execution() {
stats.dirty_cells += 1;
}
if cell.get_output().is_some() {
stats.executed_cells += 1;
}
}
CellType::Text => stats.text_cells += 1,
CellType::Markdown => stats.markdown_cells += 1,
CellType::Output => stats.output_cells += 1,
}
stats.total_characters += cell.get_text().len();
}
stats.total_cells = self.cells.len();
stats
}
pub fn search(&self, query: &str, case_sensitive: bool) -> Vec<(usize, Vec<usize>)> {
let mut results = Vec::new();
for (index, cell) in self.cells.iter().enumerate() {
let content = cell.get_text();
let search_content = if case_sensitive {
content.clone()
} else {
content.to_lowercase()
};
let search_query = if case_sensitive {
query.to_string()
} else {
query.to_lowercase()
};
let mut matches = Vec::new();
let mut start = 0;
while let Some(pos) = search_content[start..].find(&search_query) {
matches.push(start + pos);
start += pos + search_query.len();
}
if !matches.is_empty() {
results.push((index, matches));
}
}
results
}
pub fn set_file_path(&mut self, path: PathBuf) {
self.file_path = Some(path);
}
pub fn get_file_path(&self) -> Option<&PathBuf> {
self.file_path.as_ref()
}
pub fn needs_save(&self) -> bool {
self.metadata.needs_save()
}
pub fn mark_saved(&mut self) {
self.metadata.mark_saved();
}
}
impl Default for Notebook {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Default, Clone)]
pub struct NotebookStatistics {
pub total_cells: usize,
pub code_cells: usize,
pub text_cells: usize,
pub markdown_cells: usize,
pub output_cells: usize,
pub executed_cells: usize,
pub dirty_cells: usize,
pub total_characters: usize,
}
impl NotebookStatistics {
pub fn execution_rate(&self) -> f64 {
if self.code_cells == 0 {
1.0
} else {
self.executed_cells as f64 / self.code_cells as f64
}
}
pub fn average_cell_length(&self) -> f64 {
if self.total_cells == 0 {
0.0
} else {
self.total_characters as f64 / self.total_cells as f64
}
}
}
pub struct NotebookManager {
notebooks: HashMap<NotebookId, Notebook>,
active_notebook: Option<NotebookId>,
}
impl NotebookManager {
pub fn new() -> Self {
Self {
notebooks: HashMap::new(),
active_notebook: None,
}
}
pub fn create_notebook(&mut self, title: Option<String>) -> NotebookId {
let notebook = match title {
Some(title) => Notebook::with_title(title),
None => Notebook::new(),
};
let id = notebook.id;
self.notebooks.insert(id, notebook);
self.active_notebook = Some(id);
id
}
pub fn open_notebook(&mut self, notebook: Notebook) -> NotebookId {
let id = notebook.id;
self.notebooks.insert(id, notebook);
self.active_notebook = Some(id);
id
}
pub fn close_notebook(&mut self, notebook_id: &NotebookId) -> NotebookResult<Notebook> {
let notebook = self.notebooks.remove(notebook_id)
.ok_or_else(|| NotebookError::Cell("笔记本不存在".to_string()))?;
if self.active_notebook == Some(*notebook_id) {
self.active_notebook = self.notebooks.keys().next().copied();
}
Ok(notebook)
}
pub fn get_active_notebook(&self) -> Option<&Notebook> {
self.active_notebook
.and_then(|id| self.notebooks.get(&id))
}
pub fn get_active_notebook_mut(&mut self) -> Option<&mut Notebook> {
self.active_notebook
.and_then(|id| self.notebooks.get_mut(&id))
}
pub fn set_active_notebook(&mut self, notebook_id: &NotebookId) -> NotebookResult<()> {
if self.notebooks.contains_key(notebook_id) {
self.active_notebook = Some(*notebook_id);
Ok(())
} else {
Err(NotebookError::Cell("笔记本不存在".to_string()))
}
}
pub fn get_notebook(&self, notebook_id: &NotebookId) -> Option<&Notebook> {
self.notebooks.get(notebook_id)
}
pub fn get_notebook_mut(&mut self, notebook_id: &NotebookId) -> Option<&mut Notebook> {
self.notebooks.get_mut(notebook_id)
}
pub fn get_notebook_ids(&self) -> Vec<NotebookId> {
self.notebooks.keys().copied().collect()
}
pub fn notebook_count(&self) -> usize {
self.notebooks.len()
}
pub fn has_unsaved_notebooks(&self) -> bool {
self.notebooks.values().any(|nb| nb.needs_save())
}
pub fn get_unsaved_notebooks(&self) -> Vec<&Notebook> {
self.notebooks.values().filter(|nb| nb.needs_save()).collect()
}
}
impl Default for NotebookManager {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::notebook::CellContent;
#[test]
fn test_notebook_metadata() {
let mut metadata = NotebookMetadata::default();
assert_eq!(metadata.title, "未命名笔记本");
assert!(metadata.needs_save());
metadata.mark_saved();
assert!(!metadata.needs_save());
metadata.mark_modified();
assert!(metadata.needs_save());
}
#[test]
fn test_notebook_creation() {
let notebook = Notebook::new();
assert!(notebook.is_empty());
assert_eq!(notebook.cell_count(), 0);
let titled_notebook = Notebook::with_title("测试笔记本".to_string());
assert_eq!(titled_notebook.metadata.title, "测试笔记本");
}
#[test]
fn test_notebook_cell_operations() {
let mut notebook = Notebook::new();
let cell1 = NotebookCell::new_code("2 + 3".to_string());
let cell1_id = cell1.id;
notebook.add_cell(cell1);
assert_eq!(notebook.cell_count(), 1);
let cell2 = NotebookCell::new_text("文本内容".to_string());
notebook.insert_cell(0, cell2).unwrap();
assert_eq!(notebook.cell_count(), 2);
let (index, _) = notebook.find_cell(&cell1_id).unwrap();
assert_eq!(index, 1);
notebook.move_cell(1, 0).unwrap();
let (index, _) = notebook.find_cell(&cell1_id).unwrap();
assert_eq!(index, 0);
notebook.duplicate_cell(0).unwrap();
assert_eq!(notebook.cell_count(), 3);
let removed = notebook.remove_cell(0).unwrap();
assert_eq!(removed.id, cell1_id);
assert_eq!(notebook.cell_count(), 2);
}
#[test]
fn test_notebook_statistics() {
let mut notebook = Notebook::new();
notebook.add_cell(NotebookCell::new_code("x + y".to_string()));
notebook.add_cell(NotebookCell::new_text("说明文本".to_string()));
notebook.add_cell(NotebookCell::new_markdown("# 标题".to_string()));
let stats = notebook.statistics();
assert_eq!(stats.total_cells, 3);
assert_eq!(stats.code_cells, 1);
assert_eq!(stats.text_cells, 1);
assert_eq!(stats.markdown_cells, 1);
assert_eq!(stats.dirty_cells, 1); }
#[test]
fn test_notebook_search() {
let mut notebook = Notebook::new();
notebook.add_cell(NotebookCell::new_code("x + y = z".to_string()));
notebook.add_cell(NotebookCell::new_text("这是一个测试".to_string()));
notebook.add_cell(NotebookCell::new_code("y * 2".to_string()));
let results = notebook.search("y", true);
assert_eq!(results.len(), 2); assert_eq!(results[0].0, 0); assert_eq!(results[1].0, 2);
let results = notebook.search("测试", true);
assert_eq!(results.len(), 1);
assert_eq!(results[0].0, 1); }
#[test]
fn test_notebook_manager() {
let mut manager = NotebookManager::new();
assert_eq!(manager.notebook_count(), 0);
assert!(manager.get_active_notebook().is_none());
let id1 = manager.create_notebook(Some("笔记本1".to_string()));
assert_eq!(manager.notebook_count(), 1);
assert!(manager.get_active_notebook().is_some());
let id2 = manager.create_notebook(Some("笔记本2".to_string()));
assert_eq!(manager.notebook_count(), 2);
manager.set_active_notebook(&id1).unwrap();
assert_eq!(manager.get_active_notebook().unwrap().metadata.title, "笔记本1");
let closed = manager.close_notebook(&id1).unwrap();
assert_eq!(closed.metadata.title, "笔记本1");
assert_eq!(manager.notebook_count(), 1);
}
}