use std::ops::Range; use galaxyui::{ AppContext, Entity, ModelAsRef, ModelContext, ModelHandle, clipboard::ClipboardContent, }; use itertools::{Either, Itertools}; use line_ending::LineEnding; use vec1::{Vec1, vec1}; use crate::{ content::{ anchor::Anchor, buffer::{ AutoScrollBehavior, Buffer, BufferEditAction, BufferSelectAction, EditOrigin, InitialBufferState, SelectionOffsets, ShouldAutoscroll, ToBufferCharOffset, }, selection_model::BufferSelectionModel, text::{BlockType, BufferBlockItem, BufferBlockStyle, CodeBlockType, TextStyles}, version::BufferVersion, }, render::model::RenderState, selection::{SelectionMode, SelectionModel, TextDirection, TextUnit}, }; use galaxyui::elements::ListIndentLevel; use string_offset::{ByteOffset, CharOffset}; /// A wrapper for a buffer that provides access to its internal update_content method. /// It's important this is only returned from `CoreEditorModel::update_content` method /// so that we can ensure `on_buffer_version_updated` is always invoked on buffer content /// changes synchronously. /// /// This should NEVER be constructed manually outside of this crate. pub struct BufferUpdateWrapper<'a> { buffer: &'a mut Buffer, } impl BufferUpdateWrapper<'_> { pub fn apply_edit( &mut self, action: BufferEditAction, origin: EditOrigin, selection_model: ModelHandle, ctx: &mut ModelContext, ) { self.buffer .update_content(action, origin, selection_model, ctx); } pub fn apply_edit_with_autoscroll( &mut self, action: BufferEditAction, origin: EditOrigin, should_autoscroll: ShouldAutoscroll, selection_model: ModelHandle, ctx: &mut ModelContext, ) { self.buffer.update_content_with_autoscroll( action, origin, should_autoscroll, selection_model, ctx, ); } pub fn buffer(&mut self) -> &mut Buffer { self.buffer } } pub trait CoreEditorModel: Entity { type T: Entity; fn content(&self) -> &ModelHandle; fn buffer_selection_model(&self) -> &ModelHandle; fn selection_model(&self) -> &ModelHandle; fn render_state(&self) -> &ModelHandle; fn validate(&self, ctx: &impl ModelAsRef); fn active_text_style(&self) -> TextStyles; fn buffer_version(&self, ctx: &AppContext) -> BufferVersion { self.content().as_ref(ctx).buffer_version() } /// This callback is always invoked _synchronously_ after a buffer update is applied. fn on_buffer_version_updated( &self, _buffer_version: BufferVersion, _ctx: &mut ModelContext, ) { } fn update_content( &self, action: impl FnOnce(BufferUpdateWrapper, &mut ModelContext), ctx: &mut ModelContext, ) { self.content().update(ctx, |buffer, ctx| { action(BufferUpdateWrapper { buffer }, ctx) }); self.on_buffer_version_updated(self.content().as_ref(ctx).buffer_version(), ctx); } /// Requests a complete rebuild of the layout state. This is expensive, but necessary when /// layout-affecting state like the font size changes. fn rebuild_layout(&self, ctx: &mut ModelContext) { log::debug!("Rebuilding layout state"); let delta = self.content().as_ref(ctx).invalidate_layout(); let buffer_version = self.content().as_ref(ctx).buffer_version(); self.render_state().update(ctx, move |render_state, _ctx| { let scroll_position = render_state.snapshot_scroll_position(); render_state.add_pending_edit(delta, buffer_version); render_state.scroll_to(scroll_position); }); } /// Sets the document path for resolving relative paths (e.g., relative image paths in markdown). fn set_document_path(&self, path: Option, ctx: &mut ModelContext) { self.render_state().update(ctx, |render_state, _| { render_state.set_document_path(path); }); } fn user_insert(&mut self, text: &str, ctx: &mut ModelContext) { self.insert(text, EditOrigin::UserTyped, ctx); } fn insert(&mut self, text: &str, origin: EditOrigin, ctx: &mut ModelContext) { self.update_content( |mut content, ctx| { content.apply_edit( BufferEditAction::Insert { text, style: self.active_text_style(), override_text_style: None, }, origin, self.buffer_selection_model().clone(), ctx, ); }, ctx, ); self.validate(ctx); } fn system_insert_autoscroll_vertical_only( &mut self, text: &str, ctx: &mut ModelContext, ) { self.update_content( |mut content, ctx| { content.apply_edit_with_autoscroll( BufferEditAction::Insert { text, style: self.active_text_style(), override_text_style: None, }, EditOrigin::SystemEdit, ShouldAutoscroll::VerticalOnly, self.buffer_selection_model().clone(), ctx, ); }, ctx, ); self.validate(ctx); } /// Truncate the buffer to the given length. fn truncate(&mut self, len: usize, ctx: &mut ModelContext) { self.update_content( |mut content, ctx| { let byte_offset: ByteOffset = (len + 1).into(); // TODO(CLD-558) let char_offset = byte_offset.to_buffer_char_offset(content.buffer()); let max_offset = content.buffer().max_charoffset(); if char_offset < max_offset { // Use InsertAtCharOffsetRanges with an empty string to delete the range. // This uses insert_on_selection: false, which clamps anchors instead of // invalidating them. This is important for system edits like streaming // updates where we don't want to invalidate selection anchors. content.apply_edit( BufferEditAction::InsertAtCharOffsetRanges { edits: &vec1![(String::new(), char_offset..max_offset)], }, EditOrigin::SystemEdit, self.buffer_selection_model().clone(), ctx, ); } }, ctx, ); self.validate(ctx); } fn backspace(&mut self, ctx: &mut ModelContext) { // Edit the internal content model. self.update_content( |mut content, ctx| { content.apply_edit( BufferEditAction::Backspace, EditOrigin::UserInitiated, self.buffer_selection_model().clone(), ctx, ) }, ctx, ); self.validate(ctx); } /// Compute the ranges that would be deleted for a given direction and unit. /// Returns None if all selections are not single cursors (i.e., there are ranges). fn replacement_range_for_deletion( &self, direction: TextDirection, unit: TextUnit, ctx: &mut ModelContext, ) -> Option>> { // If any selection is a range, and not a cursor, return None if !self .buffer_selection_model() .as_ref(ctx) .all_single_cursors() { return None; } let selection = self.selection_model().as_ref(ctx); let selections = selection.cursors(ctx); let x_goals = selection .goal_xs .as_ref() .map(|goal| Either::Left(goal.into_iter().map(|x| Some(*x)))) .unwrap_or_else(|| Either::Right(std::iter::repeat_n(None, selections.len()))); let ranges = Vec1::try_from_vec( selections .into_iter() .zip(x_goals) .map(|(start, goal_x)| { let end = selection .navigate(start, direction, unit.clone(), 1, goal_x, ctx) .offset; start.min(end)..start.max(end) }) .collect_vec(), ) .expect("Vec1 should not be empty because input Vec1 cannot be empty."); Some(ranges) } // Internal implementation of the deletion logic. For different editor modes, they should implement their own // delete function as the detail of what content should be written to the clipboard might be slightly different. fn delete_internal( &mut self, direction: TextDirection, unit: TextUnit, cut: bool, write_to_clipboard: B, ctx: &mut ModelContext, ) where B: FnOnce( &ModelHandle, &ModelHandle, Option>>, &mut ModelContext, ), { // If any selection is a range, and not a cursor, then treat this as a backspace. // Otherwise, navigate and delete from each cursor. if let Some(ranges) = self.replacement_range_for_deletion(direction, unit, ctx) { // We don't update the selection before deleting, which means we can't use // `read_selected_text_as_clipboard_content`. That's because undo/redo captures the // selection prior to editing, and we want to keep the user's original cursor. if cut { let content = self.content(); write_to_clipboard( content, self.buffer_selection_model(), Some(ranges.clone()), /* override range */ ctx, ); } self.update_content( |mut content, ctx| { content.apply_edit( BufferEditAction::Delete(ranges), EditOrigin::UserInitiated, self.buffer_selection_model().clone(), ctx, ); }, ctx, ) } else { // If there's already a selection range, treat this as backspace instead of applying // the specific delete action. if cut { let content = self.content(); write_to_clipboard(content, self.buffer_selection_model(), None, ctx); } self.backspace(ctx); } self.validate(ctx); } fn indent(&mut self, shift: bool, ctx: &mut ModelContext) { self.update_content( |mut content, ctx| { content.apply_edit( // By default apply only 1 unit for indentation. BufferEditAction::Indent { num_unit: 1, shift }, EditOrigin::UserInitiated, self.buffer_selection_model().clone(), ctx, ); }, ctx, ); self.validate(ctx) } fn clear_buffer(&mut self, ctx: &mut ModelContext) { self.selection_model().update(ctx, |selection, ctx| { selection.update_selection(BufferSelectAction::SelectAll, AutoScrollBehavior::None, ctx) }); self.update_content( |mut content, ctx| { content.apply_edit( BufferEditAction::Backspace, EditOrigin::SystemEdit, self.buffer_selection_model().clone(), ctx, ); }, ctx, ); self.validate(ctx); } fn undo(&mut self, ctx: &mut ModelContext) { // Edit the internal content model. self.update_content( |mut content, ctx| { content.apply_edit( BufferEditAction::Undo, EditOrigin::UserInitiated, self.buffer_selection_model().clone(), ctx, ) }, ctx, ); self.validate(ctx); } fn redo(&mut self, ctx: &mut ModelContext) { // Edit the internal content model. self.update_content( |mut content, ctx| { content.apply_edit( BufferEditAction::Redo, EditOrigin::UserInitiated, self.buffer_selection_model().clone(), ctx, ) }, ctx, ); self.validate(ctx); } fn select_up(&mut self, ctx: &mut ModelContext) { self.selection_model().update(ctx, |selection, ctx| { selection.extend_selection(TextDirection::Backwards, TextUnit::Line, ctx); }); self.validate(ctx); } fn select_down(&mut self, ctx: &mut ModelContext) { self.selection_model().update(ctx, |selection, ctx| { selection.extend_selection(TextDirection::Forwards, TextUnit::Line, ctx); }); self.validate(ctx); } fn select_left(&mut self, ctx: &mut ModelContext) { self.selection_model().update(ctx, |selection, ctx| { selection.update_selection( BufferSelectAction::ExtendLeft, AutoScrollBehavior::Selection, ctx, ) }); self.validate(ctx); } fn select_right(&mut self, ctx: &mut ModelContext) { self.selection_model().update(ctx, |selection, ctx| { selection.update_selection( BufferSelectAction::ExtendRight, AutoScrollBehavior::Selection, ctx, ) }); self.validate(ctx); } /// Extends the selection to the start of the soft-wrapped line the cursor is on. fn select_to_line_start(&mut self, ctx: &mut ModelContext) { self.selection_model().update(ctx, |selection, ctx| { selection.extend_selection(TextDirection::Backwards, TextUnit::LineBoundary, ctx); }); self.validate(ctx); } /// Extends the selection to the end of the soft-wrapped line the cursor is on. fn select_to_line_end(&mut self, ctx: &mut ModelContext) { self.selection_model().update(ctx, |selection, ctx| { selection.extend_selection(TextDirection::Forwards, TextUnit::LineBoundary, ctx); }); self.validate(ctx); } /// Extends the selection to the start of the paragraph that the cursor is on. fn select_to_paragraph_start(&mut self, ctx: &mut ModelContext) { self.selection_model().update(ctx, |selection, ctx| { selection.extend_selection(TextDirection::Backwards, TextUnit::ParagraphBoundary, ctx) }); self.validate(ctx); } /// Extends the selection to the end of the paragraph that the cursor is on. fn select_to_paragraph_end(&mut self, ctx: &mut ModelContext) { self.selection_model().update(ctx, |selection, ctx| { selection.extend_selection(TextDirection::Forwards, TextUnit::ParagraphBoundary, ctx) }); self.validate(ctx); } fn move_up(&mut self, ctx: &mut ModelContext) { self.selection_model().update(ctx, |selection, ctx| { selection.move_selection(TextDirection::Backwards, TextUnit::Line, ctx); }); self.validate(ctx); } fn move_down(&mut self, ctx: &mut ModelContext) { self.selection_model().update(ctx, |selection, ctx| { selection.move_selection(TextDirection::Forwards, TextUnit::Line, ctx); }); self.validate(ctx); } fn move_left(&mut self, ctx: &mut ModelContext) { self.selection_model().update(ctx, |selection, ctx| { selection.update_selection( BufferSelectAction::MoveLeft, AutoScrollBehavior::Selection, ctx, ) }); self.validate(ctx); } fn move_right(&mut self, ctx: &mut ModelContext) { self.selection_model().update(ctx, |selection, ctx| { selection.update_selection( BufferSelectAction::MoveRight, AutoScrollBehavior::Selection, ctx, ) }); self.validate(ctx); } /// Moves the cursor to the start of the soft-wrapped line it's on. fn move_to_line_start(&mut self, ctx: &mut ModelContext) { self.selection_model().update(ctx, |selection, ctx| { selection.move_selection(TextDirection::Backwards, TextUnit::LineBoundary, ctx); }); self.validate(ctx); } /// Moves the cursor to the end of the soft-wrapped line it's on. fn move_to_line_end(&mut self, ctx: &mut ModelContext) { self.selection_model().update(ctx, |selection, ctx| { selection.move_selection(TextDirection::Forwards, TextUnit::LineBoundary, ctx); }); self.validate(ctx); } /// Moves the cursor to the start of the paragraph it's on. fn move_to_paragraph_start(&mut self, ctx: &mut ModelContext) { self.selection_model().update(ctx, |selection, ctx| { selection.move_selection(TextDirection::Backwards, TextUnit::ParagraphBoundary, ctx); }); self.validate(ctx); } /// Moves the cursor to the end of the paragraph it's on. fn move_to_paragraph_end(&mut self, ctx: &mut ModelContext) { self.selection_model().update(ctx, |selection, ctx| { selection.move_selection(TextDirection::Forwards, TextUnit::ParagraphBoundary, ctx); }); self.validate(ctx); } /// Moves to first nonwhitespace character, optionally keeping selection. fn vim_move_to_first_nonwhitespace( &mut self, keep_selection: bool, ctx: &mut ModelContext, ) { let current_selections = self.selections(ctx); let content = self.content().as_ref(ctx); let new_selections = current_selections.mapped(|selection_offset| { let cursor_offset = selection_offset.head; let first_nonwhitespace = content.containing_line_first_nonwhitespace(cursor_offset); SelectionOffsets { head: first_nonwhitespace, tail: if keep_selection { selection_offset.tail } else { first_nonwhitespace }, } }); self.update_selection( BufferSelectAction::SetSelectionOffsets { selections: new_selections, }, AutoScrollBehavior::Selection, ctx, ); } fn select_all(&mut self, ctx: &mut ModelContext) { self.update_selection(BufferSelectAction::SelectAll, AutoScrollBehavior::None, ctx); } /// Moves the selection forward to the next word ending. If `select` is true, this extends the /// selection to that location. Otherwise, it moves the cursor. fn forward_word_with_unit( &mut self, select: bool, unit: TextUnit, ctx: &mut ModelContext, ) { self.selection_model().update(ctx, |selection, ctx| { if select { selection.extend_selection(TextDirection::Forwards, unit, ctx); } else { selection.move_selection(TextDirection::Forwards, unit, ctx); } }); } /// Moves the selection back to the previous word start. If `select` is true, this extends the /// selection to that location. Otherwise, it moves the cursor. fn backward_word_with_unit( &mut self, select: bool, unit: TextUnit, ctx: &mut ModelContext, ) { self.selection_model().update(ctx, |selection, ctx| { if select { selection.extend_selection(TextDirection::Backwards, unit, ctx); } else { selection.move_selection(TextDirection::Backwards, unit, ctx); } }); } fn selections(&self, ctx: &AppContext) -> Vec1 { self.buffer_selection_model() .as_ref(ctx) .selection_offsets() } /// Return whether there is currently a single selection, either a single cursor or a single range. fn has_single_selection(&self, ctx: &AppContext) -> bool { self.buffer_selection_model().as_ref(ctx).selections().len() == 1 } fn selection_is_single_cursor(&self, ctx: &AppContext) -> bool { let selection_model = self.buffer_selection_model().as_ref(ctx); selection_model.selections().len() == 1 && selection_model.first_selection_is_single_cursor() } /// Return whether there is currently a single selection, and that selection is a single range, not /// just a cursor fn selection_is_single_range(&self, ctx: &AppContext) -> bool { let selection_model = self.buffer_selection_model().as_ref(ctx); selection_model.selections().len() == 1 && !selection_model.first_selection_is_single_cursor() } fn selection_head(&self, ctx: &AppContext) -> CharOffset { // TODO(CLD-558): This matches how we shift the selection by 1. self.buffer_selection_model() .as_ref(ctx) .first_selection_head() - 1 } fn logical_line_start(&self, offset: CharOffset, ctx: &AppContext) -> CharOffset { // TODO(CLD-558) self.content().as_ref(ctx).containing_line_start(offset) - 1 } fn logical_line_end(&self, offset: CharOffset, ctx: &AppContext) -> CharOffset { // TODO(CLD-558) self.content().as_ref(ctx).containing_line_end(offset) - 1 } /// Move the cursor to an exact content location. fn cursor_at(&self, offset: CharOffset, ctx: &mut ModelContext) { self.selection_model() .update(ctx, |selection, ctx| selection.set_cursor(offset, ctx)); self.validate(ctx); } fn add_cursor_at(&self, offset: CharOffset, ctx: &mut ModelContext) { self.selection_model() .update(ctx, |selection, ctx| selection.add_cursor(offset, ctx)); self.validate(ctx); } fn set_last_selection_head(&mut self, offset: CharOffset, ctx: &mut ModelContext) { self.selection_model().update(ctx, |selection, ctx| { selection.set_last_head(offset, ctx); }); self.validate(ctx); } /// Update the in-progress selection with a new cursor location (usually the result of dragging). fn update_pending_selection(&mut self, offset: CharOffset, ctx: &mut ModelContext) { self.selection_model().update(ctx, |selection, ctx| { selection.update_pending_selection(offset, ctx); }); self.validate(ctx); } /// Begin selecting at `offset`. The initial selection depends on the given mode. fn begin_selection( &mut self, offset: CharOffset, mode: SelectionMode, clear_selections: bool, ctx: &mut ModelContext, ) { self.selection_model().update(ctx, |selection, ctx| { selection.begin_selection(offset, mode, clear_selections, ctx) }); self.validate(ctx); } /// End any ongoing selection actions. When dragging to select, this should be called when the /// mouse is released. fn end_selection(&mut self, ctx: &mut ModelContext) { self.selection_model().update(ctx, |selection, ctx| { selection.end_selection(ctx); }); self.validate(ctx); } /// This is a helper for semantic-selection actions. fn update_selection( &mut self, action: BufferSelectAction, autoscroll: AutoScrollBehavior, ctx: &mut ModelContext, ) { self.selection_model().update(ctx, |selection, ctx| { selection.update_selection(action, autoscroll, ctx); }); self.validate(ctx); } } pub trait PlainTextEditorModel: CoreEditorModel { fn enter(&mut self, ctx: &mut ModelContext) { self.update_content( |mut content, ctx| { content.apply_edit( BufferEditAction::Enter { style: self.active_text_style(), force_newline: false, }, EditOrigin::UserTyped, self.buffer_selection_model().clone(), ctx, ); }, ctx, ); self.validate(ctx); } fn read_text_as_clipboard_content( &self, range: Range, ctx: &AppContext, ) -> ClipboardContent { let buffer = self.content().as_ref(ctx); ClipboardContent::plain_text(buffer.text_in_range(range).into_string()) } fn read_selected_text_as_clipboard_content(&self, ctx: &AppContext) -> ClipboardContent { let buffer = self.content().as_ref(ctx); ClipboardContent::plain_text( buffer .selected_text_as_plain_text(self.buffer_selection_model().clone(), ctx) .into_string(), ) } fn copy_all(&self, ctx: &mut ModelContext) { let end = self.content().as_ref(ctx).max_charoffset(); let clipboard = self.read_text_as_clipboard_content(CharOffset::from(1)..end, ctx); ctx.clipboard().write(clipboard); } fn delete( &mut self, direction: TextDirection, unit: TextUnit, cut: bool, ctx: &mut ModelContext, ) { self.delete_internal( direction, unit, cut, |buffer, selection_model, override_range, ctx| { let buffer = buffer.as_ref(ctx); let ranges = match override_range { Some(ranges) => ranges, None => selection_model.as_ref(ctx).selections_to_offset_ranges(), }; // TODO: this should allow passing in the line ending mode. let content = ClipboardContent::plain_text( buffer.text_in_ranges(ranges, LineEnding::LF).into_string(), ); ctx.clipboard().write(content); }, ctx, ); } fn reset(&mut self, state: InitialBufferState, ctx: &mut ModelContext) { self.update_content( |mut content, ctx| { let version = state.version; content.buffer().reset_undo_stack(); content.apply_edit( BufferEditAction::ReplaceWith(state), EditOrigin::SystemEdit, self.buffer_selection_model().clone(), ctx, ); content.buffer().set_version(version); }, ctx, ); self.validate(ctx); } } pub trait RichTextEditorModel: CoreEditorModel { /// Handle an Enter key. This will usually insert a new line, but has some specific rich-text /// interactions: /// * Enter at the start of a block shifts the block down /// * Enter at the start of an empty list item un-indents it. fn enter(&mut self, ctx: &mut ModelContext) { let selection_model = self.buffer_selection_model().clone(); self.update_content( |mut content, ctx| { content.apply_edit( BufferEditAction::Enter { style: self.active_text_style(), force_newline: false, }, EditOrigin::UserTyped, selection_model, ctx, ); }, ctx, ); self.validate(ctx); } /// Insert a new line. fn newline(&mut self, ctx: &mut ModelContext) { self.update_content( |mut content, ctx| { content.apply_edit( BufferEditAction::Enter { style: self.active_text_style(), force_newline: true, }, EditOrigin::UserTyped, self.buffer_selection_model().clone(), ctx, ); }, ctx, ); self.validate(ctx); } /// Read a range of the buffer as clipboard content. This will copy both the plain text and the /// rich text as HTML. fn read_text_as_clipboard_content( &self, range: Range, ctx: &AppContext, ) -> ClipboardContent { let buffer = self.content().as_ref(ctx); let ranges = vec1![range]; let mut clipboard = ClipboardContent::plain_text( buffer.text_in_ranges_with_expanded_embedded_items(ranges.clone(), ctx), ); clipboard.html = buffer.ranges_as_html(ranges.clone(), ctx); clipboard } fn read_selected_text_as_clipboard_content(&self, ctx: &AppContext) -> ClipboardContent { let buffer = self.content().as_ref(ctx); let mut clipboard = ClipboardContent::plain_text( buffer .selected_text_as_plain_text(self.buffer_selection_model().clone(), ctx) .into_string(), ); clipboard.html = buffer.selected_text_as_html(self.buffer_selection_model().clone(), ctx); clipboard } fn copy_all(&self, ctx: &mut ModelContext) { let end = self.content().as_ref(ctx).max_charoffset(); let clipboard = self.read_text_as_clipboard_content(CharOffset::from(1)..end, ctx); ctx.clipboard().write(clipboard); } fn update_code_block_type_at_offset( &mut self, code_block_type: &CodeBlockType, start: Anchor, ctx: &mut ModelContext, ) { let selection_model = self.buffer_selection_model().clone(); self.update_content( |mut content, ctx| { if let Some(start_offset) = selection_model.as_ref(ctx).resolve_anchor(&start) { content.apply_edit( BufferEditAction::UpdateCodeBlockTypeAtOffset { start: start_offset, code_block_type: code_block_type.clone(), }, EditOrigin::UserInitiated, selection_model, ctx, ); } }, ctx, ); self.validate(ctx); } fn toggle_task_list(&mut self, offset: CharOffset, ctx: &mut ModelContext) { self.update_content( |mut content, ctx| { content.apply_edit( BufferEditAction::ToggleTaskListAtOffset { start: offset }, EditOrigin::UserInitiated, self.buffer_selection_model().clone(), ctx, ); }, ctx, ); self.validate(ctx); } fn reset_with_markdown(&mut self, markdown: &str, ctx: &mut ModelContext) { let state = InitialBufferState::markdown(markdown); self.update_content( |mut content, ctx| { content.buffer().reset_undo_stack(); content.apply_edit( BufferEditAction::ReplaceWith(state), EditOrigin::SystemEdit, self.buffer_selection_model().clone(), ctx, ); }, ctx, ); self.validate(ctx); } fn update_to_new_markdown(&mut self, markdown: &str, ctx: &mut ModelContext) { use crate::content::buffer::StyledBlockBoundaryBehavior; use markdown_parser::{compute_formatted_text_delta, parse_markdown}; // Try to obtain the current formatted-text view from the buffer and // compute both the `FormattedText` delta and the common prefix length in // characters between the old and new markdown. let delta_result = (|| { let buffer = self.content().as_ref(ctx); let full_range = CharOffset::from(1)..buffer.max_charoffset(); let old_formatted = buffer.range_to_formatted_text(full_range, StyledBlockBoundaryBehavior::Inclusive); let new_formatted = match parse_markdown(markdown) { Ok(parsed) => parsed, Err(_) => return None, }; let delta = compute_formatted_text_delta(old_formatted, new_formatted); Some(delta) })(); match delta_result { Some(delta) => { // If there is nothing to change, bail out early. if delta.is_noop() { return; } self.update_content( |mut content, ctx| { content.buffer().apply_formatted_text_delta( &delta, self.buffer_selection_model().clone(), ctx, ); }, ctx, ); self.validate(ctx); } None => { // Fallback to the existing full-reset behavior if we fail to // compute a delta (e.g. parse error). log::warn!("Failed to compute formatted text delta, falling back to full reset"); self.reset_with_markdown(markdown, ctx); } } } fn delete( &mut self, direction: TextDirection, unit: TextUnit, cut: bool, ctx: &mut ModelContext, ) { self.delete_internal( direction, unit, cut, move |buffer, selection_model, override_range, ctx| { let buffer = buffer.as_ref(ctx); let ranges = match override_range { Some(range) => range, None => selection_model.as_ref(ctx).selections_to_offset_ranges(), }; let content = ClipboardContent { plain_text: buffer .text_in_ranges_with_expanded_embedded_items(ranges.clone(), ctx), html: buffer.ranges_as_html(ranges.clone(), ctx), ..Default::default() }; ctx.clipboard().write(content); }, ctx, ); } fn set_link(&mut self, tag: String, url: String, ctx: &mut ModelContext) { let selection_model = self.buffer_selection_model().clone(); self.update_content( move |mut content, ctx| { content.apply_edit( BufferEditAction::Link { tag, url }, EditOrigin::UserInitiated, selection_model, ctx, ); }, ctx, ); self.validate(ctx); } fn unset_link(&mut self, ctx: &mut ModelContext) { let selection_model = self.buffer_selection_model().clone(); self.update_content( move |mut content, ctx| { content.apply_edit( BufferEditAction::Unlink, EditOrigin::UserInitiated, selection_model, ctx, ); }, ctx, ); self.validate(ctx); } fn insert_block_item(&mut self, block_item: BufferBlockItem, ctx: &mut ModelContext) { let selection_model = self.buffer_selection_model().clone(); self.update_content( |mut content, ctx| { content.apply_edit( BufferEditAction::InsertBlockItem { block_item }, EditOrigin::UserInitiated, selection_model, ctx, ) }, ctx, ); self.validate(ctx); } /// Insert a new `block_type` block after the block starting at `block_offset`. fn insert_block_after( &mut self, block_offset: CharOffset, block_type: BlockType, ctx: &mut ModelContext, ) { // Set a single cursor to remove other selections. self.cursor_at(block_offset, ctx); let selection_model = self.buffer_selection_model().clone(); self.update_content( |mut content, ctx| { content.apply_edit( BufferEditAction::InsertBlockAfterBlockWithOffset { block_type, offset: block_offset, }, EditOrigin::UserInitiated, selection_model, ctx, ) }, ctx, ); self.validate(ctx); } fn remove_embedding_at(&mut self, offset: CharOffset, ctx: &mut ModelContext) { let selection_model = self.buffer_selection_model().clone(); self.update_content( |mut content, ctx| { content.apply_edit( BufferEditAction::RemoveEmbeddingAtOffset { offset_before_marker: offset, }, EditOrigin::UserInitiated, selection_model, ctx, ) }, ctx, ); self.validate(ctx); } fn insert_placeholder(&mut self, ctx: &mut ModelContext) { let selection_model = self.buffer_selection_model().clone(); self.update_content( |mut content, ctx| { // We can loop over the selections and dispatch updates because these updates are not undoable. // Hacky: We iterate over the selections in reverse order by char offset so that earlier // updates don't change the offsets of later updates. let selections = content .buffer() .to_rendered_selection_set(selection_model.clone(), ctx) .into_iter() .filter(|s| s.is_cursor()) .sorted_by_key(|s| s.start()) .rev(); for selection in selections { content.apply_edit( BufferEditAction::InsertPlaceholder { text: "Hello, World!", location: selection.start(), }, EditOrigin::SystemEdit, selection_model.clone(), ctx, ); } }, ctx, ); self.validate(ctx); } /// Change the block type of the current selection to `style`. fn set_block_style(&mut self, style: BufferBlockStyle, ctx: &mut ModelContext) { let selection_model = self.buffer_selection_model().clone(); self.update_content( |mut content, ctx| { content.apply_edit( BufferEditAction::StyleBlock(style), EditOrigin::UserInitiated, selection_model, ctx, ) }, ctx, ); self.validate(ctx); } fn list_indent_at_selection( content: &Buffer, selection_model: ModelHandle, ctx: &AppContext, ) -> Option { match content.active_block_type_at_first_selection(selection_model.as_ref(ctx)) { BlockType::Text(BufferBlockStyle::OrderedList { indent_level, .. }) => { Some(indent_level) } BlockType::Text(BufferBlockStyle::UnorderedList { indent_level }) => Some(indent_level), BlockType::Text(BufferBlockStyle::TaskList { indent_level, .. }) => Some(indent_level), _ => None, } } /// Convert the current selection to `style`. Unlike [`Self::set_block_style`], this preserves /// the indentation level when converting between lists. fn convert_block(&mut self, mut style: BufferBlockStyle, ctx: &mut ModelContext) { let selection_model = self.buffer_selection_model().clone(); self.update_content( |mut content, ctx| { if let BufferBlockStyle::OrderedList { indent_level, .. } | BufferBlockStyle::UnorderedList { indent_level } | BufferBlockStyle::TaskList { indent_level, .. } = &mut style && let Some(existing_indent) = Self::list_indent_at_selection( content.buffer(), selection_model.clone(), ctx, ) { *indent_level = existing_indent; } content.apply_edit( BufferEditAction::StyleBlock(style), EditOrigin::UserInitiated, selection_model, ctx, ); }, ctx, ); self.validate(ctx); } // If the active selection is a cursor at a link, expand the selection to the link. Otherwise, no-op. // Return true if the selection is successfully expanded. fn try_select_active_link(&mut self, ctx: &mut ModelContext) -> bool { let selection_model = self.buffer_selection_model().clone(); let range_action = self.content().update(ctx, |content, ctx| { if !selection_model .as_ref(ctx) .first_selection_is_single_cursor() { return None; } let current_offset = selection_model.as_ref(ctx).first_selection_head(); let selection_head = content.containing_link_start(current_offset)?; let selection_tail = content.containing_link_end(current_offset)?; Some(BufferSelectAction::SetSelectionOffsets { selections: vec1![SelectionOffsets { head: selection_head, tail: selection_tail, }], }) }); match range_action { Some(range_action) => { self.selection_model().update(ctx, |selection, ctx| { selection.update_selection(range_action, AutoScrollBehavior::Selection, ctx) }); true } None => false, } } }