Files
galaxy/crates/editor/src/content/selection_model.rs
T

387 lines
14 KiB
Rust

use std::ops::Range;
use galaxyui::{AppContext, Entity, ModelHandle};
use itertools::Itertools;
use string_offset::CharOffset;
use vec1::{Vec1, vec1};
use crate::content::{
anchor::{Anchor, AnchorSide, AnchorUpdate, Anchors},
buffer::{Buffer, SelectionOffsets, ToBufferPoint},
selection::{Selection, SelectionSet},
text::{BlockType, TextStylesWithMetadata},
};
/// A snapshot of the selection state. This includes all data reported by [`BufferEvent::SelectionChanged`].
#[derive(PartialEq, Eq)]
pub(super) struct SelectionSnapshot {
/// We must snapshot the resolved selections, rather than their anchors, because anchors are
/// updated in-place and won't directly reflect the update.
pub(super) selections: Vec1<SelectionOffsets>,
pub(super) active_text_styles: TextStylesWithMetadata,
pub(super) active_block_type: BlockType,
}
pub struct BufferSelectionModel {
selections: SelectionSet,
pub(crate) anchors: Anchors,
buffer: ModelHandle<Buffer>,
}
impl BufferSelectionModel {
pub fn new(buffer: ModelHandle<Buffer>) -> Self {
let mut anchors = Anchors::new();
let head_anchor = anchors.create_anchor(CharOffset::from(1), AnchorSide::Right);
let tail_anchor = anchors.create_anchor(CharOffset::from(1), AnchorSide::Right);
let selections = SelectionSet::new(Selection::new(head_anchor, tail_anchor));
Self {
selections,
buffer,
anchors,
}
}
pub fn update_anchors(&mut self, anchor_updates: Vec<AnchorUpdate>) {
for update in anchor_updates {
self.anchors.update(update);
}
}
pub(super) fn truncate(&mut self) {
self.selections.truncate();
}
/// Create a new anchor at the given offset.
pub fn anchor(&mut self, offset: CharOffset, ctx: &AppContext) -> Anchor {
self.anchors
.create_anchor(offset.min(self.buffer.as_ref(ctx).len()), AnchorSide::Right)
}
pub(super) fn create_anchor(&mut self, offset: CharOffset, side: AnchorSide) -> Anchor {
self.anchors.create_anchor(offset, side)
}
/// Resolve an anchor to its current offset.
pub fn resolve_anchor(&self, anchor: &Anchor) -> Option<CharOffset> {
self.anchors.resolve(anchor)
}
/// Resolve an anchor to its 0-based line number in the buffer.
pub fn line_number_from_anchor(&self, anchor: &Anchor, ctx: &AppContext) -> Option<usize> {
let char_offset = self.resolve_anchor(anchor)?;
Some(char_offset.to_buffer_point(self.buffer.as_ref(ctx)).row as usize)
}
// The following two are temporary methods until multiple selections are supported.
// Todo (kc CLD-1018): This should no longer be needed once we move to multi-selection.
pub fn selection(&self) -> &Selection {
self.selections.first()
}
pub fn selections(&self) -> &SelectionSet {
&self.selections
}
pub fn selections_mut(&mut self) -> &mut SelectionSet {
&mut self.selections
}
/// Returns the index of all lines that have active selection.
pub fn selected_lines(&self, ctx: &AppContext) -> Vec1<usize> {
Vec1::try_from_vec(
self.selections
.iter()
.flat_map(|s| {
let range = self.selection_to_offset_range(s);
let start_idx =
range.start.to_buffer_point(self.buffer.as_ref(ctx)).row as usize;
let end_idx = ((range.end - 1).to_buffer_point(self.buffer.as_ref(ctx)).row
as usize)
.max(start_idx);
start_idx..end_idx + 1
})
.sorted()
.dedup()
.collect_vec(),
)
.expect("Should have more than 1 element")
}
pub fn selection_to_offset_range(&self, selection: &Selection) -> Range<CharOffset> {
// Selection should always be valid when we have single selections.
// TODO(kevin): migrate this when moving to multi-selection.
let head = self
.resolve_anchor(selection.head())
.expect("anchor should exist");
let tail = self
.resolve_anchor(selection.tail())
.expect("anchor should exist");
if head >= tail { tail..head } else { head..tail }
}
// Todo (kc CLD-1018): This should no longer be needed once we move to multi-selection.
pub fn selection_to_first_offset_range(&self) -> Range<CharOffset> {
self.selection_to_offset_range(self.selection())
}
pub fn selections_to_offset_ranges(&self) -> Vec1<Range<CharOffset>> {
self.selections
.selection_map(|s| self.selection_to_offset_range(s))
}
fn create_selection(&mut self, head: CharOffset, tail: CharOffset) -> Selection {
Selection::new(
self.create_anchor(head, AnchorSide::Right),
self.create_anchor(tail, AnchorSide::Right),
)
}
pub fn set_selections(&mut self, selections: SelectionSet) {
self.selections = selections;
}
/// Checks for overlapping selections. If any are found, we merge them together.
///
/// Before:
/// | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
/// | S1 | | S3 |
/// | S2 |
/// | S4 |
///
/// After:
/// | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
/// | S1 |
/// | S2 |
pub fn merge_overlapping_selections(&mut self) {
let (overlap_indices, new_ranges) =
Buffer::overlapping_ranges(self.selections_to_offset_ranges().to_vec());
// Return early if there are no overlapping selections.
if overlap_indices.is_empty() {
return;
}
// To determine whether the head of new selections should be before or after the tail,
// we check whether overlapping selections have the head before the tail.
// This seems to work in the common cases.
let forwards_selection = self
.selections
.iter()
.enumerate()
.filter(|(i, _)| overlap_indices.contains(i))
.any(|(_, selection)| self.selection_head(selection) > self.selection_tail(selection));
// Keep any non-overlapping selections.
let mut new_selections: Vec<Selection> = self
.selections
.iter()
.enumerate()
.flat_map(|(i, selection)| {
if !overlap_indices.contains(&i) {
Some(selection.clone())
} else {
None
}
})
.collect();
// Create any new selections from the overlapping selections.
for range in new_ranges {
let new_selection = if forwards_selection {
self.create_selection(range.end, range.start)
} else {
self.create_selection(range.start, range.end)
};
new_selections.push(new_selection);
}
match SelectionSet::try_from(new_selections) {
Ok(selections) => self.selections = selections,
Err(_) => {
log::error!("After removing overlapping selections, there were no selections left!")
}
}
}
// Todo (kc CLD-1018): This should no longer be needed once we move to multi-selection.
pub fn first_selection_head(&self) -> CharOffset {
self.resolve_anchor(self.selections.first().head())
.expect("anchor should exist")
}
/// The current location of the selection head.
pub fn selection_head(&self, selection: &Selection) -> CharOffset {
self.resolve_anchor(selection.head())
.expect("anchor should exist")
}
pub fn selection_heads(&self) -> Vec1<CharOffset> {
self.selections
.selection_map(|s| self.resolve_anchor(s.head()).expect("anchor should exist"))
}
// Todo (kc CLD-1018): This should no longer be needed once we move to multi-selection.
pub fn first_selection_tail(&self) -> CharOffset {
self.resolve_anchor(self.selection().tail())
.expect("anchor should exist")
}
/// The current location of the selection tail.
pub fn selection_tail(&self, selection: &Selection) -> CharOffset {
self.resolve_anchor(selection.tail())
.expect("anchor should exist")
}
/// Return all selected ranges.
pub fn selection_offsets(&self) -> Vec1<SelectionOffsets> {
self.selections.selection_map(|s| SelectionOffsets {
head: self.resolve_anchor(s.head()).expect("anchor should exist"),
tail: self.resolve_anchor(s.tail()).expect("anchor should exist"),
})
}
pub fn is_single_selection(&self) -> bool {
self.selections.len() == 1
}
/// Query for the set of text styles that are fully active over the current
/// selection.
pub fn selection_text_styles(&self, ctx: &AppContext) -> TextStylesWithMetadata {
self.selections_to_offset_ranges()
.into_iter()
.map(|range| self.buffer.as_ref(ctx).range_text_styles(range))
.reduce(|style1, style2| style1.mutual_styles(style2))
.expect("At least one selection style should exist")
}
pub fn active_block_type_at_selection(
&self,
selection: &Selection,
ctx: &AppContext,
) -> BlockType {
let range = self.selection_to_offset_range(selection);
self.buffer.as_ref(ctx).block_type_at_point(range.start)
}
/// Check whether the block type for every selection allow formatting.
pub fn all_selections_allow_formatting(&self, ctx: &AppContext) -> bool {
self.selections.iter().all(|selection| {
match self.active_block_type_at_selection(selection, ctx) {
BlockType::Text(block_style) => block_style.allows_formatting(),
// If pasting content directly after a rich block item, we should keep the content's original
// styling.
BlockType::Item(_) => true,
}
})
}
// Todo: kc (CLD1018) Temporary until multiselect is implemented.
pub fn first_selection_is_single_cursor(&self) -> bool {
// Selection should always be valid when we have single selections.
// TODO(kevin): migrate this when moving to multi-selection.
self.first_selection_head() == self.first_selection_tail()
}
pub fn selection_is_single_cursor(&self, selection: &Selection) -> bool {
self.selection_head(selection) == self.selection_tail(selection)
}
/// Return true if all selections are single cursors.
pub fn all_single_cursors(&self) -> bool {
self.selections
.iter()
.all(|s| self.selection_is_single_cursor(s))
}
pub fn cursors_at_line_start(&self, ctx: &AppContext) -> bool {
self.all_single_cursors();
for selection in self.selections.iter() {
let cursor_offset = self.selection_head(selection);
if cursor_offset != self.buffer.as_ref(ctx).containing_line_start(cursor_offset) {
return false;
}
}
true
}
/// Validate the buffer content with this selection model's anchors.
pub fn validate_buffer(&self, ctx: &impl galaxyui::ModelAsRef) {
self.buffer.as_ref(ctx).validate(&self.anchors);
}
pub(super) fn shift_selections_after_offset(&mut self, offset: CharOffset, delta: usize) {
for selection in self.selections.iter_mut() {
let head_offset = self
.anchors
.resolve(selection.head())
.expect("Anchor should be valid");
if head_offset >= offset && head_offset < offset + delta {
selection.set_head(&mut self.anchors, offset + delta);
}
let tail_offset = self
.anchors
.resolve(selection.tail())
.expect("Anchor should be valid");
if tail_offset >= offset && tail_offset < offset + delta {
selection.set_tail(&mut self.anchors, offset + delta);
}
}
}
pub(super) fn set_single_cursor(&mut self, offset: CharOffset) {
self.set_selection_offsets(vec1![SelectionOffsets {
head: offset,
tail: offset
}]);
}
pub(super) fn set_clamped_selection_head(
&mut self,
selection: &mut Selection,
offset: CharOffset,
) {
selection.set_head(&mut self.anchors, offset);
}
pub(super) fn set_clamped_selection_tail(
&mut self,
selection: &mut Selection,
offset: CharOffset,
) {
selection.set_tail(&mut self.anchors, offset);
}
pub(super) fn update_selection_offsets(&mut self, clamped_selections: Vec1<SelectionOffsets>) {
debug_assert!(
clamped_selections.len() == self.selections().len(),
"To update selection offsets, you must provide the same number of offset pairs as there are currently active selections"
);
for (selection, offsets) in self.selections.iter_mut().zip(clamped_selections.iter()) {
selection.set_head(&mut self.anchors, offsets.head);
selection.set_tail(&mut self.anchors, offsets.tail);
}
}
/// Clear the current selections and replace with this new set of selections.
/// All biases will be cleared.
pub fn set_selection_offsets(&mut self, selections: Vec1<SelectionOffsets>) {
let new_selections = selections.mapped(|offsets| {
let head_anchor = self.create_anchor(offsets.head, AnchorSide::Right);
let tail_anchor = self.create_anchor(offsets.tail, AnchorSide::Right);
Selection::new(head_anchor, tail_anchor)
});
self.set_selections(new_selections.into());
}
}
impl Entity for BufferSelectionModel {
type Event = ();
}