use anyhow::{bail, Context, Result};
use nbformat::v4::Cell;
use yrs::types::ToJson;
use yrs::{Any, Array, ArrayPrelim, ArrayRef, Map, MapPrelim, MapRef, Text, TextPrelim, Transact};
use super::ydoc::YDocClient;
pub enum ClearCellSelector {
All,
ById(String),
ByIndex(i32),
}
fn add_cell_to_ydoc(doc: &yrs::Doc, cell: &Cell, index: usize) -> Result<()> {
let cells_array = doc.get_or_insert_array("cells");
let mut txn = doc.transact_mut();
let empty_map = MapPrelim::default();
cells_array.insert(&mut txn, index as u32, empty_map);
let cell_value = cells_array
.get(&txn, index as u32)
.context("Failed to get inserted cell")?;
let cell_map: MapRef = cell_value
.cast()
.map_err(|_| anyhow::anyhow!("Failed to cast cell to MapRef"))?;
match cell {
Cell::Code {
id,
metadata,
execution_count,
source,
outputs: _,
} => {
cell_map.insert(&mut txn, "cell_type", "code");
cell_map.insert(&mut txn, "id", id.as_str());
if let Some(count) = execution_count {
cell_map.insert(&mut txn, "execution_count", *count);
} else {
cell_map.insert(&mut txn, "execution_count", Any::Null);
}
cell_map.insert(&mut txn, "execution_state", "idle");
let metadata_prelim = if let Some(trusted) =
metadata.additional.get("trusted").and_then(|v| v.as_bool())
{
MapPrelim::from([("trusted", Any::Bool(trusted))])
} else {
MapPrelim::default()
};
cell_map.insert(&mut txn, "metadata", metadata_prelim);
let source_str = source_to_string(source);
cell_map.insert(&mut txn, "source", TextPrelim::new(&source_str));
cell_map.insert(&mut txn, "outputs", ArrayPrelim::default());
}
Cell::Markdown {
id,
metadata,
source,
..
} => {
cell_map.insert(&mut txn, "cell_type", "markdown");
cell_map.insert(&mut txn, "id", id.as_str());
cell_map.insert(&mut txn, "execution_state", "idle");
let metadata_prelim = if let Some(trusted) =
metadata.additional.get("trusted").and_then(|v| v.as_bool())
{
MapPrelim::from([("trusted", Any::Bool(trusted))])
} else {
MapPrelim::default()
};
cell_map.insert(&mut txn, "metadata", metadata_prelim);
let source_str = source_to_string(source);
cell_map.insert(&mut txn, "source", TextPrelim::new(&source_str));
}
Cell::Raw {
id,
metadata,
source,
} => {
cell_map.insert(&mut txn, "cell_type", "raw");
cell_map.insert(&mut txn, "id", id.as_str());
cell_map.insert(&mut txn, "execution_state", "idle");
let metadata_prelim = if let Some(trusted) =
metadata.additional.get("trusted").and_then(|v| v.as_bool())
{
MapPrelim::from([("trusted", Any::Bool(trusted))])
} else {
MapPrelim::default()
};
cell_map.insert(&mut txn, "metadata", metadata_prelim);
let source_str = source_to_string(source);
cell_map.insert(&mut txn, "source", TextPrelim::new(&source_str));
}
}
Ok(())
}
fn source_to_string(source: &[String]) -> String {
source.join("")
}
pub async fn ydoc_add_cells(
server_url: &str,
token: &str,
notebook_path: &str,
cells: &[Cell],
start_index: usize,
) -> Result<()> {
let mut ydoc_client = YDocClient::connect(
server_url.to_string(),
token.to_string(),
notebook_path.to_string(),
)
.await?;
for (i, cell) in cells.iter().enumerate() {
add_cell_to_ydoc(ydoc_client.get_doc(), cell, start_index + i)
.context("Failed to add cell to Y.js document")?;
}
ydoc_client.sync().await.context("Failed to sync changes")?;
ydoc_client.close().await?;
Ok(())
}
pub async fn ydoc_delete_cells(
server_url: &str,
token: &str,
notebook_path: &str,
indices: &[usize],
) -> Result<()> {
let mut ydoc_client = YDocClient::connect(
server_url.to_string(),
token.to_string(),
notebook_path.to_string(),
)
.await?;
delete_cells_from_ydoc(ydoc_client.get_doc(), indices)
.context("Failed to delete cells from Y.js document")?;
ydoc_client.sync().await.context("Failed to sync changes")?;
ydoc_client.close().await?;
Ok(())
}
fn delete_cells_from_ydoc(doc: &yrs::Doc, indices: &[usize]) -> Result<()> {
let cells_array = doc.get_or_insert_array("cells");
let mut txn = doc.transact_mut();
for &index in indices {
cells_array.remove(&mut txn, index as u32);
}
Ok(())
}
pub async fn ydoc_update_cell(
server_url: &str,
token: &str,
notebook_path: &str,
cell_index: usize,
new_source: Option<&str>,
append_source: Option<&str>,
) -> Result<()> {
let mut ydoc_client = YDocClient::connect(
server_url.to_string(),
token.to_string(),
notebook_path.to_string(),
)
.await?;
update_cell_source_in_ydoc(ydoc_client.get_doc(), cell_index, new_source, append_source)
.context("Failed to update cell in Y.js document")?;
ydoc_client.sync().await.context("Failed to sync changes")?;
ydoc_client.close().await?;
Ok(())
}
fn update_cell_source_in_ydoc(
doc: &yrs::Doc,
cell_index: usize,
new_source: Option<&str>,
append_source: Option<&str>,
) -> Result<()> {
let cells_array = doc.get_or_insert_array("cells");
let mut txn = doc.transact_mut();
let cell_value = cells_array
.get(&txn, cell_index as u32)
.context(format!("Cell at index {} not found", cell_index))?;
let cell_map: MapRef = cell_value
.cast()
.map_err(|_| anyhow::anyhow!("Cell at index {} is not a Map", cell_index))?;
let source_value = cell_map
.get(&txn, "source")
.context("Cell does not have a source field")?;
let source_text: yrs::TextRef = source_value
.cast()
.map_err(|_| anyhow::anyhow!("Source field is not a Y.Text"))?;
if let Some(new_text) = new_source {
let current_len = source_text.len(&txn);
if current_len > 0 {
source_text.remove_range(&mut txn, 0, current_len);
}
source_text.insert(&mut txn, 0, new_text);
} else if let Some(append_text) = append_source {
let current_len = source_text.len(&txn);
source_text.insert(&mut txn, current_len, append_text);
}
cell_map.insert(&mut txn, "execution_count", Any::Null);
Ok(())
}
pub async fn ydoc_clear_outputs(
server_url: &str,
token: &str,
notebook_path: &str,
selector: ClearCellSelector,
) -> Result<usize> {
let mut ydoc_client = YDocClient::connect(
server_url.to_string(),
token.to_string(),
notebook_path.to_string(),
)
.await?;
let cells_cleared = clear_outputs_in_ydoc(ydoc_client.get_doc(), selector)
.context("Failed to clear outputs in Y.js document")?;
ydoc_client.sync().await.context("Failed to sync changes")?;
ydoc_client.close().await?;
Ok(cells_cleared)
}
fn clear_outputs_in_ydoc(doc: &yrs::Doc, selector: ClearCellSelector) -> Result<usize> {
let cells_array = doc.get_or_insert_array("cells");
let mut txn = doc.transact_mut();
let num_cells = cells_array.len(&txn) as usize;
let indices: Vec<usize> = match selector {
ClearCellSelector::All => (0..num_cells)
.filter(|&i| {
cell_type_at(&cells_array, &txn, i)
.map(|t| t == "code")
.unwrap_or(false)
})
.collect(),
ClearCellSelector::ById(ref id) => {
let idx = (0..num_cells)
.find(|&i| {
cell_id_at(&cells_array, &txn, i)
.map(|cid| cid == *id)
.unwrap_or(false)
})
.ok_or_else(|| anyhow::anyhow!("Cell with ID '{}' not found in notebook", id))?;
let ct = cell_type_at(&cells_array, &txn, idx).unwrap_or_default();
if ct != "code" {
bail!("Can only clear outputs from code cells");
}
vec![idx]
}
ClearCellSelector::ByIndex(raw_idx) => {
let idx = normalize_ydoc_index(raw_idx, num_cells)?;
let ct = cell_type_at(&cells_array, &txn, idx).unwrap_or_default();
if ct != "code" {
bail!("Can only clear outputs from code cells");
}
vec![idx]
}
};
for &i in &indices {
let cell_value = cells_array
.get(&txn, i as u32)
.context("Cell index out of bounds")?;
let cell_map: MapRef = cell_value
.cast()
.map_err(|_| anyhow::anyhow!("Cell is not a Map"))?;
if let Some(outputs_val) = cell_map.get(&txn, "outputs") {
if let Ok(arr) = outputs_val.cast::<ArrayRef>() {
let len = arr.len(&txn);
if len > 0 {
arr.remove_range(&mut txn, 0, len);
}
}
}
cell_map.insert(&mut txn, "execution_count", Any::Null);
}
Ok(indices.len())
}
fn cell_type_at(cells_array: &ArrayRef, txn: &yrs::TransactionMut, index: usize) -> Option<String> {
let cell_value = cells_array.get(txn, index as u32)?;
let cell_map: MapRef = cell_value.cast().ok()?;
let val = cell_map.get(txn, "cell_type")?;
match val.to_json(txn) {
Any::String(s) => Some(s.to_string()),
_ => None,
}
}
fn cell_id_at(cells_array: &ArrayRef, txn: &yrs::TransactionMut, index: usize) -> Option<String> {
let cell_value = cells_array.get(txn, index as u32)?;
let cell_map: MapRef = cell_value.cast().ok()?;
let val = cell_map.get(txn, "id")?;
match val.to_json(txn) {
Any::String(s) => Some(s.to_string()),
_ => None,
}
}
fn normalize_ydoc_index(index: i32, len: usize) -> Result<usize> {
if index < 0 {
let abs = index.unsigned_abs() as usize;
if abs > len {
bail!(
"Cell index {} out of range (notebook has {} cells)",
index,
len
);
}
Ok(len - abs)
} else {
let idx = index as usize;
if idx >= len {
bail!(
"Cell index {} out of range (notebook has {} cells)",
index,
len
);
}
Ok(idx)
}
}