mod content; mod edit; mod keys; mod kind; mod media; mod table; mod view; use std::{collections::HashMap, rc::Rc}; use gpui::{ App, AppContext as _, Context, Entity, EventEmitter, Focusable, Global, SharedString, Subscription, Window, }; pub use kind::{Align, BlockKind, Media}; pub(crate) use kind::{matching, shortcut}; use super::markdown::markdown_highlights; use crate::forms::{History, InputEvent, TextInput, code_highlights}; /// A block as the editor gives it out and takes it in: its kind and the text of each of its fields. #[derive(Clone, Debug, PartialEq)] pub struct BlockData { pub kind: BlockKind, pub texts: Vec, } impl BlockData { pub fn new(kind: BlockKind, texts: impl IntoIterator>) -> Self { let texts: Vec = texts.into_iter().map(Into::into).collect(); assert!( kind.accepts(texts.len()), "a {} block cannot hold {} fields", kind.label(), texts.len() ); Self { kind, texts } } } /// What a block editor tells its owner. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum BlockEvent { Changed, } /// Renders a diagram's source to SVG, or says what is wrong with it. pub(crate) type RenderDiagram = Rc Result>; /// Text shared by synced blocks, by name, across every editor in the app. #[derive(Default)] struct SyncedTexts(HashMap); impl Global for SyncedTexts {} pub(crate) struct Block { pub key: u64, pub kind: BlockKind, pub fields: Vec>, _changes: Vec, } /// The whole document as undo keeps it, and where the caret was: block, field and selection. #[derive(Clone, Debug)] struct Snapshot { blocks: Vec<(u64, BlockData)>, caret: Option<(u64, usize, std::ops::Range)>, } /// A document of blocks, Notion-like: prose, lists and to-dos, quotes and callouts, toggles, code, math, diagrams, dividers, tables, media, columns and synced blocks. Markdown at a paragraph's start changes its kind; `/` offers every kind; `@` and `:` suggest people and emoji; blocks drag by their handle. Undo covers text and structure alike. pub struct BlockEditor { pub(crate) blocks: Vec, next: u64, history: History, current: Snapshot, /// The field that last held focus, as block and field. caret: Option<(u64, usize)>, /// The selection a key went down on, before its field changed. typed_from: Option<(u64, usize, std::ops::Range)>, pub(crate) people: Vec, pub(crate) diagram: Option, _synced: Subscription, } impl EventEmitter for BlockEditor {} impl BlockEditor { pub fn new(blocks: Vec, window: &mut Window, cx: &mut Context) -> Self { let synced = cx.observe_global_in::(window, |editor, _, cx| editor.pull(cx)); let mut editor = Self { blocks: Vec::new(), next: 0, history: History::default(), current: Snapshot { blocks: Vec::new(), caret: None, }, caret: None, typed_from: None, people: Vec::new(), diagram: None, _synced: synced, }; for data in blocks { let key = editor.fresh_key(); let block = editor.build(data, key, window, cx); editor.blocks.push(block); } editor.current = editor.snapshot(cx); editor } /// Handles `@` suggests. pub fn people(mut self, handles: impl IntoIterator>) -> Self { self.people = handles.into_iter().map(Into::into).collect(); self } /// Draws diagram blocks: their source to SVG, or an error to show in its place. pub fn diagrams(mut self, render: impl Fn(&str) -> Result + 'static) -> Self { self.diagram = Some(Rc::new(render)); self } /// The document as it stands. pub fn blocks(&self, cx: &App) -> Vec { self.snapshot(cx) .blocks .into_iter() .map(|(_, data)| data) .collect() } /// A field for `kind`: inline markdown styled as typed, or code colored as code. fn field( kind: &BlockKind, text: &str, window: &mut Window, cx: &mut Context, ) -> Entity { let code = *kind == BlockKind::Code; let field = cx.new(|cx| { let hint = match kind { BlockKind::Paragraph | BlockKind::Table { .. } => "", kind => kind.placeholder(), }; let field = TextInput::new(window, cx) .multi_line(1, usize::MAX) .placeholder(hint); match (code, kind.is_source()) { (true, _) => field.highlighter(code_highlights), (false, true) => field, (false, false) => field.highlighter(markdown_highlights), } }); field.update(cx, |field, cx| field.set_text(text, cx)); field } fn fresh_key(&mut self) -> u64 { self.next += 1; self.next } fn build( &mut self, data: BlockData, key: u64, window: &mut Window, cx: &mut Context, ) -> Block { assert!( data.kind.accepts(data.texts.len()), "a {} block cannot hold {} fields", data.kind.label(), data.texts.len() ); let mut texts = data.texts; if let BlockKind::Synced(name) = &data.kind { let shared = cx .try_global::() .and_then(|synced| synced.0.get(name).cloned()); match shared { Some(shared) => texts[0] = shared, None => { cx.default_global::() .0 .insert(name.clone(), texts[0].clone()); } } } let fields: Vec> = texts .iter() .map(|text| Self::field(&data.kind, text, window, cx)) .collect(); let changes = fields .iter() .map(|field| Self::watch(key, field, window, cx)) .collect(); Block { key, kind: data.kind, fields, _changes: changes, } } fn watch( key: u64, field: &Entity, window: &mut Window, cx: &mut Context, ) -> Subscription { cx.subscribe_in(field, window, move |editor, field, event, window, cx| { editor.on_field(key, field, *event, window, cx) }) } /// Adds an empty field to a block, at `at` or last. pub(crate) fn add_field( &mut self, key: u64, at: Option, window: &mut Window, cx: &mut Context, ) { let ix = self.index(key); let field = Self::field(&self.blocks[ix].kind, "", window, cx); let watched = Self::watch(key, &field, window, cx); let block = &mut self.blocks[ix]; let at = at.unwrap_or(block.fields.len()); block.fields.insert(at, field); block._changes.insert(at, watched); } pub(crate) fn remove_field(&mut self, key: u64, field: usize) { let ix = self.index(key); let block = &mut self.blocks[ix]; block.fields.remove(field); drop(block._changes.remove(field)); } pub(crate) fn index(&self, key: u64) -> usize { self.blocks .iter() .position(|block| block.key == key) .unwrap_or_else(|| panic!("block {key} left the editor")) } fn on_field( &mut self, key: u64, field: &Entity, event: InputEvent, window: &mut Window, cx: &mut Context, ) { let ix = self.index(key); match event { InputEvent::Changed => { let typed = field.focus_handle(cx).is_focused(window); if typed && self.blocks[ix].kind == BlockKind::Paragraph && let Some((kind, length)) = shortcut(field.read(cx).text()) && field.read(cx).cursor() == length { log::info!( "block editor: markdown turns block {key} into {}", kind.label() ); field.update(cx, |field, cx| { field.select(0..length, cx); field.insert("", cx); }); self.turn_into(key, kind, window, cx); return; } if let BlockKind::Synced(name) = &self.blocks[ix].kind { let text = field.read(cx).text().to_string(); let shared = cx .try_global::() .and_then(|synced| synced.0.get(name)); if shared != Some(&text) { log::info!("block editor: synced {name} written"); cx.default_global::() .0 .insert(name.clone(), text); } } self.settle(true, cx); } InputEvent::Focus => { self.caret = self.blocks[ix] .fields .iter() .position(|each| each == field) .map(|at| (key, at)); if field.read(cx).is_empty() { let hint = self.blocks[ix].kind.placeholder(); field.update(cx, |field, cx| field.set_placeholder(hint, cx)); } } InputEvent::Blur if self.blocks[ix].kind == BlockKind::Paragraph => { field.update(cx, |field, cx| field.set_placeholder("", cx)); } _ => {} } } /// Takes in synced text written elsewhere. fn pull(&mut self, cx: &mut Context) { let shared: Vec<(Entity, String)> = self .blocks .iter() .filter_map(|block| match &block.kind { BlockKind::Synced(name) => cx .try_global::() .and_then(|synced| synced.0.get(name)) .map(|text| (block.fields[0].clone(), text.clone())), _ => None, }) .collect(); for (field, text) in shared { if field.read(cx).text() != text { log::debug!("block editor: synced text arrived"); field.update(cx, |field, cx| field.set_text(text, cx)); } } } pub(crate) fn focus_field( &self, key: u64, field: usize, at: Option, window: &mut Window, cx: &mut App, ) { let block = &self.blocks[self.index(key)]; let Some(field) = block.fields.get(field) else { return; }; if let Some(at) = at { field.update(cx, |field, cx| field.select(at..at, cx)); } window.focus(&field.focus_handle(cx)); } /// Adds a block after `after`, or first, and puts the caret in it. pub fn insert( &mut self, after: Option, data: BlockData, window: &mut Window, cx: &mut Context, ) -> u64 { self.before_change(cx); let key = self.fresh_key(); let block = self.build(data, key, window, cx); let at = after.map_or(0, |after| self.index(after) + 1); log::info!("block editor: {} block at {at}", block.kind.label()); let end = block .fields .first() .map(|field| field.read(cx).text().len()); self.blocks.insert(at, block); self.after_change(cx); self.focus_field(key, 0, end, window, cx); key } /// Changes a block's kind, keeping its first field's text; fields the new kind needs start empty. pub fn turn_into( &mut self, key: u64, kind: BlockKind, window: &mut Window, cx: &mut Context, ) { self.before_change(cx); let ix = self.index(key); let first = self.blocks[ix] .fields .first() .map(|field| field.read(cx).text().to_string()) .unwrap_or_default(); let mut texts = vec![String::new(); kind.fields()]; if let Some(text) = texts.first_mut() { *text = first; } log::info!("block editor: block {key} turns into {}", kind.label()); let block = self.build(BlockData { kind, texts }, key, window, cx); self.blocks[ix] = block; self.after_change(cx); self.focus_field(key, 0, None, window, cx); } /// Changes a block's kind in place, as a to-do's box or a toggle's state does. pub fn set_kind(&mut self, key: u64, kind: BlockKind, cx: &mut Context) { let ix = self.index(key); assert!( kind.accepts(self.blocks[ix].fields.len()), "set_kind keeps the fields; turn_into changes them" ); self.before_change(cx); self.blocks[ix].kind = kind; self.after_change(cx); } pub fn remove(&mut self, key: u64, window: &mut Window, cx: &mut Context) { self.before_change(cx); let ix = self.index(key); log::info!("block editor: block {key} removed"); self.blocks.remove(ix); self.after_change(cx); if let Some(previous) = ix.checked_sub(1).and_then(|ix| self.blocks.get(ix)) { let key = previous.key; let end = previous .fields .first() .map(|field| field.read(cx).text().len()); self.focus_field(key, 0, end, window, cx); } } pub fn duplicate(&mut self, key: u64, window: &mut Window, cx: &mut Context) { let ix = self.index(key); let data = self.snapshot(cx).blocks[ix].1.clone(); self.insert(Some(key), data, window, cx); } /// Moves the block at `from` to place `to`. pub fn move_block(&mut self, from: usize, to: usize, cx: &mut Context) { self.before_change(cx); log::info!("block editor: block moved from {from} to {to}"); let block = self.blocks.remove(from); self.blocks.insert(to.min(self.blocks.len()), block); self.after_change(cx); } /// Whether `key`'s field `field` holds focus. pub(crate) fn focused(&self, window: &Window, cx: &App) -> Option<(u64, usize)> { self.blocks.iter().find_map(|block| { block .fields .iter() .position(|field| field.focus_handle(cx).is_focused(window)) .map(|ix| (block.key, ix)) }) } }