1221 lines
43 KiB
Rust
1221 lines
43 KiB
Rust
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<BufferSelectionModel>,
|
|
ctx: &mut ModelContext<Buffer>,
|
|
) {
|
|
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<BufferSelectionModel>,
|
|
ctx: &mut ModelContext<Buffer>,
|
|
) {
|
|
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<Buffer>;
|
|
fn buffer_selection_model(&self) -> &ModelHandle<BufferSelectionModel>;
|
|
fn selection_model(&self) -> &ModelHandle<SelectionModel>;
|
|
fn render_state(&self) -> &ModelHandle<RenderState>;
|
|
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<Self::T>,
|
|
) {
|
|
}
|
|
|
|
fn update_content(
|
|
&self,
|
|
action: impl FnOnce(BufferUpdateWrapper, &mut ModelContext<Buffer>),
|
|
ctx: &mut ModelContext<Self::T>,
|
|
) {
|
|
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<Self::T>) {
|
|
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<std::path::PathBuf>, ctx: &mut ModelContext<Self::T>) {
|
|
self.render_state().update(ctx, |render_state, _| {
|
|
render_state.set_document_path(path);
|
|
});
|
|
}
|
|
|
|
fn user_insert(&mut self, text: &str, ctx: &mut ModelContext<Self::T>) {
|
|
self.insert(text, EditOrigin::UserTyped, ctx);
|
|
}
|
|
|
|
fn insert(&mut self, text: &str, origin: EditOrigin, ctx: &mut ModelContext<Self::T>) {
|
|
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::T>,
|
|
) {
|
|
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::T>) {
|
|
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<Self::T>) {
|
|
// 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<Self::T>,
|
|
) -> Option<Vec1<Range<CharOffset>>> {
|
|
// 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<B>(
|
|
&mut self,
|
|
direction: TextDirection,
|
|
unit: TextUnit,
|
|
cut: bool,
|
|
write_to_clipboard: B,
|
|
ctx: &mut ModelContext<Self::T>,
|
|
) where
|
|
B: FnOnce(
|
|
&ModelHandle<Buffer>,
|
|
&ModelHandle<BufferSelectionModel>,
|
|
Option<Vec1<Range<CharOffset>>>,
|
|
&mut ModelContext<Self::T>,
|
|
),
|
|
{
|
|
// 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::T>) {
|
|
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::T>) {
|
|
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<Self::T>) {
|
|
// 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<Self::T>) {
|
|
// 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::T>) {
|
|
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::T>) {
|
|
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::T>) {
|
|
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::T>) {
|
|
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::T>) {
|
|
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::T>) {
|
|
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::T>) {
|
|
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::T>) {
|
|
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::T>) {
|
|
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::T>) {
|
|
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::T>) {
|
|
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::T>) {
|
|
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::T>) {
|
|
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::T>) {
|
|
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::T>) {
|
|
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::T>) {
|
|
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<Self::T>,
|
|
) {
|
|
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::T>) {
|
|
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::T>,
|
|
) {
|
|
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::T>,
|
|
) {
|
|
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<SelectionOffsets> {
|
|
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::T>) {
|
|
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::T>) {
|
|
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::T>) {
|
|
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::T>) {
|
|
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::T>,
|
|
) {
|
|
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::T>) {
|
|
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::T>,
|
|
) {
|
|
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::T>) {
|
|
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<CharOffset>,
|
|
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<Self::T>) {
|
|
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::T>,
|
|
) {
|
|
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::T>) {
|
|
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<Self::T>) {
|
|
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::T>) {
|
|
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<CharOffset>,
|
|
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<Self::T>) {
|
|
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<Self::T>,
|
|
) {
|
|
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::T>) {
|
|
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<Self::T>) {
|
|
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<Self::T>) {
|
|
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::T>,
|
|
) {
|
|
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<Self::T>) {
|
|
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<Self::T>) {
|
|
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<Self::T>) {
|
|
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<Self::T>,
|
|
) {
|
|
// 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<Self::T>) {
|
|
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<Self::T>) {
|
|
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<Self::T>) {
|
|
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<BufferSelectionModel>,
|
|
ctx: &AppContext,
|
|
) -> Option<ListIndentLevel> {
|
|
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<Self::T>) {
|
|
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<Self::T>) -> 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,
|
|
}
|
|
}
|
|
}
|