Files
galaxy/app/src/editor/view/model/mod.rs
T

3226 lines
123 KiB
Rust

mod buffer;
mod display_map;
mod selections;
use self::buffer::Peer;
pub use {
buffer::{
Anchor, AnchorBias, Chars, EditOrigin, Operation as CrdtOperation, PeerSelectionData,
ReplicaId, SubwordBoundaries, TextRun, TextStyleOperation, ToBufferOffset, ToCharOffset,
ToPoint,
},
display_map::{Bias, DisplayMap, DisplayPoint, MovementResult, ToDisplayPoint},
selections::{
DrawableSelection, LocalDrawableSelectionData, LocalPendingSelection, LocalSelection,
LocalSelections, MarkedTextState, RemoteDrawableSelectionData, SelectAction, Selection,
SelectionMode,
},
};
use std::{
cmp::{self},
collections::{HashMap, HashSet},
mem,
ops::Range,
rc::Rc,
};
use num_traits::SaturatingSub;
use string_offset::{ByteOffset, CharOffset};
use vec1::{vec1, Vec1};
use galaxyui::{
accessibility::{AccessibilityContent, WarpA11yRole},
text_layout::TextStyle,
AppContext, Entity, ModelAsRef, ModelContext, ModelHandle,
};
use galaxyui::{
text::{point::Point, word_boundaries::WordBoundariesPolicy, TextBuffer},
SingletonEntity,
};
use crate::{editor::RangeExt, vim_registers::VimRegisters};
use vim::{
find_next_paragraph_end, find_previous_paragraph_start,
vim::{
BracketChar, CharacterMotion, Direction, FindCharMotion, FirstNonWhitespaceMotion,
LineMotion, MotionType, TextObjectInclusion, TextObjectType, VimOperator, WordBound,
WordMotion,
},
vim_a_paragraph, vim_inner_paragraph,
};
use vim::{
vim_a_block, vim_a_quote, vim_a_word, vim_find_char_on_line, vim_find_matching_bracket,
vim_inner_block, vim_inner_quote, vim_inner_word, vim_word_iterator_from_offset,
};
use buffer::{Buffer, Text};
use super::{movement, PlainTextEditorViewAction, SelectionInsertion, ValidInputType};
use itertools::{
FoldWhile::{Continue, Done},
Itertools,
};
use lazy_static::lazy_static;
lazy_static! {
static ref AUTOCOMPLETE_SYMBOLS: HashMap<&'static str, &'static str> = HashMap::from([
("(", ")"),
("[", "]"),
("{", "}"),
("\'", "\'"),
("\"", "\""),
]);
static ref CLOSING_SYMBOLS: HashSet<&'static str> =
AUTOCOMPLETE_SYMBOLS.values().cloned().collect();
}
/// A snapshot of the editor that does not expose
/// buffer implementation details.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EditorSnapshot {
/// When taking a snapshot, we intentionally use char offsets.
/// We can't use the [`LocalSelection`] type because those rely
/// on anchors which are associated to edit IDs. Using char offsets
/// allows us to ensure we can restore selection state _across_
/// buffers (e.g. ephemeral -> regular).
selections: Vec1<Range<CharOffset>>,
/// Similar to the reason we use offsets instead of anchors
/// for [`Self::selections`], we use [`TextRun`]s instead
/// of [`Fragment`]s or similar to describe the buffer text.
buffer_text_runs: Vec<TextRun>,
}
impl EditorSnapshot {
fn new(selections: Vec1<Range<CharOffset>>, buffer_text_runs: Vec<TextRun>) -> Self {
Self {
selections,
buffer_text_runs,
}
}
fn buffer_text_runs(&self) -> &Vec<TextRun> {
&self.buffer_text_runs
}
pub fn text(&self) -> String {
TextRun::text_from_text_runs(&self.buffer_text_runs)
}
pub fn selections(&self) -> &Vec1<Range<CharOffset>> {
&self.selections
}
}
/// Enum representing editor responses to edit and selection actions.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum InteractionState {
/// Editor will respond to both edits and selection actions; cursor shown.
Editable,
/// Editor was set in an editable state, but has selections that cannot be edited.
EditableWithInvalidSelection,
/// Editor will respond to selection actions, and ignore edit actions. No cursor is shown.
Selectable,
/// Editor will ignore selection and edit actions. Editor is rendered with dimmed text and no cursor.
Disabled,
}
type StaticMutableModelCallback = fn(&mut EditorModel, &mut ModelContext<EditorModel>);
type StaticCallback = fn(&EditorModel, &mut ModelContext<EditorModel>);
#[derive(Copy, Clone)]
pub enum UpdateBufferOption {
/// No special behaviour.
None,
/// The edits are not recorded on the undo / redo stack.
#[allow(dead_code)]
SkipUndoRedoRecord,
/// The edits are recorded in a dedicated, ephemeral buffer
/// so as not to change the real buffer.
/// Ephemeral edits can be materialized as soon as a non-ephemeral edit
/// is made. This is done by replacing the real buffer with the ephemeral
/// buffer before the non-ephemeral edit is applied.
IsEphemeral,
}
impl UpdateBufferOption {
fn skip_undo(&self) -> bool {
matches!(self, Self::SkipUndoRedoRecord | Self::IsEphemeral)
}
}
/// A helper struct to group together state needed
/// to specify how the buffer should be updated.
struct UpdateBuffer<U>
where
U: FnOnce(&mut EditorModel, &mut ModelContext<EditorModel>),
{
action: PlainTextEditorViewAction,
edit_origin: EditOrigin,
option: UpdateBufferOption,
callback: U,
}
impl<U> UpdateBuffer<U>
where
U: FnOnce(&mut EditorModel, &mut ModelContext<EditorModel>),
{
fn skip_undo(&self) -> bool {
self.option.skip_undo()
}
}
/// Helper struct to change the underlying buffer via the model.
/// By edit, we mean string manipulation of the buffer or selection changes.
///
/// The view does not have access to the buffer so instead we expose
/// this struct for the view to declare how the buffer should be changed
/// while the model is responsible for actually changing the buffer
/// and updating corresponding state.
///
/// There are 4 available, optional callbacks. They are executed in the following order:
/// 1) change_selection_callback
/// 2) before_buffer_edit_callback
/// 3) update_buffer_callback
/// 4) post_buffer_edit_change_selection_callback
///
/// The buffer changes specified by a single `Edits` instance
/// are considered atomic / batched. See [`EditorModel::edit`] for how this struct is used.
///
/// TODO (suraj): technically, there's nothing enforcing that these callbacks
/// only do what they're described to do (e.g. the change_selection callback
/// can edit the buffer). To enforce this, we should only expose what needs
/// to be exposed in the respective callbacks. For example, the selection-related
/// callbacks only need the mutable selection state in the callback (not the
/// whole mutable model state). Similarly, for the edit callback, we only need
/// the mutable Buffer. To keep using the single API, we can have the editor model
/// helper APIs return callbacks that can then be used as the callbacks in `Edits`.
/// The view APIs can use this to compose an edit.
pub struct Edits<C, B, U, G>
where
C: FnOnce(&mut EditorModel, &mut ModelContext<EditorModel>),
B: FnOnce(&EditorModel, &mut ModelContext<EditorModel>),
U: FnOnce(&mut EditorModel, &mut ModelContext<EditorModel>),
G: FnOnce(&mut EditorModel, &mut ModelContext<EditorModel>),
{
change_selections_callback: Option<C>,
before_buffer_edit_callback: Option<B>,
update_buffer: Option<UpdateBuffer<U>>,
post_buffer_edit_change_selections_callback: Option<G>,
}
impl
Edits<
StaticMutableModelCallback,
StaticCallback,
StaticMutableModelCallback,
StaticMutableModelCallback,
>
{
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
Self {
change_selections_callback: None,
before_buffer_edit_callback: None,
update_buffer: None,
post_buffer_edit_change_selections_callback: None,
}
}
}
impl<C, B, U, G> Edits<C, B, U, G>
where
C: FnOnce(&mut EditorModel, &mut ModelContext<EditorModel>),
B: FnOnce(&EditorModel, &mut ModelContext<EditorModel>),
U: FnOnce(&mut EditorModel, &mut ModelContext<EditorModel>),
G: FnOnce(&mut EditorModel, &mut ModelContext<EditorModel>),
{
/// Allows callers to specify a callback to update the selection state
/// as part of an edit.
pub fn with_change_selections<T>(self, change_selection: T) -> Edits<T, B, U, G>
where
T: FnOnce(&mut EditorModel, &mut ModelContext<EditorModel>),
{
Edits {
change_selections_callback: Some(change_selection),
before_buffer_edit_callback: self.before_buffer_edit_callback,
update_buffer: self.update_buffer,
post_buffer_edit_change_selections_callback: self
.post_buffer_edit_change_selections_callback,
}
}
/// An intermediate callback between `change_selections` and `update_buffer`.
/// The model provided by the callback is _not_ mutable.
pub fn with_before_buffer_edit<T>(self, before_buffer_edit: T) -> Edits<C, T, U, G>
where
T: FnOnce(&EditorModel, &mut ModelContext<EditorModel>),
{
Edits {
change_selections_callback: self.change_selections_callback,
before_buffer_edit_callback: Some(before_buffer_edit),
update_buffer: self.update_buffer,
post_buffer_edit_change_selections_callback: self
.post_buffer_edit_change_selections_callback,
}
}
/// Allows callers to specify a callback to update the buffer
/// as part of an edit.
pub fn with_update_buffer<T>(
self,
action: PlainTextEditorViewAction,
edit_origin: EditOrigin,
callback: T,
) -> Edits<C, B, T, G>
where
T: FnOnce(&mut EditorModel, &mut ModelContext<EditorModel>),
{
let update_buffer = UpdateBuffer {
action,
edit_origin,
option: UpdateBufferOption::None,
callback,
};
Edits {
change_selections_callback: self.change_selections_callback,
before_buffer_edit_callback: self.before_buffer_edit_callback,
update_buffer: Some(update_buffer),
post_buffer_edit_change_selections_callback: self
.post_buffer_edit_change_selections_callback,
}
}
/// Allows callers to specify a callback to update the buffer
/// as part of an edit, without adding an entry to the undo / redo stack.
///
/// This should be used very sparingly. Prefer to use
/// [`Self::with_update_buffer`].
pub fn with_update_buffer_options<T>(
self,
action: PlainTextEditorViewAction,
edit_origin: EditOrigin,
option: UpdateBufferOption,
callback: T,
) -> Edits<C, B, T, G>
where
T: FnOnce(&mut EditorModel, &mut ModelContext<EditorModel>),
{
let update_buffer = UpdateBuffer {
action,
edit_origin,
option,
callback,
};
Edits {
change_selections_callback: self.change_selections_callback,
before_buffer_edit_callback: self.before_buffer_edit_callback,
update_buffer: Some(update_buffer),
post_buffer_edit_change_selections_callback: self
.post_buffer_edit_change_selections_callback,
}
}
/// Allows callers to specify a callback to update the selection state
/// after the buffer has been updated.
pub fn with_post_buffer_edit_change_selections<T>(
self,
post_buffer_edit_change_selections: T,
) -> Edits<C, B, U, T>
where
T: FnOnce(&mut EditorModel, &mut ModelContext<EditorModel>),
{
Edits {
change_selections_callback: self.change_selections_callback,
before_buffer_edit_callback: self.before_buffer_edit_callback,
update_buffer: self.update_buffer,
post_buffer_edit_change_selections_callback: Some(post_buffer_edit_change_selections),
}
}
fn is_pure_selection_change(&self) -> bool {
self.update_buffer.is_none() && self.includes_selection_change()
}
fn includes_selection_change(&self) -> bool {
self.change_selections_callback.is_some()
|| self.post_buffer_edit_change_selections_callback.is_some()
}
fn is_ephemeral(&self) -> bool {
self.update_buffer
.as_ref()
.is_some_and(|u| matches!(u.option, UpdateBufferOption::IsEphemeral))
}
}
struct BufferAndDisplayMaps {
/// The main buffer and display map.
regular: (ModelHandle<Buffer>, ModelHandle<DisplayMap>),
/// A buffer and display map dedicated for ephemeral edits (see [`UpdateBufferOption::IsEphemeral`]).
/// If [`Some`], then the ephemeral buffer is active.
ephemeral: Option<(ModelHandle<Buffer>, ModelHandle<DisplayMap>)>,
}
impl BufferAndDisplayMaps {
/// Returns a [`ModelHandle`] to the active [`Buffer`].
fn buffer_handle(&self) -> &ModelHandle<Buffer> {
if let Some((buffer, _)) = self.ephemeral.as_ref() {
buffer
} else {
&self.regular.0
}
}
/// Returns a [`ModelHandle`] to the active [`DisplayMap`].
fn display_map_handle(&self) -> &ModelHandle<DisplayMap> {
if let Some((_, display_map)) = self.ephemeral.as_ref() {
display_map
} else {
&self.regular.1
}
}
/// Deactivates any ephemeral state.
fn deactivate_ephemeral_state(&mut self) {
self.ephemeral.take();
}
/// Activates a new ephemeral state.
fn activate_new_ephemeral_state(&mut self, ctx: &mut ModelContext<EditorModel>) {
let tab_size = self.regular.1.as_ref(ctx).tab_size();
let ephemeral_buffer = ctx.add_model(|_| Buffer::new(""));
let ephemeral_display_map: ModelHandle<DisplayMap> =
ctx.add_model(|ctx| DisplayMap::new(ephemeral_buffer.clone(), tab_size, ctx));
ctx.subscribe_to_model(
&ephemeral_buffer,
EditorModel::handle_buffer_event_for_non_collaborative_editor,
);
ctx.subscribe_to_model(
&ephemeral_display_map,
EditorModel::handle_display_map_event,
);
self.ephemeral = Some((ephemeral_buffer, ephemeral_display_map));
}
}
pub struct EditorModel {
/// The extent to which the editor is interactable.
interaction_state: InteractionState,
/// The [`Buffer`]s and [`DisplayMap`]s used by this editor.
/// Use [`Self::buffer`] and [`Self::display_map`] to access the
/// correct buffer and display map, respectively.
buffer_and_display_map: BufferAndDisplayMaps,
/// This field stores the editor-specific state needed for Vim's visual mode. Visual mode works
/// differently from the typical editor selection. The typical editor selection stores two
/// Anchors in [`Selection::start`] and [`Selection::end`] to delimit the selection range,
/// either of which may be the current cursor position, or "head". However, the Visual mode
/// selection range may be different from the cursor position. In linewise visual mode,
/// selection start and end are at line boundaries, for example. Therefore, visual mode derives
/// the selection range from the current cursor position, "head", and the "visual tail" which
/// is the anchor for where the cursor was _when visual mode was entered_. This field stores
/// where the cursor(s) was at the time visual mode was entered.
vim_visual_tails: Vec<Anchor>,
/// Counter for recording whether the last editor action contains an autocompleted symbol.
/// This increments with each consecutive autocompleted insertion.
consecutive_autocomplete_insertion_edits_counter: usize,
/// The maximum buffer length for the editor. Any operations that cause the buffer to exceed
/// this length will be rejected.
max_buffer_len: Option<usize>,
/// The types of inputs that are valid for this editor.
valid_input_type: ValidInputType,
/// The buffer text before the last edit / undo / redo.
last_buffer_text: String,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum EditorModelEvent {
Edited {
edit_origin: EditOrigin,
},
StylesUpdated,
DisplayMapUpdated,
SelectionsChanged,
ShellCut(String),
UpdatePeers {
operations: Rc<Vec<CrdtOperation>>,
},
/// An edit action has replaced the entire buffer's content.
BufferReplaced,
}
impl Entity for EditorModel {
type Event = EditorModelEvent;
}
/// The public interface.
impl EditorModel {
/// Helper method to emit a11y event when the selections change. For now, only single cursor
/// selections are supported.
/// The app will emit values based on the following matrix:
///
/// | Start | End | What should be read |
/// |-------------- |-------------- |--------------------- |
/// | No selection | No selection | Delta (characters) |
/// | No selection | Selection | "Selected: {delta}" |
/// | Selection | Selection | "Selected: {delta}" |
/// | Selection | No selection | "Unselected" |
///
pub fn delta_for_a11y(
&self,
selection_before: Range<ByteOffset>,
was_selecting: bool,
ctx: &mut ModelContext<Self>,
) -> AccessibilityContent {
let selection_after = self.first_selection(ctx).to_byte_offset(self.buffer(ctx));
let is_selecting = !self.first_selection(ctx).is_cursor_only(self.buffer(ctx));
let text = self.buffer(ctx).text();
// Note: is_empty on a range means that start >= end
// We use it to verify whether the direction of the selection has changed
let (before_point, after_point) =
if selection_before.is_empty() == selection_after.is_empty() {
(selection_before.start, selection_after.start)
} else {
// we changed direction when selecting
(selection_before.end, selection_after.start)
};
let (start, end) = if before_point < after_point {
(before_point, after_point)
} else {
(after_point, before_point)
};
let delta = &text[start.as_usize()..end.as_usize()];
match (was_selecting, is_selecting) {
(false, false) => {
AccessibilityContent::new_without_help(delta, WarpA11yRole::UserAction)
}
(_, true) => {
// Note that Range is start <= x < end, and in our case, when deciding what was the action
// (selecting or unselecting) we need to take end element into consideration also,
// hence the helper `inclusive_contains` method.
fn inclusive_contains(selection: &Range<ByteOffset>, element: ByteOffset) -> bool {
selection.contains(&element) || selection.end == element
}
// Since the range here may show as empty, we're swapping it, so that it's (n, m), where
// n<m
let selection_after = if selection_after.is_empty() {
selection_after.end..selection_after.start
} else {
selection_after
};
let action = if inclusive_contains(&selection_after, start)
&& inclusive_contains(&selection_after, end)
{
"selected"
} else {
"unselected"
};
AccessibilityContent::new(delta, format!(", {action}"), WarpA11yRole::UserAction)
}
(true, false) => {
AccessibilityContent::new_without_help("Unselected", WarpA11yRole::UserAction)
}
}
}
pub fn new(
base_text: String,
tab_size: usize,
max_buffer_len: Option<usize>,
valid_input_type: ValidInputType,
ctx: &mut ModelContext<Self>,
) -> Self {
let regular_buffer = ctx.add_model(|_| Buffer::new(base_text.clone()));
let regular_display_map =
ctx.add_model(|ctx| DisplayMap::new(regular_buffer.clone(), tab_size, ctx));
ctx.subscribe_to_model(&regular_buffer, Self::handle_buffer_event);
ctx.subscribe_to_model(&regular_display_map, Self::handle_display_map_event);
let regular = (regular_buffer, regular_display_map);
Self {
interaction_state: InteractionState::Editable,
buffer_and_display_map: BufferAndDisplayMaps {
regular,
ephemeral: None,
},
vim_visual_tails: vec![],
consecutive_autocomplete_insertion_edits_counter: 0,
max_buffer_len,
valid_input_type,
last_buffer_text: base_text,
}
}
pub fn replica_id<C: ModelAsRef>(&self, ctx: &C) -> ReplicaId {
self.collaborative_buffer().as_ref(ctx).replica_id()
}
pub fn registered_peers<C: ModelAsRef>(&self, ctx: &C) -> HashMap<ReplicaId, Peer> {
self.buffer(ctx).registered_peers()
}
pub fn register_remote_peer(
&mut self,
replica_id: ReplicaId,
selection_data: PeerSelectionData,
ctx: &mut ModelContext<Self>,
) {
self.collaborative_buffer().update(ctx, |buffer, _ctx| {
buffer.register_peer(replica_id, selection_data);
});
}
pub fn unregister_all_remote_peers(&mut self, ctx: &mut ModelContext<Self>) {
self.collaborative_buffer().update(ctx, |buffer, _ctx| {
buffer.unregister_all_peers();
});
}
pub fn unregister_remote_peer(&mut self, replica_id: &ReplicaId, ctx: &mut ModelContext<Self>) {
self.collaborative_buffer().update(ctx, |buffer, _ctx| {
buffer.unregister_peer(replica_id);
});
}
pub fn set_remote_peer_selection_data(
&mut self,
replica_id: &ReplicaId,
selection_data: PeerSelectionData,
ctx: &mut ModelContext<Self>,
) {
self.collaborative_buffer().update(ctx, |buffer, _ctx| {
buffer.set_peer_selection_data(replica_id, selection_data);
});
}
pub fn recreate_buffer(&mut self, replica_id: Option<ReplicaId>, ctx: &mut ModelContext<Self>) {
let replica_id = replica_id.unwrap_or(self.replica_id(ctx));
let tab_size = self.buffer_and_display_map.regular.1.as_ref(ctx).tab_size();
self.collaborative_buffer().update(ctx, |buffer, _| {
buffer.recreate(replica_id, "");
});
self.collaborative_display_map().update(ctx, |map, ctx| {
map.recreate(tab_size, ctx);
});
self.buffer_and_display_map.deactivate_ephemeral_state();
}
fn refresh_batch_version(&mut self, ctx: &mut ModelContext<Self>) {
self.buffer_handle().update(ctx, |buffer, _| {
buffer.refresh_version_on_edits_and_selection_changes_batch()
});
}
fn start_batch<U>(&mut self, update: Option<&UpdateBuffer<U>>, ctx: &mut ModelContext<Self>)
where
U: FnOnce(&mut EditorModel, &mut ModelContext<EditorModel>),
{
if let Some(update) = update {
if !self.is_ephemeral() {
self.last_buffer_text = self.buffer_text(ctx);
}
self.buffer_handle().update(ctx, |buffer, _| {
buffer.start_edits_and_selection_changes_batch(
update.edit_origin,
update.action,
update.skip_undo(),
);
});
} else {
self.buffer_handle().update(ctx, |buffer, _| {
buffer.start_selection_changes_only_batch();
});
}
}
fn end_batch(&mut self, ctx: &mut ModelContext<Self>) {
self.buffer_handle().update(ctx, |buffer, ctx| {
buffer.end_batch(ctx);
});
}
pub fn change_selections(
&mut self,
new_selections: impl Into<LocalSelections>,
ctx: &mut ModelContext<Self>,
) {
self.buffer_handle().update(ctx, |buffer, _| {
if let Err(e) = buffer.change_selections(new_selections.into()) {
log::warn!("Failed to change selections: {e}");
}
});
}
/// The main API to update the underlying buffer. View APIs
/// should never attempt to update the buffer state without going
/// through this API. There are guards in place to ensure we are using
/// this API when updating the buffer in a controlled fashion.
///
/// Selection changes are only applied if the editor is in a selectable state.
/// Buffer changes are only applied if the editor is in an editable state.
pub fn edit<F, G, H, M>(&mut self, ctx: &mut ModelContext<Self>, edit: Edits<F, G, H, M>)
where
F: FnOnce(&mut Self, &mut ModelContext<Self>),
G: FnOnce(&Self, &mut ModelContext<Self>),
H: FnOnce(&mut Self, &mut ModelContext<Self>),
M: FnOnce(&mut Self, &mut ModelContext<Self>),
{
let can_select = self.can_select();
let can_edit = self.can_edit();
let first_cursor_before = self.first_selection(ctx).to_byte_offset(self.buffer(ctx));
let was_selecting = !self.first_selection(ctx).is_cursor_only(self.buffer(ctx));
let is_pure_selection_change = can_select && edit.is_pure_selection_change();
// If this is an ephemeral edit, then snapshot the current buffer
// so that the ephemeral edit is applied on the latest buffer state
// (whether that's the real buffer or the last ephemeral buffer).
// This needs to be done _before_ we start the batch below so that
// 1) we take the snapshot of the correct (regular vs. ephemeral) buffer
// 2) we star the the batch on the correct (regular vs. ephemeral) buffer
let restore_from_snapshot = if can_edit && edit.is_ephemeral() {
let snapshot = self.as_snapshot(ctx);
self.buffer_and_display_map
.activate_new_ephemeral_state(ctx);
Some(snapshot)
} else if can_edit && self.is_ephemeral() && edit.update_buffer.is_some() {
// We're materializing an ephemeral edit, so snapshot the ephemeral buffer
// so that we can apply it to the regular buffer.
let snapshot = self.as_snapshot(ctx);
self.buffer_and_display_map.deactivate_ephemeral_state();
Some(snapshot)
} else {
None
};
// Start the batch after setting the ephemeral state so that
// the batch is started on the right buffer.
// Note: now that we've started a batch, it must be ended.
// We cannot return early in this function.
self.start_batch(edit.update_buffer.as_ref(), ctx);
// If we need to restore between regular and ephemeral states,
// do it now so that any selections / edits beyond are applied to the
// right buffer state. For example, if we tried to select
// without having restored the snapshot, we would be selecting
// on the wrong underlying buffer.
if let Some(snapshot) = restore_from_snapshot {
self.restore_from_snapshot(snapshot, ctx);
// Refresh batch version here as we already recorded edits for the snapshot
// restoration.
self.refresh_batch_version(ctx);
// Do a custom undo / redo record without exhausting the batched edits.
// This should be fine because there shouldn't have been any other edits so far.
self.buffer_handle().update(ctx, |buffer, ctx| {
buffer.record_edits(PlainTextEditorViewAction::ReplaceBuffer, ctx);
});
}
if can_select {
if let Some(change_selection) = edit.change_selections_callback {
change_selection(self, ctx);
}
}
if let Some(before_buffer_edit) = edit.before_buffer_edit_callback {
before_buffer_edit(self, ctx);
}
let prev_state = self.consecutive_autocomplete_insertion_edits_counter;
if can_edit {
if let Some(update_buffer) = edit.update_buffer {
(update_buffer.callback)(self, ctx);
}
}
let next_state = self.consecutive_autocomplete_insertion_edits_counter;
if can_select {
if let Some(post_buffer_edit_change_selection) =
edit.post_buffer_edit_change_selections_callback
{
post_buffer_edit_change_selection(self, ctx);
}
}
// Only emit a11y content for pure selection changes because edits
// have their own a11y content already.
// TODO: technically, a pure selection change can also happen via the `update_buffer` API.
// We might want to actually do this similar to how we emit selection changes (i.e. in `Buffer::end_batch`).
if is_pure_selection_change {
let a11y_content = self.delta_for_a11y(first_cursor_before, was_selecting, ctx);
ctx.emit_a11y_content(a11y_content);
}
// Check if the last edit inserted a new autocomplete symbol.
if prev_state == next_state {
self.consecutive_autocomplete_insertion_edits_counter = 0;
}
self.end_batch(ctx);
}
pub fn apply_remote_operations(
&mut self,
operations: Vec<CrdtOperation>,
ctx: &mut ModelContext<Self>,
) {
// The ephemeral buffer isn't collaborative so these operations are meant for the main buffer.
self.buffer_and_display_map
.regular
.0
.update(ctx, |buffer, ctx| {
if let Err(e) = buffer.apply_ops(operations, ctx) {
log::warn!("Failed to apply remote edits to buffer: {e}");
}
})
}
pub fn interaction_state(&self) -> InteractionState {
self.interaction_state
}
pub fn set_interaction_state(&mut self, interaction_state: InteractionState) {
self.interaction_state = interaction_state;
}
pub fn can_edit(&self) -> bool {
matches!(self.interaction_state, InteractionState::Editable)
}
pub fn can_select(&self) -> bool {
matches!(
self.interaction_state,
InteractionState::Selectable
| InteractionState::Editable
| InteractionState::EditableWithInvalidSelection
)
}
pub fn vim_visual_tails(&self) -> &Vec<Anchor> {
&self.vim_visual_tails
}
pub fn fold(&mut self, ctx: &mut ModelContext<Self>) {
if self.can_edit() {
let mut fold_ranges = Vec::new();
let map = self.display_map(ctx);
for selection in self.selections(ctx).iter() {
let (start, end) = selection.display_range(map, ctx).sorted();
let buffer_start_row = start.to_buffer_point(map, Bias::Left, ctx).unwrap().row;
for row in (0..=end.row()).rev() {
if self.is_line_foldable(row, ctx) && !map.is_line_folded(row) {
let fold_range = self.foldable_range_for_line(row, ctx).unwrap();
if fold_range.end.row >= buffer_start_row {
fold_ranges.push(fold_range);
if row <= start.row() {
break;
}
}
}
}
}
if !fold_ranges.is_empty() {
self.display_map_handle().update(ctx, |map, ctx| {
map.fold(fold_ranges, ctx).unwrap();
});
}
}
}
pub fn fold_selected_ranges(&mut self, ctx: &mut ModelContext<Self>) {
if self.can_edit() {
let buffer = self.buffer(ctx);
let ranges = self
.selections(ctx)
.iter()
.map(|s| s.range(buffer))
.collect::<Vec<_>>();
self.display_map_handle().update(ctx, |map, ctx| {
map.fold(ranges, ctx).unwrap();
});
}
}
pub fn unfold(&mut self, ctx: &mut ModelContext<Self>) {
if self.can_edit() {
let map = self.display_map(ctx);
let buffer = self.buffer(ctx);
let ranges = self
.selections(ctx)
.iter()
.map(|s| {
let (start, end) = s.display_range(map, ctx).sorted();
let mut start = start.to_buffer_point(map, Bias::Left, ctx).unwrap();
let mut end = end.to_buffer_point(map, Bias::Left, ctx).unwrap();
start.column = 0;
end.column = buffer.line_len(end.row).unwrap();
start..end
})
.collect::<Vec<_>>();
self.display_map_handle().update(ctx, |map, ctx| {
map.unfold(ranges, ctx).unwrap();
});
}
}
pub fn undo(&mut self, ctx: &mut ModelContext<Self>) {
if !self.can_edit() {
return;
}
if self.is_ephemeral() {
self.buffer_and_display_map.deactivate_ephemeral_state();
ctx.emit(EditorModelEvent::Edited {
edit_origin: EditOrigin::UserInitiated,
});
return;
}
self.last_buffer_text = self.buffer_text(ctx);
self.buffer_handle().update(ctx, |buffer, ctx| {
buffer.undo(ctx);
ctx.notify();
});
}
pub fn redo(&mut self, ctx: &mut ModelContext<Self>) {
if !self.can_edit() {
return;
}
self.last_buffer_text = self.buffer_text(ctx);
self.buffer_handle().update(ctx, |buffer, ctx| {
buffer.redo(ctx);
ctx.notify();
});
}
pub fn reset_undo_redo_stack(&mut self, ctx: &mut ModelContext<Self>) {
self.buffer_handle().update(ctx, |buffer, _ctx| {
buffer.reset_undo_stack();
})
}
pub fn consecutive_autocomplete_insertion_edits_counter(&self) -> usize {
self.consecutive_autocomplete_insertion_edits_counter
}
pub fn as_snapshot<C: ModelAsRef>(&self, ctx: &C) -> EditorSnapshot {
self.buffer(ctx).snapshot()
}
pub fn buffer_edit<I, S, T>(
&mut self,
old_ranges: I,
new_text: T,
ctx: &mut ModelContext<Self>,
) -> anyhow::Result<()>
where
I: IntoIterator<Item = Range<S>>,
S: ToCharOffset,
T: Into<Text>,
{
self.buffer_handle().update(ctx, |buffer, ctx| {
buffer.edit(old_ranges, new_text, ctx).map(|_| ())
})
}
pub fn selected_text<C: ModelAsRef>(&self, ctx: &C) -> String {
let buffer = self.buffer(ctx);
let last_ix = self.selections(ctx).len() - 1;
self.selections(ctx)
.iter()
.enumerate()
.flat_map(|(ix, selection)| {
let start = selection.start().to_char_offset(buffer).unwrap();
let end = selection.end().to_char_offset(buffer).unwrap();
buffer
.chars_at(start)
.unwrap()
.take(end.as_usize() - start.as_usize())
.chain(if ix < last_ix { Some('\n') } else { None })
})
.collect()
}
pub fn selected_text_strings(&self, ctx: &AppContext) -> Vec<String> {
let buffer = self.buffer(ctx);
self.selections(ctx)
.iter()
.map(|selection| {
let start = selection
.start()
.to_char_offset(buffer)
.expect("Start character in selection buffer should exist");
let end = selection
.end()
.to_char_offset(buffer)
.expect("End character in selection buffer should exist");
buffer
.chars_at(start)
.expect("Buffer should contain character at start index of selection")
.take(end.as_usize().saturating_sub(start.as_usize()))
.collect()
})
.collect()
}
pub fn indent(&mut self, ctx: &mut ModelContext<Self>) {
let start = self.first_selection(ctx).start().to_point(self.buffer(ctx));
let end = self.first_selection(ctx).end().to_point(self.buffer(ctx));
if let Some((start, end)) = start.ok().zip(end.ok()) {
self.buffer_handle().update(ctx, |buffer, ctx| {
if let Err(error) = buffer.indent(start.row..end.row + 1, ctx) {
log::error!("error indenting text: {error}");
}
});
}
}
pub fn unindent(&mut self, ctx: &mut ModelContext<Self>) {
self.buffer_handle().update(ctx, |buffer, ctx| {
// Unlike indent, unindent is applied always: to multiple selections and even single characters/words.
let row_ranges = buffer
.local_selections()
.selections
.iter()
.map(|selection| {
selection.start().to_point(buffer).unwrap().row
..selection.end().to_point(buffer).unwrap().row + 1
})
.dedup()
.collect::<Vec<_>>();
if let Err(error) = buffer.unindent(row_ranges, ctx) {
log::error!("error unindenting text: {error}");
};
});
}
pub fn insert(
&mut self,
text: &str,
text_style: Option<TextStyle>,
ctx: &mut ModelContext<Self>,
) {
self.insert_internal(text, text_style, SelectionInsertion::No, ctx);
}
pub fn insert_selected_text(&mut self, text: &str, ctx: &mut ModelContext<Self>) {
self.insert_internal(text, None, SelectionInsertion::Yes, ctx);
}
pub fn clear_all_styles(&self, ctx: &mut ModelContext<Self>) {
let len = self.buffer_text(ctx).len();
let full_range = ByteOffset::from(0)..ByteOffset::from(len);
self.update_buffer_styles([full_range], TextStyleOperation::clear_all(), ctx);
}
pub fn update_buffer_styles<I, S>(
&self,
old_ranges: I,
text_style_operation: TextStyleOperation,
ctx: &mut ModelContext<Self>,
) where
I: IntoIterator<Item = Range<S>>,
S: ToCharOffset,
{
self.buffer_handle().update(ctx, |buffer, ctx| {
if let Err(e) = buffer.update_styles(old_ranges, text_style_operation, ctx) {
log::error!("Error with updating styles: {e:?}")
}
});
}
/// Autocomplete user inserted text under the following conditions:
/// 1) The symbol has a corresponding closing symbol (e.g. ( -> ), " -> ", etc)
/// 2) All of the current cursors are not immediately next to any alphanumeric character
///
// If there are only selections (no single cursor), autocomplete by wrapping the selection with start and end symbol.
pub fn insert_and_maybe_autocomplete_symbols(
&mut self,
text: &str,
ctx: &mut ModelContext<Self>,
) {
if CLOSING_SYMBOLS.contains(&text)
&& text.chars().count() == 1
&& self.all_cursors_next_character_matches_char(
text.chars()
.next()
.expect("Autocompleted symobl should have at least one character"),
ctx,
)
{
let map = self.display_map(ctx);
// Moves cursor to the right.
let mut new_selections = self.selections(ctx).clone();
for selection in new_selections.iter_mut() {
let end = selection.end().to_display_point(map, ctx).unwrap();
let cursor = map
.anchor_before(
movement::right(map, end, ctx, false)
.expect("moving right should return a valid DisplayPoint"),
Bias::Right,
ctx,
)
.expect("DisplayPoint should convert to an Anchor");
selection.set_start(cursor.clone());
selection.set_end(cursor);
selection.set_reversed(false);
selection.goal_start_column = None;
selection.goal_end_column = None;
}
self.change_selections(new_selections, ctx);
return;
}
// Autocomplete for symbols like double quotes and brackets.
if let Some(completion) = AUTOCOMPLETE_SYMBOLS.get(text) {
if self.no_cursor_only_selections(ctx) {
self.insert_characters_wrap_around_selection(text, completion, ctx);
return;
}
let buffer = self.buffer(ctx);
// Check whether all of the current cursors are not immediately next to any alphanumeric characters.
let not_alphanumeric = self.selections(ctx).iter().all(|selection| {
let position = selection
.start()
.to_point(buffer)
.expect("Start of selection should exist");
let right_not_alphanumeric = buffer
.chars_at(position)
.unwrap()
.next()
.map(|right_char| !right_char.is_alphanumeric())
.unwrap_or(true);
let left_not_alphanumeric = buffer
.chars_at(position)
.unwrap()
.rev()
.next()
.map(|left_char| !left_char.is_alphanumeric())
.unwrap_or(true);
right_not_alphanumeric && left_not_alphanumeric
});
// Always autocomplete for brackets and parentheses. For quotes, only autocomplete
// if both sides of the cursor are not alphabetic or numeric characters.
if (text != "\"" && text != "\'") || not_alphanumeric {
self.insert_internal(text, None, SelectionInsertion::No, ctx);
let offset_ranges = self.selection_to_char_offset_ranges(ctx);
// Complete the closing symbol without moving the cursor.
self.buffer_handle().update(ctx, |buffer, ctx| {
if let Err(error) = buffer.edit(offset_ranges.iter().cloned(), *completion, ctx)
{
log::error!("error inserting text: {error}");
};
});
self.consecutive_autocomplete_insertion_edits_counter += 1;
return;
}
}
self.insert_internal(text, None, SelectionInsertion::No, ctx);
}
pub fn insert_internal(
&mut self,
text: &str,
text_style: Option<TextStyle>,
select_insertion: SelectionInsertion,
ctx: &mut ModelContext<Self>,
) {
let offset_ranges = self.selection_to_char_offset_ranges(ctx);
if self.text_insertion_would_exceed_max_buffer_len(text, ctx) {
return;
}
if self.text_is_invalid_input_type(text) {
return;
}
let old_buffer_count = self.buffer_len(ctx);
self.buffer_handle().update(ctx, |buffer, ctx| {
if let Err(error) = buffer.edit(
offset_ranges.iter().cloned(),
Text::new(text, text_style),
ctx,
) {
log::error!("error inserting text: {error}");
};
});
let char_count = text.chars().count() as isize;
let mut delta = 0_isize;
let mut total_deleted = 0_isize;
let buffer = self.buffer(ctx);
let new_selections = offset_ranges.mapped(|range| {
let range_start = range.start.as_usize() as isize;
let range_end = range.end.as_usize() as isize;
let end = buffer
.anchor_before(CharOffset::from(
(range_start + delta + char_count) as usize,
))
.unwrap();
let start = match select_insertion {
SelectionInsertion::Yes => buffer
.anchor_before(CharOffset::from((range_start + delta) as usize))
.unwrap(),
SelectionInsertion::No => end.clone(),
};
let deleted_count = range_end - range_start;
total_deleted += deleted_count;
delta += char_count - deleted_count;
LocalSelection {
selection: Selection {
start,
end,
reversed: false,
},
clamp_direction: Default::default(),
goal_start_column: None,
goal_end_column: None,
}
});
if total_deleted as usize == old_buffer_count.as_usize() {
ctx.emit(EditorModelEvent::BufferReplaced);
}
self.change_selections(new_selections, ctx);
}
pub fn marked_text_state(&self, ctx: &AppContext) -> MarkedTextState {
self.buffer(ctx).marked_text_state()
}
pub fn update_marked_text(
&mut self,
text: &str,
marked_text_style: Option<TextStyle>,
selected_range: &Range<usize>,
ctx: &mut ModelContext<Self>,
) {
let marked_text_state = self.marked_text_state(ctx);
// If there was no marked text before, then we should replace each selection with blank text.
if marked_text_state == MarkedTextState::Inactive {
self.insert_internal("", None, SelectionInsertion::No, ctx);
}
// Insert the marked text in all selections as selected text.
self.insert_internal(text, marked_text_style, SelectionInsertion::Yes, ctx);
self.set_marked_text_state(
MarkedTextState::Active {
selected_range: selected_range.clone(),
},
ctx,
);
}
fn set_marked_text_state(
&mut self,
marked_text_state: MarkedTextState,
ctx: &mut ModelContext<Self>,
) {
self.buffer_handle().update(ctx, |buffer, _ctx| {
buffer.set_marked_text_state(marked_text_state);
});
}
pub fn commit_incomplete_marked_text(&mut self, ctx: &mut ModelContext<Self>) {
// We assume that all "selections" are actually the composed marked text.
let selected_strings = self.selected_text_strings(ctx);
let incomplete_marked_text = selected_strings.first().map(|s| s.as_str()).unwrap_or("");
self.clear_marked_text_and_commit(incomplete_marked_text, ctx);
}
pub fn clear_marked_text_and_commit(&mut self, text: &str, ctx: &mut ModelContext<Self>) {
self.insert_internal(text, None, SelectionInsertion::No, ctx);
self.set_marked_text_state(MarkedTextState::Inactive, ctx);
}
pub fn clear_marked_text(&mut self, ctx: &mut ModelContext<Self>) {
if self.marked_text_state(ctx) == MarkedTextState::Inactive {
return;
}
self.edit(
ctx,
Edits::new().with_update_buffer_options(
PlainTextEditorViewAction::UpdateMarkedText,
EditOrigin::UserTyped,
UpdateBufferOption::None,
|editor_model, ctx| {
editor_model.insert_internal("", None, SelectionInsertion::No, ctx);
editor_model.set_marked_text_state(MarkedTextState::Inactive, ctx);
},
),
);
}
// Used for restoring the buffer from a snapshot.
fn restore_from_snapshot(&mut self, snapshot: EditorSnapshot, ctx: &mut ModelContext<Self>) {
let version = self.buffer_handle().as_ref(ctx).versions();
self.clear_selections(ctx);
self.clear_buffer(ctx);
let mut text_added_offset = CharOffset::from(0);
self.buffer_handle().update(ctx, |buffer, ctx| {
for styled_text in snapshot.buffer_text_runs() {
if let Err(error) = buffer.edit(
[(text_added_offset..text_added_offset)],
Text::new(styled_text.text(), Some(styled_text.text_style())),
ctx,
) {
log::error!("error inserting text: {error}");
};
text_added_offset += styled_text.text().chars().count();
}
});
let edits = self.buffer(ctx).edits_since(version).collect::<Vec<_>>();
self.display_map_handle()
.update(ctx, move |display_map, ctx| {
display_map.apply_edits(&edits, ctx).unwrap();
});
let buffer = self.buffer(ctx);
let restored_selection = snapshot
.selections()
.iter()
.filter_map(|range| {
// Handle the error to convert saved selection offsets to editor anchors gracefully
// as the restored buffer state might not match exactly to the saved snapshot.
let Some(end) = buffer.anchor_before(range.end).ok() else {
log::error!(
"error restoring snapshot with selection end {} on text with max range {}",
range.end,
text_added_offset
);
return None;
};
let Some(start) = buffer.anchor_before(range.start).ok() else {
log::error!(
"error restoring snapshot with selection start {} on text with max range {}",
range.start,
text_added_offset
);
return None;
};
let mut selection = LocalSelection {
selection: Selection::single_cursor(end),
clamp_direction: Default::default(),
goal_start_column: None,
goal_end_column: None,
};
selection.set_head(buffer, start);
Some(selection)
})
.collect_vec();
let new_selections = if let Ok(restored_selection) = Vec1::try_from_vec(restored_selection)
{
restored_selection
} else {
// Make sure there is at least one root selection if the
// restored selection is empty.
vec1![LocalSelection {
selection: Selection::single_cursor(Anchor::Start),
clamp_direction: Default::default(),
goal_start_column: None,
goal_end_column: None,
}]
};
self.change_selections(new_selections, ctx);
}
pub fn selection_insertion_index(&self, start: &Anchor, app: &AppContext) -> usize {
self.buffer(app)
.local_selections()
.selection_insertion_index(start, self.buffer(app))
}
/// Set all selection ranges back to a single cursor originating at the start anchor.
pub fn deselect(&mut self, ctx: &mut ModelContext<Self>) {
let mut new_selections = self.selections(ctx).clone();
let buffer = self.buffer(ctx);
for selection in new_selections.iter_mut() {
let Ok(anchor) = buffer.anchor_at(selection.start(), AnchorBias::Left) else {
continue;
};
selection.set_selection(Selection::single_cursor(anchor));
selection.goal_start_column = None;
selection.goal_end_column = None;
}
self.change_selections(new_selections, ctx);
}
/// Helper method for moving the cursor within the input box / editor.
/// @param keep_selection - If true, the movement will also select the text spanned by the
/// movement.
/// @param func - Closure which gets buffer & selection and returns the location to which the
/// cursor should be moved.
/// @param ctx
pub fn move_cursor<F, C>(&mut self, keep_selection: bool, func: F, ctx: &mut ModelContext<Self>)
where
C: ToCharOffset,
F: Fn(&Buffer, &mut LocalSelection) -> C,
{
let mut new_selections = self.selections(ctx).clone();
let buffer = self.buffer(ctx);
for selection in new_selections.iter_mut() {
let point = func(buffer, selection);
let cursor = buffer.anchor_before(point).unwrap();
if keep_selection {
selection.set_head(buffer, cursor);
} else {
selection.set_selection(Selection::single_cursor(cursor));
}
selection.goal_start_column = None;
selection.goal_end_column = None;
}
self.change_selections(new_selections, ctx);
}
/// Move each cursor to the column with the left-most non-whitespace character.
pub fn cursor_line_start_non_whitespace(
&mut self,
keep_selection: bool,
ctx: &mut ModelContext<Self>,
) {
self.move_cursor(
keep_selection,
|buffer, selection| {
let start = selection.head().to_point(buffer).unwrap();
let line = buffer.line(start.row).unwrap();
let non_whitespace =
line.chars().position(|c| !c.is_whitespace()).unwrap_or(0) as u32;
Point::new(start.row, non_whitespace)
},
ctx,
);
}
pub fn cursor_line_start(&mut self, keep_selection: bool, ctx: &mut ModelContext<Self>) {
self.move_cursor(
keep_selection,
|buffer, selection| {
let start = selection.head().to_point(buffer).unwrap();
Point::new(start.row, 0)
},
ctx,
);
}
/// Move selection start point to the start of the current line.
fn selection_line_start(&mut self, ctx: &mut ModelContext<Self>) {
let buffer = self.buffer(ctx);
let mut new_selections = self.selections(ctx).clone();
for selection in new_selections.iter_mut() {
let Ok(mut start_point) = selection.start().to_point(buffer) else {
continue;
};
start_point.column = 0;
let Ok(start_anchor) = buffer.anchor_before(start_point) else {
continue;
};
selection.set_start(start_anchor);
}
self.change_selections(new_selections, ctx);
}
#[cfg(test)]
pub fn selection_line_start_test(&mut self, ctx: &mut ModelContext<Self>) {
self.selection_line_start(ctx)
}
/// Move selection end point to the end of the current line.
fn selection_line_end(&mut self, ctx: &mut ModelContext<Self>) {
let buffer = self.buffer(ctx);
let mut new_selections = self.selections(ctx).clone();
for selection in new_selections.iter_mut() {
let Ok(mut end_point) = selection.end().to_point(buffer) else {
continue;
};
let Ok(line_len) = buffer.line_len(end_point.row) else {
continue;
};
end_point.column = line_len;
let Ok(end_anchor) = buffer.anchor_before(end_point) else {
continue;
};
selection.set_end(end_anchor);
}
self.change_selections(new_selections, ctx);
}
pub fn cursor_line_end(&mut self, keep_selection: bool, ctx: &mut ModelContext<Self>) {
self.move_cursor(
keep_selection,
|buffer, selection| {
let end = selection.end().to_point(buffer).unwrap();
Point::new(end.row, buffer.line_len(end.row).unwrap())
},
ctx,
);
}
pub fn move_to_buffer_end(&mut self, keep_selection: bool, ctx: &mut ModelContext<Self>) {
self.move_cursor(keep_selection, |buffer, _| buffer.max_point(), ctx);
}
/// Whichever columns the selection range(s) start and end on their respective rows, extend the
/// range to include whole lines. Set `include_newline` to `true` to add a trailing (preferred)
/// or leading newline.
pub fn extend_selection_linewise(
&mut self,
include_newline: bool,
ctx: &mut ModelContext<Self>,
) {
self.selection_line_start(ctx);
self.selection_line_end(ctx);
if include_newline {
self.include_newline_in_selection(ctx);
}
}
/// Like [`Self::extend_selection_linewise`] except this accounts for the case where visual mode
/// allows the block cursor to be on top of the newline at the end of a line.
fn extend_selection_linewise_visual_mode(
&mut self,
include_newline: bool,
ctx: &mut ModelContext<Self>,
) {
self.selection_line_start(ctx);
// Specialized "line end" logic here
self.move_cursor(
/* keep_selection */ true,
|buffer, selection| {
let end = selection.end().to_point(buffer).unwrap();
// If the selection ends on column 0, that's actually because the block cursor was
// on the newline of the above line. In that case, treat it as already on the line
// end.
if end.column == 0 {
end
} else {
Point::new(end.row, buffer.line_len(end.row).unwrap())
}
},
ctx,
);
if include_newline {
self.include_newline_in_selection(ctx);
}
}
/// This method does Vim's "%" command. This command checks if there is a bracket under the
/// cursor, or to the right of the cursor on the same line. If so, jump to the bracket that
/// matches it.
pub fn vim_move_cursor_to_matching_bracket(
&mut self,
keep_selection: bool,
ctx: &mut ModelContext<Self>,
) {
self.move_cursor(
keep_selection,
|buffer, selection| {
let offset = selection
.head()
.to_char_offset(buffer)
.expect("selection must be a valid CharOffset");
// Create an iterator to determine the starting point for the search. Vim will only
// consider starting points on the current line.
let mut iter = buffer
.chars_at(offset)
.expect("infallible with a valid CharOffset")
.take_while(|c| *c != '\n');
// Start by checking the char under the current cursor position.
let Some(c) = iter.next() else {
return offset;
};
let (bracket, start_offset) = match BracketChar::try_from(c) {
// If the current char is a bracket, match that.
Ok(bracket) => (bracket, offset),
// If not, move forward on this line until we find a bracket, and begin the
// search there.
Err(_) => match iter
.enumerate()
.find_map(|(i, c)| Some((i, BracketChar::try_from(c).ok()?)))
{
None => return offset,
Some((i, bracket)) => (bracket, offset + i + 1),
},
};
vim_find_matching_bracket(buffer, &bracket, start_offset).unwrap_or(offset)
},
ctx,
);
}
/// This method does Vim's `[` command. It moves to the enclosing bracket around the cursor. It
/// is similar to the `%` command, but it does not require the cursor to start on a bracket.
pub fn vim_move_cursor_to_unmatched_bracket(
&mut self,
bracket: &BracketChar,
keep_selection: bool,
ctx: &mut ModelContext<Self>,
) {
self.move_cursor(
keep_selection,
|buffer, selection| {
let offset = selection
.head()
.to_char_offset(buffer)
.expect("selection must be a valid CharOffset");
vim_find_matching_bracket(buffer, bracket, offset).unwrap_or(offset)
},
ctx,
);
}
/// Expand the selection to the operand for a visual mode command.
pub fn vim_visual_selection_range(
&mut self,
motion_type: MotionType,
include_newline: bool,
ctx: &mut ModelContext<Self>,
) {
let buffer = self.buffer(ctx);
let vim_visual_tails = mem::take(&mut self.vim_visual_tails);
let new_selections = self
.selections(ctx)
.iter()
.zip(vim_visual_tails.iter())
.filter_map(|(selection, visual_tail)| {
let mut end = selection.head().to_char_offset(buffer).ok()?;
let mut start = visual_tail.to_char_offset(buffer).ok()?;
if start > end {
mem::swap(&mut start, &mut end);
}
let max_offset = buffer.max_point().to_char_offset(buffer).ok()?;
// Visual mode includes the char under the block cursor for operators.
// For linewise, only include +1 if it won't move onto the next line (i.e., the
// current char at `end` is not a newline).
if end < max_offset
&& (motion_type != MotionType::Linewise
|| buffer
.chars_at(end)
.is_ok_and(|mut it| it.next().unwrap_or_default() != '\n'))
{
end += 1;
}
Some(start..end)
})
.collect_vec();
let _ = self.select_ranges_by_offset(new_selections, ctx);
if motion_type == MotionType::Linewise {
self.extend_selection_linewise_visual_mode(include_newline, ctx);
}
}
/// Implements moving left/right using buffer offsets instead of the DisplayMap.
///
/// This is necessary for two reasons:
///
/// 1. The `DisplayMap` doesn't get updated until the current event handler
/// has finished running.
/// This means we can't call any function that relies on the `DisplayMap`
/// in an event handler if the buffer has been edited because it will be
/// out of date.
///
/// 2. Functions that move the cursor, such as move_up and move_left, rely
/// on the `DisplayMap` to translate buffer offsets into `DisplayPoints`.
/// This means those movement functions don't work in unit tests,
/// unless the window is forcibly painted first.
///
/// Why stop at a line boundary? In Vim mode, moving sideways using h/l
/// and the left/right arrow keys only navigates within a line, instead
/// of wrapping around to the line above or below.
pub fn move_cursors_by_offset(
&mut self,
char_count: u32,
direction: &Direction,
keep_selection: bool,
stop_at_line_boundary: bool,
ctx: &mut ModelContext<Self>,
) {
self.move_cursor(
keep_selection,
|buffer, selection| {
let head = selection
.head()
.to_char_offset(buffer)
.expect("Selection head must be valid CharOffset");
let offset_change = if stop_at_line_boundary {
let head_point = head
.to_point(buffer)
.expect("Selection head must be valid Point");
match direction {
// When moving left, there are <current column> characters
// between the current position and the start of the line.
// Respect the line boundary by moving at most that number
// of characters.
Direction::Backward => u32::min(head_point.column, char_count),
Direction::Forward => {
let line_len = buffer
.line_len(head_point.row)
.expect("Selection head row should have a length");
// When moving right, there are (line_len - <current column>)
// characters between the current position and the end of the
// line. Respect the line boundary by moving at most that
// number of characters.
u32::min(line_len.saturating_sub(head_point.column), char_count)
}
}
} else {
char_count
};
match direction {
Direction::Backward => head.saturating_sub(&(offset_change as usize).into()),
Direction::Forward => {
let max_offset = buffer
.max_point()
.to_char_offset(buffer)
.expect("Buffer::max_point must be valid CharOffset");
cmp::min(max_offset, head + offset_change as usize)
}
}
},
ctx,
);
}
/// Implements moving left/right using buffer offsets, skipping past newlines.
/// See `move_cursors_by_offset` for an explanation of why we use buffer offsets
/// instead of the DisplayMap.
///
/// This behavior is used by space/backspace navigation in Vim mode.
pub fn move_cursor_ignoring_newlines(
&mut self,
char_count: u32,
direction: &Direction,
keep_selection: bool,
ctx: &mut ModelContext<Self>,
) {
self.move_cursor(
keep_selection,
|buffer, selection| {
let head = selection
.head()
.to_char_offset(buffer)
.expect("Selection head must be valid CharOffset");
match direction {
Direction::Backward => {
let offset_change = buffer
.chars_rev_at(head)
.expect("Buffer must have characters at the current head.")
.enumerate()
.fold_while(0, |chars_so_far, (rev_index, c)| {
if chars_so_far < char_count {
if c == '\n' {
Continue(chars_so_far)
} else {
Continue(chars_so_far + 1)
}
} else {
Done(rev_index as u32)
}
})
.into_inner();
head.saturating_sub(&(offset_change as usize).into())
}
Direction::Forward => {
let offset_change = buffer
.chars_at(head)
.expect("Buffer must have characters at the current selection head.")
.enumerate()
.fold_while(0, |chars_so_far, (index, c)| {
if chars_so_far < char_count {
if c == '\n' {
Continue(chars_so_far)
} else {
Continue(chars_so_far + 1)
}
} else {
Done(index as u32)
}
})
.into_inner();
let max_offset = buffer
.max_point()
.to_char_offset(buffer)
.expect("Buffer::max_point must be valid CharOffset");
cmp::min(max_offset, head + offset_change as usize)
}
}
},
ctx,
);
}
/// Implements moving up using buffer offsets instead of the DisplayMap.
///
/// This is necessary for two reasons:
///
/// 1. The `DisplayMap` doesn't get updated until the current event handler
/// has finished running.
/// This means we can't call any function that relies on the `DisplayMap`
/// in an event handler if the buffer has been edited because it will be
/// out of date.
///
/// 2. Functions that move the cursor, such as move_up and move_left, rely
/// on the `DisplayMap` to translate buffer offsets into `DisplayPoints`.
/// This means those movement functions don't work in unit tests,
/// unless the window is forcibly painted first.
pub fn move_up_by_offset(&mut self, count: u32, ctx: &mut ModelContext<Self>) {
let buffer = self.buffer(ctx);
let mut new_selections = self.selections(ctx).clone();
for selection in new_selections.iter_mut() {
let mut point = selection
.head()
.to_point(buffer)
.expect("Selection head must be a valid Point");
point.row = point.row.saturating_sub(count);
let goal_column = match selection.goal_end_column {
Some(goal_column) => cmp::max(goal_column, point.column),
None => point.column,
};
point.column = u32::min(
goal_column,
buffer.line_len(point.row).unwrap_or(point.column),
);
let Ok(cursor) = buffer.anchor_at(point, AnchorBias::Left) else {
continue;
};
selection.set_selection(Selection::single_cursor(cursor));
selection.goal_start_column = Some(goal_column);
selection.goal_end_column = Some(goal_column);
}
self.change_selections(new_selections, ctx);
}
/// Implements moving down using buffer offsets instead of the DisplayMap.
///
/// This is necessary for two reasons:
///
/// 1. The `DisplayMap` doesn't get updated until the current event handler
/// has finished running.
/// This means we can't call any function that relies on the `DisplayMap`
/// in an event handler if the buffer has been edited because it will be
/// out of date.
///
/// 2. Functions that move the cursor, such as move_up and move_left, rely
/// on the `DisplayMap` to translate buffer offsets into `DisplayPoints`.
/// This means those movement functions don't work in unit tests,
/// unless the window is forcibly painted first.
pub fn move_down_by_offset(&mut self, count: u32, ctx: &mut ModelContext<Self>) {
let buffer = self.buffer(ctx);
let max_point = buffer.max_point();
let mut new_selections = self.selections(ctx).clone();
for selection in new_selections.iter_mut() {
let mut point = selection
.head()
.to_point(buffer)
.expect("Selection head must be a valid Point");
point.row = cmp::min(point.row + count, max_point.row);
let goal_column = match selection.goal_end_column {
Some(goal_column) => cmp::max(goal_column, point.column),
None => point.column,
};
point.column = cmp::min(
goal_column,
buffer.line_len(point.row).unwrap_or(point.column),
);
let Ok(cursor) = buffer.anchor_at(point, AnchorBias::Left) else {
continue;
};
selection.set_selection(Selection::single_cursor(cursor));
selection.goal_start_column = Some(goal_column);
selection.goal_end_column = Some(goal_column);
}
self.change_selections(new_selections, ctx);
}
/// See if the character `c` exists on the line of the cursor(s) in the specified direction. If
/// it does, move the cursor there.
pub fn vim_find_char(
&mut self,
keep_selection: bool,
occurrence_count: u32,
motion: &FindCharMotion,
ctx: &mut ModelContext<Self>,
) {
self.move_cursor(
keep_selection,
|buffer, selection| {
let point = selection
.head()
.to_point(buffer)
.expect("selection head must be a valid Point");
let Ok(line) = buffer.line(point.row) else {
return point;
};
let column = point.column as usize;
if let Some(new_column) =
vim_find_char_on_line(&line, column, motion, occurrence_count, keep_selection)
{
Point::new(point.row, new_column as u32)
} else {
point
}
},
ctx,
);
}
/// Toggle case for the selected region.
/// This function currently only supports a single selection.
pub fn toggle_selection_case(&mut self, ctx: &mut ModelContext<Self>) {
let buffer = self.buffer(ctx);
// Currently, this only supports a single selection.
let selection = self.first_selection(ctx);
let selection_len = {
let offsets = selection.to_offset(buffer);
offsets
.start
.as_usize()
.saturating_sub(offsets.end.as_usize())
};
let new_chars = buffer
.chars_at(selection.start().to_owned())
.expect("buffer should have characters at the Selection start")
.take(selection_len)
.map(|char_at_cursor| match char_at_cursor {
c if c.is_lowercase() => c.to_uppercase().next().unwrap_or(c),
c if c.is_uppercase() => c.to_lowercase().next().unwrap_or(c),
c => c,
})
.collect::<String>();
self.insert_internal(new_chars.as_str(), None, SelectionInsertion::No, ctx);
}
/// Uppercase all characters in the selected region.
/// This function currently only supports a single selection.
pub fn selection_to_uppercase(&mut self, ctx: &mut ModelContext<Self>) {
let buffer = self.buffer(ctx);
// Currently, this only supports a single selection.
let selection = self.first_selection(ctx);
let uppercased = buffer
.text_for_range(selection.range(buffer))
.expect("buffer should have text in the selection")
.to_uppercase();
self.insert_internal(uppercased.as_str(), None, SelectionInsertion::No, ctx);
}
/// Lowercase all characters in the selected region.
/// This function currently only supports a single selection.
pub fn selection_to_lowercase(&mut self, ctx: &mut ModelContext<Self>) {
let buffer = self.buffer(ctx);
// Currently, this only supports a single selection.
let selection = self.first_selection(ctx);
let lowercased = buffer
.text_for_range(selection.range(buffer))
.expect("buffer should have text in the selection")
.to_lowercase();
self.insert_internal(lowercased.as_str(), None, SelectionInsertion::No, ctx);
}
/// Clear any selected regions, leaving the cursor at the end of the first selection
pub fn clear_selections(&mut self, ctx: &mut ModelContext<Self>) {
if !self.selections(ctx).is_empty() {
let buffer = self.buffer(ctx);
let mut first_selection = self.first_selection(ctx).clone();
first_selection.set_head(buffer, first_selection.tail().clone());
self.change_selections(vec1![first_selection], ctx);
}
}
pub fn clear_buffer(&mut self, ctx: &mut ModelContext<Self>) {
self.buffer_handle().update(ctx, |buffer, ctx| {
if let Err(error) = buffer.edit(Some(0.into()..buffer.len()), "", ctx) {
log::error!("error clearing text: {error}");
};
});
self.clear_selections(ctx);
}
/// Replaces the first n characters in the buffer with the given text, while
/// keeping any existing selections untouched (but consolidating in case
/// the existing selections are no longer valid after the replacement).
pub fn replace_first_n_characters(
&mut self,
n: CharOffset,
text: &str,
ctx: &mut ModelContext<Self>,
) {
self.buffer_handle().update(ctx, |buffer, ctx| {
if let Err(error) = buffer.edit(Some(0.into()..n), text, ctx) {
log::error!("error replacing first n chars: {error}");
};
});
}
/// Replaces the last `n` characters in the buffer with the given text, while
/// keeping any existing selections untouched (but consolidating in case
/// the existing selections are no longer valid after the replacement).
pub fn replace_last_n_characters(
&mut self,
n: CharOffset,
text: &str,
ctx: &mut ModelContext<Self>,
) {
self.buffer_handle().update(ctx, |buffer, ctx| {
let len = buffer.len();
let start = len.saturating_sub(&n);
if let Err(error) = buffer.edit(Some(start..len), text, ctx) {
log::error!("error replacing last n chars: {error}");
};
});
}
pub fn reset_selections_to_point(&mut self, point: &Point, ctx: &mut ModelContext<Self>) {
let buffer = self.buffer(ctx);
let cursor = buffer
.anchor_before(point.to_char_offset(buffer).unwrap())
.unwrap();
let mut first_selection = self.first_selection(ctx).clone();
first_selection.set_selection(Selection::single_cursor(cursor));
self.change_selections(vec1![first_selection], ctx);
}
/// Selects ranges by `CharOffset`s. Note if the selections specified in `ranges` are empty, the
/// selections are not set since there must be at least one selection.
pub fn select_ranges_by_offset<T, G>(
&mut self,
ranges: T,
ctx: &mut ModelContext<Self>,
) -> anyhow::Result<()>
where
T: IntoIterator<Item = Range<G>>,
G: ToCharOffset,
{
let buffer = self.buffer(ctx);
let new_selections: Vec<_> = ranges
.into_iter()
.flat_map(|range| {
let start = range.start.to_char_offset(buffer).ok()?;
let end = range.end.to_char_offset(buffer).ok()?;
Some(LocalSelection {
selection: Selection {
start: buffer.anchor_after(start).ok()?,
end: buffer.anchor_before(end).ok()?,
reversed: false,
},
clamp_direction: Default::default(),
goal_start_column: None,
goal_end_column: None,
})
})
.sorted_by(|a, b| {
a.start()
.cmp(b.start(), buffer)
.expect("anchors should be comparable")
})
.collect();
// Only set the selections if the new selections are not empty.
if let Ok(new_selections) = Vec1::try_from_vec(new_selections) {
self.change_selections(new_selections, ctx);
}
Ok(())
}
pub fn select_ranges_by_display_point<T>(
&mut self,
ranges: T,
ctx: &mut ModelContext<Self>,
) -> anyhow::Result<()>
where
T: IntoIterator<Item = Range<DisplayPoint>>,
{
let buffer = self.buffer(ctx);
let map = self.display_map(ctx);
let mut new_selections = Vec::new();
for range in ranges {
new_selections.push(LocalSelection {
selection: Selection {
start: map.anchor_after(range.start, Bias::Left, ctx)?,
end: map.anchor_before(range.end, Bias::Left, ctx)?,
reversed: false,
},
clamp_direction: Default::default(),
goal_start_column: None,
goal_end_column: None,
});
}
new_selections.sort_unstable_by(|a, b| a.start().cmp(b.start(), buffer).unwrap());
if let Ok(new_selections) = Vec1::try_from_vec(new_selections) {
self.change_selections(new_selections, ctx);
}
Ok(())
}
pub fn select_word_right(&mut self, ctx: &mut ModelContext<Self>) {
let buffer = self.buffer(ctx);
let mut new_selections = self.selections(ctx).clone();
for selection in new_selections.iter_mut() {
let start_position = selection.start().to_point(buffer).unwrap();
if let Ok(mut word_ends) = buffer.word_ends_from_offset_exclusive(start_position) {
let word_end = word_ends.next().unwrap_or(start_position);
let cursor = buffer.anchor_before(word_end).unwrap();
selection.set_end(cursor);
}
}
self.change_selections(new_selections, ctx);
}
pub fn select_word_left(&mut self, ctx: &mut ModelContext<Self>) {
let buffer = self.buffer(ctx);
let mut new_selections = self.selections(ctx).clone();
for selection in new_selections.iter_mut() {
let start_position = selection.start().to_point(buffer).unwrap();
if let Ok(mut word_starts) =
buffer.word_starts_backward_from_offset_exclusive(start_position)
{
let word_start = word_starts.next().unwrap_or(start_position);
let cursor = buffer.anchor_before(word_start).unwrap();
selection.set_start(cursor);
}
}
self.change_selections(new_selections, ctx);
}
/// Helper method to get an anchor at the start of the word closest to the given position
pub fn get_word_start(
&mut self,
position: DisplayPoint,
policy: WordBoundariesPolicy,
ctx: &ModelContext<Self>,
) -> Anchor {
let app = ctx;
let buffer = self.buffer(ctx);
let map = self.display_map(ctx);
let position = position
.to_buffer_point(map, Bias::Left, ctx)
.expect("Should be able to get point");
// Get the previous word start, including the current position if applicable
let word_start = buffer
.word_starts_backward_from_offset_inclusive(position)
.ok()
.and_then(|word_starts| word_starts.with_policy(policy).next())
.unwrap_or(position);
map.anchor_before(
word_start
.to_display_point(map, app)
.expect("Should be able to get start point of word"),
Bias::Left,
app,
)
.expect("Should be able to get anchor")
}
/// Helper method to get an anchor at the end of the word closest to the given position
pub fn get_word_end(
&mut self,
position: DisplayPoint,
policy: WordBoundariesPolicy,
ctx: &ModelContext<Self>,
) -> Anchor {
let app = ctx;
let buffer = self.buffer(ctx);
let map = self.display_map(ctx);
let position = position
.to_buffer_point(map, Bias::Left, ctx)
.expect("Should be able to get point");
// Get the next word ending, including the current position if applicable
let word_end = match buffer
.word_ends_from_offset_inclusive(position)
.ok()
.and_then(|word_ends| word_ends.with_policy(policy).next())
{
Some(end) => end,
None => position,
};
map.anchor_before(
word_end
.to_display_point(map, app)
.expect("Should be able to get start point of word"),
Bias::Left,
app,
)
.expect("Should be able to get anchor")
}
pub fn max_point(&self, app: &AppContext) -> DisplayPoint {
self.display_map(app).max_point(app)
}
pub fn backspace(&mut self, ctx: &mut ModelContext<Self>) {
// Only select left for empty selections
let buffer = self.buffer(ctx);
let mut new_selections = self.selections(ctx).clone();
for selection in new_selections.iter_mut() {
if selection.start().to_point(buffer).unwrap()
== selection.end().to_point(buffer).unwrap()
{
let head = selection
.head()
.to_char_offset(buffer)
.expect("Selection head should exist");
let cursor = buffer
.anchor_before(
head.saturating_sub(&1.into())
.to_point(buffer)
.expect("Delete offset should exist"),
)
.expect("Offset should be in range");
selection.set_head(buffer, cursor);
}
}
ctx.emit_a11y_content(AccessibilityContent::new(
self.selected_text(ctx),
", deleted",
WarpA11yRole::UserAction,
));
self.change_selections(new_selections, ctx);
self.insert("", None, ctx);
}
pub fn delete(&mut self, ctx: &mut ModelContext<Self>) {
let buffer = self.buffer(ctx);
let mut new_selections = self.selections(ctx).clone();
for selection in new_selections.iter_mut() {
if selection.start().to_point(buffer).unwrap()
== selection.end().to_point(buffer).unwrap()
{
let head = selection.head().to_char_offset(buffer).unwrap();
if let Ok(point) = (head + 1).to_point(buffer) {
let cursor = buffer.anchor_before(point).unwrap();
selection.set_head(buffer, cursor);
}
}
}
ctx.emit_a11y_content(AccessibilityContent::new(
self.selected_text(ctx),
", deleted",
WarpA11yRole::UserAction,
));
self.change_selections(new_selections, ctx);
self.insert("", None, ctx);
}
// Deletes the character before and after the cursor.
pub fn remove_before_and_after_cursor(&mut self, ctx: &mut ModelContext<Self>) {
let buffer = self.buffer(ctx);
let mut new_selections = self.selections(ctx).clone();
for selection in new_selections.iter_mut() {
if selection.start().to_point(buffer).unwrap()
== selection.end().to_point(buffer).unwrap()
{
let head = selection.head().to_char_offset(buffer).unwrap();
if selection.reversed() {
selection.set_end(
buffer
.anchor_before(head.saturating_sub(&1.into()).to_point(buffer).unwrap())
.expect("Selection offset should be in range"),
);
} else {
selection.set_start(
buffer
.anchor_before(head.saturating_sub(&1.into()).to_point(buffer).unwrap())
.expect("Selection offset should be in range"),
);
}
if let Ok(point) = (head + 1).to_point(buffer) {
let cursor = buffer.anchor_before(point).unwrap();
selection.set_head(buffer, cursor);
}
}
}
self.consecutive_autocomplete_insertion_edits_counter -= 1;
self.change_selections(new_selections, ctx);
self.insert("", None, ctx);
}
/// Selects one word from each cursor
pub fn vim_select_words(
&mut self,
motion: &WordMotion,
word_count: u32,
ctx: &mut ModelContext<Self>,
) {
let WordMotion {
bound,
word_type,
direction,
} = motion;
let buffer = self.buffer(ctx);
let mut new_selections = self.selections(ctx).clone();
new_selections.iter_mut().for_each(|selection| {
if let Ok(initial_offset) = selection.end().to_char_offset(buffer) {
if let Ok(boundaries) = vim_word_iterator_from_offset(
initial_offset,
buffer,
*direction,
*bound,
*word_type,
) {
let mut word_boundary = boundaries
.take(word_count as usize)
.last()
.unwrap_or(initial_offset);
let (new_start, new_end) = match direction {
// Account for vim word motion quirks across newlines.
Direction::Forward => {
// `de`, unlike other word motions, will include character it lands on
// in the operation.
if *bound == WordBound::End {
if let Ok(point) = (word_boundary + 1).to_char_offset(buffer) {
word_boundary = point;
}
} else if *bound == WordBound::Start && word_count == 1 {
// `dw`, can not traverse a newline unless the count > 1. We have
// to check this range for newlines and cut the range short in that
// case.
if let Ok(mut text) =
buffer.chars_for_range(initial_offset..word_boundary)
{
if let Some((i, _)) = text.find_position(|c| *c == '\n') {
word_boundary = initial_offset + i;
}
}
}
(initial_offset, word_boundary)
}
Direction::Backward => {
// `db` will traverse *but not delete* a newline if the count is 1 and
// the cursor starts on column zero and the line above is not empty.
let mut end = initial_offset;
if *bound == WordBound::Start && word_count == 1 {
if let Ok(mut char_iter) = buffer.chars_rev_at(initial_offset) {
if char_iter.next().is_some_and(|c| c == '\n')
&& char_iter.next().is_some_and(|c| c != '\n')
{
end -= 1;
}
}
}
(word_boundary, end)
}
};
if let Ok(anchor_start) = buffer.anchor_before(new_start) {
selection.set_start(anchor_start);
}
if let Ok(anchor_end) = buffer.anchor_before(new_end) {
selection.set_end(anchor_end);
}
}
}
});
self.change_selections(new_selections, ctx);
}
pub fn vim_select_for_char_motion(
&mut self,
motion: &CharacterMotion,
motion_type: &MotionType,
operator: &VimOperator,
operand_count: u32,
ctx: &mut ModelContext<Self>,
) {
let include_newline = *operator != VimOperator::Change;
match motion {
CharacterMotion::Down => {
self.extend_selection_below(operand_count, ctx);
}
CharacterMotion::Up => {
self.extend_selection_above(operand_count, ctx);
}
CharacterMotion::Left => {
self.move_cursors_by_offset(
operand_count,
&Direction::Backward,
/* keep_selection */ true,
/* stop_at_line_boundary */ true,
ctx,
);
}
CharacterMotion::Right => {
self.move_cursors_by_offset(
operand_count,
&Direction::Forward,
/* keep_selection */ true,
/* stop_at_line_boundary */ true,
ctx,
);
}
CharacterMotion::WrappingLeft => {
self.move_cursors_by_offset(
operand_count,
&Direction::Backward,
/* keep_selection */ true,
/* stop_at_line_boundary */ false,
ctx,
);
}
CharacterMotion::WrappingRight => {
self.move_cursors_by_offset(
operand_count,
&Direction::Forward,
/* keep_selection */ true,
/* stop_at_line_boundary */ false,
ctx,
);
}
}
if *motion_type == MotionType::Linewise {
self.extend_selection_linewise(include_newline, ctx);
}
}
pub fn vim_select_for_line_motion(
&mut self,
motion: &LineMotion,
count: u32,
ctx: &mut ModelContext<Self>,
) {
match motion {
LineMotion::Start => {
self.cursor_line_start(true /* keep_selection */, ctx)
}
LineMotion::FirstNonWhitespace => {
self.cursor_line_start_non_whitespace(true /* keep_selection */, ctx)
}
LineMotion::End => self.cursor_line_end(true /* keep_selection */, ctx),
};
self.extend_selection_below(count.saturating_sub(1), ctx);
// Ensure that the seletion covers the whole line below
// if it happens to be longer than the current one.
if motion == &LineMotion::End {
self.cursor_line_end(true, ctx);
}
}
pub fn vim_select_for_first_nonwhitespace_motion(
&mut self,
motion: &FirstNonWhitespaceMotion,
motion_type: &MotionType,
operator: &VimOperator,
operand_count: u32,
ctx: &mut ModelContext<Self>,
) {
let include_newline = *operator != VimOperator::Change;
match motion {
FirstNonWhitespaceMotion::Down => {
self.extend_selection_below(operand_count, ctx);
}
FirstNonWhitespaceMotion::DownMinusOne => {
self.extend_selection_below(operand_count - 1, ctx);
}
FirstNonWhitespaceMotion::Up => {
self.extend_selection_above(operand_count, ctx);
}
}
self.cursor_line_start_non_whitespace(true /* keep_selection */, ctx);
if *motion_type == MotionType::Linewise {
self.extend_selection_linewise(include_newline, ctx);
}
}
pub fn vim_select_for_matching_bracket(&mut self, ctx: &mut ModelContext<Self>) {
self.vim_move_cursor_to_matching_bracket(/* keep_selection */ true, ctx);
// Annoyingly, operations which have "%" as an operand will include one
// additional char on the right side. So, for this case we need to loop
// through the selections and "manually" increment the selection end
// after altering the selections with `move_cursor_to_matching_bracket`.
let buffer = self.buffer(ctx);
let mut new_selections = self.selections(ctx).clone();
for selection in new_selections.iter_mut() {
// Only increment the selection end if a particular selection is
// actually selecting a range (is _not_ just a cursor). For each
// selection, `move_cursor_to_matching_bracket` might have had no
// effect if a matching bracket wasn't found, and those cases leave
// the cursor as "only a cursor".
if !selection.is_cursor_only(buffer) {
let Ok(end_offset) = selection.end().to_char_offset(buffer) else {
continue;
};
let Ok(new_anchor) = buffer.anchor_at(end_offset + 1, AnchorBias::Right) else {
continue;
};
selection.set_end(new_anchor);
}
}
self.change_selections(new_selections, ctx);
}
pub fn vim_select_text_object(
&mut self,
object_type: &TextObjectType,
inclusion: TextObjectInclusion,
operator: &VimOperator,
ctx: &mut ModelContext<Self>,
) {
let new_selections = {
let buffer = self.buffer(ctx);
self.selections(ctx)
.iter()
.filter_map(|selection| {
let offset = selection.head().to_char_offset(buffer).ok()?;
match (object_type, inclusion) {
(TextObjectType::Word(word_type), TextObjectInclusion::Around) => {
vim_a_word(buffer, offset, *word_type)
}
(TextObjectType::Word(word_type), TextObjectInclusion::Inner) => {
vim_inner_word(buffer, offset, *word_type)
}
(TextObjectType::Paragraph, TextObjectInclusion::Around) => {
vim_a_paragraph(buffer, offset)
}
(TextObjectType::Paragraph, TextObjectInclusion::Inner) => {
vim_inner_paragraph(buffer, offset)
}
(TextObjectType::Quote(quote_type), TextObjectInclusion::Around) => {
vim_a_quote(buffer, offset, *quote_type)
}
(TextObjectType::Quote(quote_type), TextObjectInclusion::Inner) => {
vim_inner_quote(buffer, offset, *quote_type)
}
(TextObjectType::Block(bracket_type), TextObjectInclusion::Around) => {
vim_a_block(buffer, offset, *bracket_type)
}
(TextObjectType::Block(bracket_type), TextObjectInclusion::Inner) => {
let preserve_leading_padding = *operator == VimOperator::Change;
vim_inner_block(buffer, offset, *bracket_type, preserve_leading_padding)
}
}
})
.collect_vec()
};
let _ = self.select_ranges_by_offset(new_selections, ctx);
if let TextObjectType::Paragraph = object_type {
let include_newline = *operator != VimOperator::Change;
self.extend_selection_linewise(include_newline, ctx);
}
}
pub fn vim_move_by_paragraph(
&mut self,
count: u32,
direction: &Direction,
keep_selection: bool,
ctx: &mut ModelContext<Self>,
) {
let buffer = self.buffer(ctx);
let max_offset = buffer.len();
self.move_cursor(
keep_selection,
|buffer, selection| {
let Ok(mut offset) = selection.head().to_char_offset(buffer) else {
return selection.head().clone();
};
match direction {
Direction::Forward => {
for _ in 0..count {
offset = find_next_paragraph_end(buffer, offset).unwrap_or(max_offset);
}
}
Direction::Backward => {
for _ in 0..count {
offset =
find_previous_paragraph_start(buffer, offset).unwrap_or_default();
}
}
}
buffer
.anchor_before(offset)
.unwrap_or_else(|_| selection.head().clone())
},
ctx,
);
}
/// Returns true iff any selection is past the last character in the line.
/// In Vim mode, this scenario needs to be corrected (see [`Self::vim_enforce_cursor_line_cap`]).
pub fn vim_needs_line_capping<C: ModelAsRef>(&self, ctx: &C) -> bool {
let buffer = self.buffer(ctx);
self.selections(ctx).iter().any(|selection| {
let start_pt = selection.start_to_point(buffer);
let end_pt = selection.end_to_point(buffer);
let line_len = buffer.line_len(end_pt.row).unwrap_or_default();
line_len > 0 && (start_pt.column >= line_len || end_pt.column >= line_len)
})
}
/// If the cursor is after the last character in the line, move it back
/// so that it's covering the last character in the line instead.
pub fn vim_enforce_cursor_line_cap(&mut self, ctx: &mut ModelContext<Self>) {
let buffer = self.buffer(ctx);
let mut new_selections = self.selections(ctx).clone();
new_selections.iter_mut().for_each(|selection| {
let start_pt = selection.start_to_point(buffer);
let end_pt = selection.end_to_point(buffer);
let line_len = buffer.line_len(end_pt.row).unwrap_or_default();
if line_len > 0 && start_pt.column >= line_len {
selection.set_start(
buffer
.anchor_before(
selection
.start()
.to_char_offset(buffer)
.unwrap_or_default()
.saturating_sub(&1.into()),
)
.expect(
"Selection start - 1 (checked subtraction) should be a valid Anchor",
),
);
}
if line_len > 0 && end_pt.column >= line_len {
selection.set_end(
buffer
.anchor_before(
selection
.end()
.to_char_offset(buffer)
.unwrap_or_default()
.saturating_sub(&1.into()),
)
.expect("Selection end - 1 (checked subtraction) should be a valid Anchor"),
);
}
});
self.change_selections(new_selections, ctx);
}
pub fn shell_cut(&mut self, ctx: &mut ModelContext<Self>) {
let text = self.selected_text(ctx);
if !text.is_empty() {
ctx.emit(EditorModelEvent::ShellCut(text));
self.backspace(ctx);
}
}
/// Returns all local selections that intersect the provided range.
pub fn local_selections_intersecting_range<'a>(
&'a self,
range: Range<DisplayPoint>,
app: &'a AppContext,
) -> impl 'a + Iterator<Item = (&'a LocalSelection, Range<DisplayPoint>)> {
let map = self.display_map(app);
self.buffer(app)
.local_selections()
.selections_intersecting_range(range.clone(), map, app)
}
/// Returns drawable local + remote selections that intersect
/// the provided range.
pub fn all_drawable_selections_intersecting_range<'a>(
&'a self,
range: Range<DisplayPoint>,
app: &'a AppContext,
) -> impl 'a + Iterator<Item = DrawableSelection> {
let map = self.display_map(app);
self.buffer(app)
.local_selections()
.drawable_selections_intersecting_range(range.clone(), self.replica_id(app), map, app)
.chain(self.buffer(app).remote_selections().flat_map(
move |(replica_id, selections)| {
selections.drawable_selections_intersecting_range(
range.clone(),
replica_id.clone(),
map,
app,
)
},
))
}
pub fn pending_selection<'a, C: ModelAsRef>(
&'a self,
ctx: &'a C,
) -> Option<&'a LocalPendingSelection> {
self.buffer(ctx).local_selections().pending.as_ref()
}
pub fn selections<'a, C: ModelAsRef>(&'a self, ctx: &'a C) -> &'a Vec1<LocalSelection> {
&self.buffer(ctx).local_selections().selections
}
fn is_ephemeral(&self) -> bool {
self.buffer_and_display_map.ephemeral.is_some()
}
fn collaborative_buffer(&self) -> &ModelHandle<Buffer> {
&self.buffer_and_display_map.regular.0
}
fn collaborative_display_map(&self) -> &ModelHandle<DisplayMap> {
&self.buffer_and_display_map.regular.1
}
/// We only expose an immutable reference to the [`Buffer`]
/// so that callers can query anchor positions and such.
/// We do _not_ want to expose an arbitrary model handle
/// because that can otherwise be `update`d.
pub fn buffer<'a, C: ModelAsRef>(&self, ctx: &'a C) -> &'a Buffer {
self.buffer_handle().as_ref(ctx)
}
/// We only expose an immutable reference to the [`Buffer`]
/// so that callers can query visible ranges and such.
/// We do _not_ want to expose an arbitrary model handle
/// because that can otherwise be `update`d.
pub fn display_map<'a, C: ModelAsRef>(&self, ctx: &'a C) -> &'a DisplayMap {
self.display_map_handle().as_ref(ctx)
}
fn buffer_handle(&self) -> &ModelHandle<Buffer> {
self.buffer_and_display_map.buffer_handle()
}
fn display_map_handle(&self) -> &ModelHandle<DisplayMap> {
self.buffer_and_display_map.display_map_handle()
}
#[cfg(test)]
pub fn displayed_text(&self, app: &AppContext) -> String {
self.display_map(app).text(app)
}
pub fn buffer_text<C: ModelAsRef>(&self, ctx: &C) -> String {
self.buffer(ctx).text()
}
/// The length of the buffer as a [`CharOffset`].
pub fn buffer_len<C: ModelAsRef>(&self, ctx: &C) -> CharOffset {
self.buffer(ctx).len()
}
pub fn last_buffer_text(&self) -> &str {
self.last_buffer_text.as_str()
}
pub fn last_action<C: ModelAsRef>(&self, ctx: &C) -> Option<PlainTextEditorViewAction> {
self.buffer(ctx).last_action()
}
pub fn first_selection<'a, C: ModelAsRef>(&'a self, ctx: &'a C) -> &'a LocalSelection {
self.buffer(ctx).local_selections().first()
}
pub fn last_selection<'a, C: ModelAsRef>(&'a self, ctx: &'a C) -> &'a LocalSelection {
self.buffer(ctx).local_selections().last()
}
pub fn is_single_cursor_only<C: ModelAsRef>(&self, ctx: &C) -> bool {
self.selections(ctx).len() == 1 && self.selections(ctx)[0].is_cursor_only(self.buffer(ctx))
}
#[cfg(test)]
pub fn is_cursor_only<C: ModelAsRef>(&self, selection: &LocalSelection, ctx: &C) -> bool {
selection.is_cursor_only(self.buffer(ctx))
}
pub fn copy_selection_to_vim_register(
&self,
register_name: char,
motion_type: MotionType,
ctx: &mut ModelContext<Self>,
) {
let mut text = self.selected_text(ctx);
// Linewise motions have a special case to handle w.r.t. where the newlines are.
// All linewise copies should add a trailing, not a leading, newline to the selected
// content. This is already upheld for most cases when [`Self::extend_selections_to_lines`]
// had been called, but not when the selection includes the final line in the buffer. In
// that case, since the final line has no trailing newline to select, a leading newline is
// selected instead. This method will try to detect that case and correct for it by moving
// that newline to the end.
if motion_type == MotionType::Linewise {
let buffer = self.buffer(ctx);
let max_point = buffer.max_point();
// Check if any of the selections extend to the end.
let selection_at_end = self.selections(ctx).iter().any(|selection| {
selection
.end()
.to_point(buffer)
.expect("Selection end must be valid Point")
== max_point
});
if selection_at_end {
// If they do, first add the trailing newline.
text.push('\n');
// We can only be sure we should remove the leading newline if there is only one
// selection. If there are multiple, it may be that another selection range started
// on a newline that the user explicitly selected and we don't want to trim in that
// case.
#[allow(clippy::assigning_clones)]
if self.selections(ctx).len() == 1 {
text = text.trim_start_matches('\n').to_owned();
}
}
}
VimRegisters::handle(ctx).update(ctx, |registers, ctx| {
registers.write_to_register(register_name, text, motion_type, ctx);
});
}
/// Extend the current selection to cover line(s) below its current endpoint.
/// Ex: buffer contains "abcd\nefg" and self.selections contains "b".
/// After running this function, self.selections will contain "bcd\ne".
pub fn extend_selection_below(
&mut self,
line_extension_count: u32,
ctx: &mut ModelContext<Self>,
) {
if line_extension_count == 0 {
return;
}
let buffer = self.buffer(ctx);
let max_point = self.max_point(ctx);
let mut new_selections = self.selections(ctx).clone();
for selection in new_selections.iter_mut() {
// Move selection end to the next lines.
let Ok(mut end_point) = selection.end().to_point(buffer) else {
continue;
};
if end_point.row + line_extension_count > max_point.row() {
end_point.row = max_point.row();
end_point.column = max_point.column();
} else {
end_point.row += line_extension_count;
let line_len = buffer.line_len(end_point.row).unwrap_or(0);
end_point.column = u32::min(end_point.column, line_len);
}
let Ok(anchor) = buffer.anchor_after(end_point) else {
continue;
};
selection.set_end(anchor);
}
self.change_selections(new_selections, ctx);
}
/// Extend the current selection to cover line(s) above its current start point.
/// Ex: buffer contains "abcd\nefg" and self.selections contains "f".
/// After running this function, self.selections will contain "cd\nef".
pub fn extend_selection_above(
&mut self,
line_extension_count: u32,
ctx: &mut ModelContext<Self>,
) {
if line_extension_count == 0 {
return;
}
let buffer = self.buffer(ctx);
let mut new_selections = self.selections(ctx).clone();
for selection in new_selections.iter_mut() {
// Move selection start to the previous lines.
let Ok(mut start_point) = selection.start().to_point(buffer) else {
continue;
};
start_point.row = start_point.row.saturating_sub(line_extension_count);
start_point.column = u32::min(
start_point.column,
buffer
.line_len(start_point.row)
.unwrap_or(start_point.column),
);
let Ok(start_anchor) = buffer.anchor_before(start_point) else {
continue;
};
selection.set_start(start_anchor);
}
self.change_selections(new_selections, ctx);
}
pub fn vim_set_visual_tail_to_selection_heads<C: ModelAsRef>(&mut self, ctx: &C) {
self.vim_visual_tails = self
.selections(ctx)
.iter()
.map(|selection| selection.head().clone())
.collect();
}
pub fn vim_set_visual_tails(&mut self, new_tails: Vec<Anchor>) {
self.vim_visual_tails = new_tails;
}
pub fn is_selecting(&self, app: &AppContext) -> bool {
self.pending_selection(app).is_some()
}
pub fn line_len(&self, display_row: u32, app: &AppContext) -> anyhow::Result<u32> {
self.display_map(app).line_len(display_row, app)
}
pub fn chars_preceding_selections<'a, C: ModelAsRef>(
&'a self,
app: &'a C,
) -> impl Iterator<Item = Chars<'a>> + 'a {
let buffer = self.buffer(app);
self.selections(app)
.iter()
.filter_map(|selection| buffer.chars_at(selection.start()).ok())
}
pub fn any_selections_span_entire_buffer<C: ModelAsRef>(&self, app: &C) -> bool {
let buffer = self.buffer(app);
self.selections(app)
.iter()
.any(|selection| selection.spans_entire_buffer(buffer))
}
#[cfg(test)]
pub fn selection_to_byte_offset<C: ModelAsRef>(
&self,
selection: &LocalSelection,
ctx: &C,
) -> Range<ByteOffset> {
selection.to_byte_offset(self.buffer(ctx))
}
}
/// The private interface.
impl EditorModel {
fn handle_buffer_event(&mut self, event: &buffer::Event, ctx: &mut ModelContext<Self>) {
match event {
buffer::Event::Edited { edit_origin, .. } => ctx.emit(EditorModelEvent::Edited {
edit_origin: *edit_origin,
}),
buffer::Event::SelectionsChanged => ctx.emit(EditorModelEvent::SelectionsChanged),
buffer::Event::StylesUpdated => ctx.emit(EditorModelEvent::StylesUpdated),
buffer::Event::UpdatePeers { operations } => ctx.emit(EditorModelEvent::UpdatePeers {
operations: operations.clone(),
}),
}
}
fn handle_buffer_event_for_non_collaborative_editor(
&mut self,
event: &buffer::Event,
ctx: &mut ModelContext<Self>,
) {
// For non-collaborative editors, we don't care about fanning out updates to peers.
if !matches!(event, buffer::Event::UpdatePeers { .. }) {
self.handle_buffer_event(event, ctx);
}
}
fn handle_display_map_event(
&mut self,
event: &display_map::Event,
ctx: &mut ModelContext<Self>,
) {
match event {
display_map::Event::Folded | display_map::Event::Unfolded => {
ctx.emit(EditorModelEvent::DisplayMapUpdated)
}
}
}
fn selection_to_char_offset_ranges(
&self,
ctx: &mut ModelContext<Self>,
) -> Vec1<Range<CharOffset>> {
self.selections(ctx).clone().mapped(|selection| {
let start = selection.start().to_char_offset(self.buffer(ctx)).unwrap();
let end = selection.end().to_char_offset(self.buffer(ctx)).unwrap();
start..end
})
}
// Insert characters around the current selections. For example, if the selection is "abc"
// and the before_cursor_text is "(" and after_cursor_text is ")", the resulting
// buffer will be "(abc)".
fn insert_characters_wrap_around_selection(
&mut self,
before_cursor_text: &str,
after_cursor_text: &str,
ctx: &mut ModelContext<Self>,
) {
let offset_ranges = self.selection_to_char_offset_ranges(ctx);
let before_cursor_char_len = before_cursor_text.chars().count();
let after_cursor_char_len = after_cursor_text.chars().count();
self.buffer_handle().update(ctx, |buffer, ctx| {
// Inserting the user-typed characters.
if let Err(error) = buffer.edit(
offset_ranges
.iter()
.map(|range| range.start..range.start)
.clone(),
before_cursor_text,
ctx,
) {
log::error!("error inserting text: {error}");
};
// Inserting the autocompleted characters. Because the buffer has
// changed due to the insertion of user-typed characters, we need to
// shift the insertion range by its index here.
if let Err(error) = buffer.edit(
offset_ranges
.iter()
.enumerate()
.map(|(idx, range)| range.end + idx + 1..range.end + idx + 1)
.clone(),
after_cursor_text,
ctx,
) {
log::error!("error inserting text: {error}");
};
});
let buffer = self.buffer(ctx);
let mut delta = before_cursor_char_len;
let new_selections = offset_ranges.mapped(|range| {
let range_start = range.start;
let range_end = range.end;
let end = buffer.anchor_before(range_end + delta).unwrap();
let start = buffer.anchor_before(range_start + delta).unwrap();
delta += before_cursor_char_len + after_cursor_char_len;
LocalSelection {
selection: Selection {
start,
end,
reversed: false,
},
clamp_direction: Default::default(),
goal_start_column: None,
goal_end_column: None,
}
});
self.change_selections(new_selections, ctx);
}
fn no_cursor_only_selections(&self, ctx: &mut ModelContext<Self>) -> bool {
self.selections(ctx)
.iter()
.all(|selection| !selection.is_cursor_only(self.buffer(ctx)))
}
fn all_cursors_next_character_matches_char(
&self,
character: char,
ctx: &mut ModelContext<Self>,
) -> bool {
let buffer = self.buffer(ctx);
self.selections(ctx).iter().all(|selection| {
let position = selection
.start()
.to_point(buffer)
.expect("Start of selection should exist");
buffer
.chars_at(position)
.unwrap()
.next()
.map(|right_char| right_char == character)
.unwrap_or(false)
})
}
/// Attempt to include a newline in the selection, if there one.
/// By default, attempt to select the newline following the selection.
/// If that's not possible, attempt to select the newline before the selection.
fn include_newline_in_selection(&mut self, ctx: &mut ModelContext<Self>) {
let buffer = self.buffer(ctx);
let mut new_selections = self.selections(ctx).clone();
new_selections.iter_mut().for_each(|selection| {
let start = selection
.start()
.to_char_offset(buffer)
.expect("current selection start should have valid char offset");
let end = selection
.end()
.to_char_offset(buffer)
.expect("current selection end should have valid char offset");
let start_newline = start.as_usize() > 0
&& buffer
.chars_at(start.saturating_sub(&1.into()))
.is_ok_and(|mut chars| chars.next().unwrap_or_default() == '\n');
let end_newline = buffer
.chars_at(end)
.is_ok_and(|mut chars| chars.next().unwrap_or_default() == '\n');
// By default, attempt to select the newline following the line text.
if end_newline {
if let Ok(point) = (end + 1).to_point(buffer) {
selection.set_end(
buffer
.anchor_before(point)
.expect("valid point should be vaild anchor"),
);
}
// If that's not possible, attempt to select the newline before the line text.
} else if start_newline {
if let Ok(point) = start.saturating_sub(&1.into()).to_point(buffer) {
selection.set_start(
buffer
.anchor_before(point)
.expect("valid point should be valid anchor"),
);
}
}
});
self.change_selections(new_selections, ctx);
}
/// Determines if adding the given string `text` to the buffer (which replaces all selections)
/// would exceed the maximum buffer length.
fn text_insertion_would_exceed_max_buffer_len(&self, text: &str, app: &AppContext) -> bool {
let Some(max_buffer_len) = self.max_buffer_len else {
return false;
};
let text_len = text.chars().count();
let selections = self.selected_text_strings(app);
// `diff` represents the difference in length that inserting `text` into each
// selection would result in.
let diff = selections.iter().fold(0, |running_diff, curr| {
let diff_from_selection = text_len as isize - curr.chars().count() as isize;
running_diff + diff_from_selection
});
let buffer = self.buffer(app);
buffer.len().as_usize() as isize + diff > max_buffer_len as isize
}
fn text_is_invalid_input_type(&self, text: &str) -> bool {
match self.valid_input_type {
ValidInputType::All => false,
ValidInputType::PositiveInteger => text.parse::<u16>().is_err() && !text.is_empty(),
ValidInputType::NoSpaces => text.contains(char::is_whitespace),
}
}
fn is_line_foldable(&self, display_row: u32, app: &AppContext) -> bool {
let max_point = self.max_point(app);
if display_row >= max_point.row() {
false
} else {
let (start_indent, is_blank) = self.line_indent(display_row, app).unwrap();
if is_blank {
false
} else {
for display_row in display_row + 1..=max_point.row() {
let (indent, is_blank) = self.line_indent(display_row, app).unwrap();
if !is_blank {
return indent > start_indent;
}
}
false
}
}
}
fn line_indent(&self, display_row: u32, app: &AppContext) -> anyhow::Result<(usize, bool)> {
let mut indent = 0;
let mut is_blank = true;
for c in self
.display_map(app)
.chars_at(DisplayPoint::new(display_row, 0), app)?
{
if c == ' ' {
indent += 1;
} else {
is_blank = c == '\n';
break;
}
}
Ok((indent, is_blank))
}
fn foldable_range_for_line(
&self,
start_row: u32,
app: &AppContext,
) -> anyhow::Result<Range<Point>> {
let map = self.display_map(app);
let max_point = self.max_point(app);
let (start_indent, _) = self.line_indent(start_row, app)?;
let start = DisplayPoint::new(start_row, self.line_len(start_row, app)?);
let mut end = None;
for row in start_row + 1..=max_point.row() {
let (indent, is_blank) = self.line_indent(row, app)?;
if !is_blank && indent <= start_indent {
end = Some(DisplayPoint::new(row - 1, self.line_len(row - 1, app)?));
break;
}
}
let end = end.unwrap_or(max_point);
Ok(start.to_buffer_point(map, Bias::Left, app)?
..end.to_buffer_point(map, Bias::Left, app)?)
}
}
#[cfg(test)]
#[path = "mod_test.rs"]
mod tests;