## Why `textDocument/onTypeFormatting` edits that insert or replace text at an empty cursor use its right bias and move it past the new text. In paired tags, pressing Enter can therefore leave the cursor on the closing tag instead of between the tags. ## What - Capture a left-biased pin for each empty cursor before requesting on-type formatting. - Skip cursor tracking unless a matching language server advertises the trigger. - Restore only unchanged empty cursors whose displacement is fully covered by formatting transaction ranges, so intervening user edits are preserved. - Reset vertical movement state when restoring a cursor. ## Testing - `cargo test -p editor test_on_type_formatting` (5 passed) - `./script/clippy -p editor` ## References - Fixes https://github.com/zed-industries/zed/issues/61574 Release Notes: - Fixed the cursor being moved past text inserted or replaced at its position during on-type formatting. --------- Co-authored-by: Kirill Bulatov <kirill@zed.dev>
170 lines
4.1 KiB
Rust
170 lines
4.1 KiB
Rust
use crate::{Anchor, BufferSnapshot, TextDimension};
|
|
use std::cmp::Ordering;
|
|
use std::ops::Range;
|
|
|
|
#[derive(Default, Copy, Clone, Debug, PartialEq)]
|
|
pub enum SelectionGoal {
|
|
#[default]
|
|
None,
|
|
HorizontalPosition(f64),
|
|
HorizontalRange {
|
|
start: f64,
|
|
end: f64,
|
|
},
|
|
WrappedHorizontalPosition((u32, f32)),
|
|
}
|
|
|
|
#[derive(Copy, Clone, Debug, PartialEq)]
|
|
pub struct Selection<T> {
|
|
pub id: usize,
|
|
pub start: T,
|
|
pub end: T,
|
|
pub reversed: bool,
|
|
pub goal: SelectionGoal,
|
|
}
|
|
|
|
impl<T: Clone> Selection<T> {
|
|
/// A place where the selection had stopped at.
|
|
pub fn head(&self) -> T {
|
|
if self.reversed {
|
|
self.start.clone()
|
|
} else {
|
|
self.end.clone()
|
|
}
|
|
}
|
|
|
|
/// A place where selection was initiated from.
|
|
pub fn tail(&self) -> T {
|
|
if self.reversed {
|
|
self.end.clone()
|
|
} else {
|
|
self.start.clone()
|
|
}
|
|
}
|
|
|
|
pub fn map<F, S>(&self, f: F) -> Selection<S>
|
|
where
|
|
F: Fn(T) -> S,
|
|
{
|
|
Selection::<S> {
|
|
id: self.id,
|
|
start: f(self.start.clone()),
|
|
end: f(self.end.clone()),
|
|
reversed: self.reversed,
|
|
goal: self.goal,
|
|
}
|
|
}
|
|
|
|
pub fn collapse_to(&mut self, point: T, new_goal: SelectionGoal) {
|
|
self.start = point.clone();
|
|
self.end = point;
|
|
self.goal = new_goal;
|
|
self.reversed = false;
|
|
}
|
|
}
|
|
|
|
impl<T: Copy + Ord> Selection<T> {
|
|
pub fn is_empty(&self) -> bool {
|
|
self.start == self.end
|
|
}
|
|
|
|
pub fn set_head(&mut self, head: T, new_goal: SelectionGoal) {
|
|
if head.cmp(&self.tail()) < Ordering::Equal {
|
|
if !self.reversed {
|
|
self.end = self.start;
|
|
self.reversed = true;
|
|
}
|
|
self.start = head;
|
|
} else {
|
|
if self.reversed {
|
|
self.start = self.end;
|
|
self.reversed = false;
|
|
}
|
|
self.end = head;
|
|
}
|
|
self.goal = new_goal;
|
|
}
|
|
|
|
pub fn set_tail(&mut self, tail: T, new_goal: SelectionGoal) {
|
|
if tail.cmp(&self.head()) <= Ordering::Equal {
|
|
if self.reversed {
|
|
self.end = self.start;
|
|
self.reversed = false;
|
|
}
|
|
self.start = tail;
|
|
} else {
|
|
if !self.reversed {
|
|
self.start = self.end;
|
|
self.reversed = true;
|
|
}
|
|
self.end = tail;
|
|
}
|
|
self.goal = new_goal;
|
|
}
|
|
|
|
pub fn set_head_tail(&mut self, head: T, tail: T, new_goal: SelectionGoal) {
|
|
if head < tail {
|
|
self.reversed = true;
|
|
self.start = head;
|
|
self.end = tail;
|
|
} else {
|
|
self.reversed = false;
|
|
self.start = tail;
|
|
self.end = head;
|
|
}
|
|
self.goal = new_goal;
|
|
}
|
|
|
|
pub fn swap_head_tail(&mut self) {
|
|
if self.reversed {
|
|
self.reversed = false;
|
|
} else {
|
|
std::mem::swap(&mut self.start, &mut self.end);
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<T: Copy> Selection<T> {
|
|
pub fn range(&self) -> Range<T> {
|
|
self.start..self.end
|
|
}
|
|
}
|
|
|
|
impl<T: std::ops::Sub + Copy> Selection<T> {
|
|
pub fn len(&self) -> <T as std::ops::Sub>::Output {
|
|
self.end - self.start
|
|
}
|
|
}
|
|
|
|
impl<T: Copy + Eq> Selection<T> {
|
|
#[cfg(feature = "test-support")]
|
|
pub fn from_offset(offset: T) -> Self {
|
|
Selection {
|
|
id: 0,
|
|
start: offset,
|
|
end: offset,
|
|
goal: SelectionGoal::None,
|
|
reversed: false,
|
|
}
|
|
}
|
|
|
|
pub fn equals(&self, offset_range: &Range<T>) -> bool {
|
|
self.start == offset_range.start && self.end == offset_range.end
|
|
}
|
|
}
|
|
|
|
impl Selection<Anchor> {
|
|
pub fn resolve<'a, D: 'a + TextDimension>(
|
|
&'a self,
|
|
snapshot: &'a BufferSnapshot,
|
|
) -> Selection<D> {
|
|
Selection {
|
|
id: self.id,
|
|
start: snapshot.summary_for_anchor(&self.start),
|
|
end: snapshot.summary_for_anchor(&self.end),
|
|
reversed: self.reversed,
|
|
goal: self.goal,
|
|
}
|
|
}
|
|
}
|