···12661266/// This handles keyboard shortcuts only. Text input and deletion
12671267/// are handled by beforeinput. Navigation (arrows, etc.) is passed
12681268/// through to the browser.
12691269-///
12701270-/// # Arguments
12711271-/// * `doc` - The editor document
12721272-/// * `config` - Keybinding configuration
12731273-/// * `combo` - The key combination from the keyboard event
12741274-/// * `range` - Current cursor position / selection range
12751275-///
12761276-/// # Returns
12771277-/// Whether the event was handled.
12781269pub fn handle_keydown_with_bindings(
12791270 doc: &mut EditorDocument,
12801271 config: &KeybindingConfig,
-12
crates/weaver-app/src/components/editor/cursor.rs
···1313use wasm_bindgen::JsCast;
14141515/// Restore cursor position in the DOM after re-render.
1616-///
1717-/// # Arguments
1818-/// - `char_offset`: Cursor position as char offset in document
1919-/// - `offset_map`: Mappings from source to DOM positions
2020-/// - `editor_id`: DOM ID of the contenteditable element
2121-/// - `snap_direction`: Optional direction hint for snapping from invisible content
2222-///
2323-/// # Algorithm
2424-/// 1. Find offset mapping containing char_offset
2525-/// 2. Get DOM node by mapping.node_id
2626-/// 3. Walk text nodes to find UTF-16 position
2727-/// 4. Set cursor with Selection API
2816#[cfg(all(target_family = "wasm", target_os = "unknown"))]
2917pub fn restore_cursor_position(
3018 char_offset: usize,
···319319 ///
320320 /// MUST be called from within a reactive context (e.g., `use_hook`) to
321321 /// properly initialize Dioxus Signals.
322322- ///
323323- /// # Arguments
324324- /// * `entry` - The entry record fetched from PDS
325325- /// * `entry_ref` - StrongRef to the entry (URI + CID)
326322 pub fn from_entry(entry: &Entry<'_>, entry_ref: StrongRef<'static>) -> Self {
327323 let mut doc = Self::new(entry.content.to_string());
328324···691687 Ok(())
692688 }
693689694694- /// Undo the last operation.
695695- /// Returns true if an undo was performed.
696696- /// Automatically updates cursor position from the Loro cursor.
690690+ /// Undo the last operation. Automatically updates cursor position.
697691 pub fn undo(&mut self) -> LoroResult<bool> {
698692 // Sync Loro cursor to current position BEFORE undo
699693 // so it tracks through the undo operation
···709703 Ok(result)
710704 }
711705712712- /// Redo the last undone operation.
713713- /// Returns true if a redo was performed.
714714- /// Automatically updates cursor position from the Loro cursor.
706706+ /// Redo the last undone operation. Automatically updates cursor position.
715707 pub fn redo(&mut self) -> LoroResult<bool> {
716708 // Sync Loro cursor to current position BEFORE redo
717709 self.sync_loro_cursor();
···181181/// If the position is already valid, returns it directly. Otherwise,
182182/// searches in the preferred direction first, falling back to the other
183183/// direction if needed.
184184-///
185185-/// # Arguments
186186-/// - `offset_map`: The offset mappings for the paragraph
187187-/// - `char_offset`: The target char offset
188188-/// - `preferred_direction`: Which direction to search first when snapping
189189-///
190190-/// # Returns
191191-/// The snapped position, or None if no valid position exists.
192184pub fn find_nearest_valid_position(
193185 offset_map: &[OffsetMapping],
194186 char_offset: usize,
···6262}
63636464/// Result of fetching an entry for editing.
6565-/// Contains the entry data and URI, but NOT an EditorDocument.
6666-/// The document must be created in a reactive context (use_hook) to properly initialize Signals.
6765#[derive(Clone, PartialEq)]
6866pub struct LoadedEntry {
6967 pub entry: Entry<'static>,
···7169}
72707371/// Fetch an existing entry from the PDS for editing.
7474-///
7575-/// Returns the entry data and URI. The caller should create an `EditorDocument`
7676-/// from this data using `EditorDocument::from_entry()` inside a reactive context.
7777-///
7878-/// # Arguments
7979-/// * `fetcher` - The fetcher for making API calls
8080-/// * `uri` - The AT-URI of the entry to load (e.g., `at://did:plc:xxx/sh.weaver.notebook.entry/rkey`)
8181-///
8282-/// # Returns
8383-/// The entry and its URI, or an error.
8472pub async fn load_entry_for_editing(
8573 fetcher: &Fetcher,
8674 uri: &AtUri<'_>,
···154142/// - Without notebook but with entry_uri in doc: uses `put_record` to update existing
155143/// - Without notebook and no entry_uri: uses `create_record` for free-floating entry
156144///
157157-/// Draft image paths (`/image/{did}/draft/{blob_rkey}/{name}`) are rewritten to
158158-/// published paths (`/image/{did}/{entry_rkey}/{name}`) before publishing.
159159-///
145145+/// Draft image paths are rewritten to published paths before publishing.
160146/// On successful create, sets `doc.entry_uri` so subsequent publishes update the same record.
161161-///
162162-/// # Arguments
163163-/// * `fetcher` - The authenticated fetcher/client
164164-/// * `doc` - The editor document containing entry data (mutable to update entry_uri)
165165-/// * `notebook_title` - Optional title of the notebook to publish to
166166-/// * `draft_key` - Storage key for the draft (for cleanup)
167167-///
168168-/// # Returns
169169-/// The AT-URI of the created/updated entry, or an error.
170147pub async fn publish_entry(
171148 fetcher: &Fetcher,
172149 doc: &mut EditorDocument,
-9
crates/weaver-app/src/components/editor/render.rs
···104104///
105105/// Uses cached paragraph renders when possible, only re-rendering changed paragraphs.
106106/// For "safe" edits (no boundary changes), skips boundary rediscovery entirely.
107107-///
108108-/// # Arguments
109109-/// - `text`: The document text to render
110110-/// - `cache`: Previous render cache (if any)
111111-/// - `edit`: Information about the most recent edit (if any)
112112-/// - `image_resolver`: Optional resolver for mapping image URLs to data/CDN URLs
113113-///
114114-/// # Returns
115115-/// Tuple of (rendered paragraphs, updated cache)
116107pub fn render_paragraphs_incremental(
117108 text: &LoroText,
118109 cache: Option<&RenderCache>,
···7171}
72727373/// Save editor state to LocalStorage (WASM only).
7474-///
7575-/// # Arguments
7676-/// * `doc` - The editor document to save
7777-/// * `key` - Storage key (e.g., "new:abc123" for new entries, or AT-URI for existing)
7874#[cfg(all(target_family = "wasm", target_os = "unknown"))]
7975pub fn save_to_storage(
8076 doc: &EditorDocument,
···10399///
104100/// Returns an EditorDocument restored from CRDT snapshot if available,
105101/// otherwise falls back to just the text content.
106106-///
107107-/// # Arguments
108108-/// * `key` - Storage key (e.g., "new:abc123" for new entries, or AT-URI for existing)
109102#[cfg(all(target_family = "wasm", target_os = "unknown"))]
110103pub fn load_from_storage(key: &str) -> Option<EditorDocument> {
111104 let snapshot: EditorSnapshot = LocalStorage::get(storage_key(key)).ok()?;
···158151///
159152/// Unlike `load_from_storage`, this doesn't create an EditorDocument and is safe
160153/// to call outside of reactive context. Use with `load_and_merge_document`.
161161-///
162162-/// # Arguments
163163-/// * `key` - Storage key (e.g., "new:abc123" for new entries, or AT-URI for existing)
164154#[cfg(all(target_family = "wasm", target_os = "unknown"))]
165155pub fn load_snapshot_from_storage(key: &str) -> Option<LocalSnapshotData> {
166156 let snapshot: EditorSnapshot = LocalStorage::get(storage_key(key)).ok()?;
···195185}
196186197187/// Delete a draft from LocalStorage (WASM only).
198198-///
199199-/// # Arguments
200200-/// * `key` - Storage key to delete
201188#[cfg(all(target_family = "wasm", target_os = "unknown"))]
202189pub fn delete_draft(key: &str) {
203190 LocalStorage::delete(storage_key(key));
+9-56
crates/weaver-app/src/components/editor/sync.rs
···178178///
179179/// Uploads the current Loro snapshot as a blob and creates an `sh.weaver.edit.root`
180180/// record referencing the entry (or draft key if unpublished).
181181-///
182182-/// # Arguments
183183-/// * `fetcher` - The authenticated fetcher
184184-/// * `doc` - The editor document
185185-/// * `draft_key` - The draft key (used for unpublished entries)
186186-/// * `entry_uri` - Optional AT-URI of the published entry
187187-/// * `entry_cid` - Optional CID of the published entry
188181pub async fn create_edit_root(
189182 fetcher: &Fetcher,
190183 doc: &EditorDocument,
···244237}
245238246239/// Create a diff record with updates since the last sync.
247247-///
248248-/// # Arguments
249249-/// * `fetcher` - The authenticated fetcher
250250-/// * `doc` - The editor document
251251-/// * `root_uri` - URI of the edit root
252252-/// * `root_cid` - CID of the edit root
253253-/// * `prev_diff` - Optional reference to the previous diff
254254-/// * `draft_key` - The draft key (used for doc reference)
255255-/// * `entry_uri` - Optional AT-URI of the published entry
256256-/// * `entry_cid` - Optional CID of the published entry
257240pub async fn create_diff(
258241 fetcher: &Fetcher,
259242 doc: &EditorDocument,
···352335///
353336/// If no edit root exists, creates one with a full snapshot.
354337/// If a root exists, creates a diff with updates since last sync.
355355-///
356338/// Updates the document's sync state on success.
357357-///
358358-/// # Arguments
359359-/// * `fetcher` - The authenticated fetcher
360360-/// * `doc` - The editor document (mutable to update sync state)
361361-/// * `draft_key` - The draft key for this document
362362-///
363363-/// # Returns
364364-/// The sync result indicating what was created.
365339pub async fn sync_to_pds(
366340 fetcher: &Fetcher,
367341 doc: &mut EditorDocument,
···480454///
481455/// Finds the edit root via constellation backlinks, fetches all diffs,
482456/// and returns the snapshot + updates needed to reconstruct the document.
483483-///
484484-/// # Arguments
485485-/// * `fetcher` - The authenticated fetcher
486486-/// * `entry_uri` - The AT-URI of the entry to load edit state for
487487-///
488488-/// # Returns
489489-/// The edit state if found, or None if no edit root exists for this entry.
490457pub async fn load_edit_state_from_pds(
491458 fetcher: &Fetcher,
492459 entry_uri: &AtUri<'_>,
···500467 // Build root URI
501468 let root_uri = AtUri::new(&format!(
502469 "at://{}/{}/{}",
503503- root_id.did(),
470470+ root_id.did,
504471 ROOT_NSID,
505505- root_id.rkey().as_ref()
472472+ root_id.rkey.as_ref()
506473 ))
507474 .map_err(|e| WeaverError::InvalidNotebook(format!("Invalid root URI: {}", e)))?
508475 .into_static();
···530497 // Fetch the root snapshot blob
531498 let root_snapshot = fetch_blob(
532499 fetcher,
533533- &root_id.did(),
500500+ &root_id.did,
534501 root_output.value.snapshot.blob().cid(),
535502 )
536503 .await?;
···555522 > = BTreeMap::new();
556523557524 for diff_id in &diff_ids {
558558- let rkey = diff_id.rkey();
559559- let rkey_str: &str = rkey.as_ref();
525525+ let rkey_str: &str = diff_id.rkey.as_ref();
560526 let diff_uri = AtUri::new(&format!(
561527 "at://{}/{}/{}",
562562- diff_id.did(),
528528+ diff_id.did,
563529 DIFF_NSID,
564530 rkey_str
565531 ))
···600566 let diff_bytes = if let Some(ref inline) = diff.inline_diff {
601567 inline.clone()
602568 } else if let Some(ref snapshot) = diff.snapshot {
603603- fetch_blob(fetcher, &root_id.did(), snapshot.blob().cid()).await?
569569+ fetch_blob(fetcher, &root_id.did, snapshot.blob().cid()).await?
604570 } else {
605571 tracing::warn!("Diff has neither inline_diff nor snapshot, skipping");
606572 continue;
···622588623589/// Load document state by merging local storage and PDS state.
624590///
625625-/// This is the main entry point for loading a document with full sync support.
626626-/// It:
627627-/// 1. Loads from localStorage (if available)
628628-/// 2. Loads from PDS (if available)
629629-/// 3. Merges both using Loro's CRDT merge
630630-///
631631-/// The result is a `LoadedDocState` containing a pre-merged LoroDoc that can be
632632-/// converted to an EditorDocument inside a reactive context using `use_hook`.
633633-///
634634-/// # Arguments
635635-/// * `fetcher` - The authenticated fetcher
636636-/// * `draft_key` - The localStorage key for this draft
637637-/// * `entry_uri` - Optional AT-URI if editing an existing entry
638638-///
639639-/// # Returns
640640-/// A `LoadedDocState` with merged state, or None if no state exists anywhere.
591591+/// Loads from localStorage and PDS (if available), then merges both using Loro's
592592+/// CRDT merge. The result is a pre-merged LoroDoc that can be converted to an
593593+/// EditorDocument inside a reactive context using `use_hook`.
641594pub async fn load_and_merge_document(
642595 fetcher: &Fetcher,
643596 draft_key: &str,
···18181919impl VisibilityState {
2020 /// Calculate visibility based on cursor position and selection.
2121- ///
2222- /// # Arguments
2323- /// - `cursor_offset`: Current cursor position (char offset)
2424- /// - `selection`: Optional selection range
2525- /// - `syntax_spans`: All syntax spans in the document
2626- /// - `paragraphs`: All paragraphs (for block-level visibility lookup)
2721 pub fn calculate(
2822 cursor_offset: usize,
2923 selection: Option<&Selection>,
-25
crates/weaver-app/src/components/editor/writer.rs
···176176 }
177177178178 /// Add a pending image with a data URL for immediate preview.
179179- ///
180180- /// # Arguments
181181- /// * `name` - The image name used in markdown (e.g., "photo.jpg")
182182- /// * `data_url` - The base64 data URL for preview
183179 pub fn add_pending(&mut self, name: String, data_url: String) {
184180 self.images.insert(name, ResolvedImage::Pending(data_url));
185181 }
186182187183 /// Promote a pending image to uploaded (draft) status.
188188- ///
189189- /// # Arguments
190190- /// * `name` - The image name used in markdown
191191- /// * `blob_rkey` - The rkey of the PublishedBlob record
192192- /// * `ident` - The AT identifier (DID or handle) of the blob owner
193184 pub fn promote_to_uploaded(
194185 &mut self,
195186 name: &str,
···203194 }
204195205196 /// Add an already-uploaded draft image.
206206- ///
207207- /// # Arguments
208208- /// * `name` - The name/URL used in markdown (e.g., "photo.jpg")
209209- /// * `blob_rkey` - The rkey of the PublishedBlob record
210210- /// * `ident` - The AT identifier (DID or handle) of the blob owner
211197 pub fn add_uploaded(
212198 &mut self,
213199 name: String,
···219205 }
220206221207 /// Add a published image.
222222- ///
223223- /// # Arguments
224224- /// * `name` - The name/URL used in markdown (e.g., "photo.jpg")
225225- /// * `entry_rkey` - The rkey of the entry record containing this image
226226- /// * `ident` - The AT identifier (DID or handle) of the entry owner
227208 pub fn add_published(
228209 &mut self,
229210 name: String,
···240221 }
241222242223 /// Build a resolver from editor images and user identifier.
243243- ///
244244- /// # Arguments
245245- /// * `images` - Iterator of editor images
246246- /// * `ident` - The AT identifier (DID or handle) of the user
247247- /// * `entry_rkey` - If Some, images are resolved as published (`/image/{ident}/{entry_rkey}/{name}`).
248248- /// If None, images are resolved as drafts using their `published_blob_uri`.
249224 ///
250225 /// For draft mode (entry_rkey=None), only images with a `published_blob_uri` are included.
251226 /// For published mode (entry_rkey=Some), all images are included.
···3232 pub entries: Vec<StrongRef<'a>>,
3333}
34343535-/// too many cows, so we have conversions
3635pub fn mcow_to_cow(cow: CowStr<'_>) -> std::borrow::Cow<'_, str> {
3736 match cow {
3837 CowStr::Borrowed(s) => std::borrow::Cow::Borrowed(s),
···4039 }
4140}
42414343-/// too many cows, so we have conversions
4442pub fn cow_to_mcow(cow: std::borrow::Cow<'_, str>) -> CowStr<'_> {
4543 match cow {
4644 std::borrow::Cow::Borrowed(s) => CowStr::Borrowed(s),
···4846 }
4947}
50485151-/// too many cows, so we have conversions
5249pub fn mdcow_to_cow(cow: markdown_weaver::CowStr<'_>) -> std::borrow::Cow<'_, str> {
5350 match cow {
5451 markdown_weaver::CowStr::Borrowed(s) => std::borrow::Cow::Borrowed(s),