Merge pull request #973 from zed-industries/selections-refactor

Pull selections out of editor into selections collection
This commit is contained in:
Keith Simmons 2022-05-13 16:07:26 -07:00 committed by GitHub
commit 2f7eb6dbc5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 1998 additions and 1722 deletions

View file

@ -37,7 +37,7 @@ impl Breadcrumbs {
cx: &AppContext,
) -> Option<(ModelHandle<Buffer>, Vec<OutlineItem<Anchor>>)> {
let editor = self.editor.as_ref()?.read(cx);
let cursor = editor.newest_anchor_selection().head();
let cursor = editor.selections.newest_anchor().head();
let multibuffer = &editor.buffer().read(cx);
let (buffer_id, symbols) = multibuffer
.read(cx)

View file

@ -3003,7 +3003,7 @@ mod tests {
// Type a completion trigger character as the guest.
editor_b.update(cx_b, |editor, cx| {
editor.select_ranges([13..13], None, cx);
editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
editor.handle_input(&Input(".".into()), cx);
cx.focus(&editor_b);
});
@ -4215,7 +4215,9 @@ mod tests {
// Move cursor to a location that contains code actions.
editor_b.update(cx_b, |editor, cx| {
editor.select_ranges([Point::new(1, 31)..Point::new(1, 31)], None, cx);
editor.change_selections(None, cx, |s| {
s.select_ranges([Point::new(1, 31)..Point::new(1, 31)])
});
cx.focus(&editor_b);
});
@ -4452,7 +4454,7 @@ mod tests {
// Move cursor to a location that can be renamed.
let prepare_rename = editor_b.update(cx_b, |editor, cx| {
editor.select_ranges([7..7], None, cx);
editor.change_selections(None, cx, |s| s.select_ranges([7..7]));
editor.rename(&Rename, cx).unwrap()
});
@ -5460,8 +5462,12 @@ mod tests {
});
// When client B starts following client A, all visible view states are replicated to client B.
editor_a1.update(cx_a, |editor, cx| editor.select_ranges([0..1], None, cx));
editor_a2.update(cx_a, |editor, cx| editor.select_ranges([2..3], None, cx));
editor_a1.update(cx_a, |editor, cx| {
editor.change_selections(None, cx, |s| s.select_ranges([0..1]))
});
editor_a2.update(cx_a, |editor, cx| {
editor.change_selections(None, cx, |s| s.select_ranges([2..3]))
});
workspace_b
.update(cx_b, |workspace, cx| {
workspace
@ -5483,11 +5489,11 @@ mod tests {
Some((worktree_id, "2.txt").into())
);
assert_eq!(
editor_b2.read_with(cx_b, |editor, cx| editor.selected_ranges(cx)),
editor_b2.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
vec![2..3]
);
assert_eq!(
editor_b1.read_with(cx_b, |editor, cx| editor.selected_ranges(cx)),
editor_b1.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
vec![0..1]
);
@ -5526,11 +5532,11 @@ mod tests {
// Changes to client A's editor are reflected on client B.
editor_a1.update(cx_a, |editor, cx| {
editor.select_ranges([1..1, 2..2], None, cx);
editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2]));
});
editor_b1
.condition(cx_b, |editor, cx| {
editor.selected_ranges(cx) == vec![1..1, 2..2]
editor.selections.ranges(cx) == vec![1..1, 2..2]
})
.await;
@ -5540,11 +5546,13 @@ mod tests {
.await;
editor_a1.update(cx_a, |editor, cx| {
editor.select_ranges([3..3], None, cx);
editor.change_selections(None, cx, |s| s.select_ranges([3..3]));
editor.set_scroll_position(vec2f(0., 100.), cx);
});
editor_b1
.condition(cx_b, |editor, cx| editor.selected_ranges(cx) == vec![3..3])
.condition(cx_b, |editor, cx| {
editor.selections.ranges(cx) == vec![3..3]
})
.await;
// After unfollowing, client B stops receiving updates from client A.

View file

@ -5,7 +5,7 @@ use collections::{BTreeSet, HashSet};
use editor::{
diagnostic_block_renderer,
display_map::{BlockDisposition, BlockId, BlockProperties, RenderBlock},
highlight_diagnostic_message, Editor, ExcerptId, MultiBuffer, ToOffset,
highlight_diagnostic_message, Autoscroll, Editor, ExcerptId, MultiBuffer, ToOffset,
};
use gpui::{
actions, elements::*, fonts::TextStyle, serde_json, AnyViewHandle, AppContext, Entity,
@ -417,8 +417,9 @@ impl ProjectDiagnosticsEditor {
}];
} else {
groups = self.path_states.get(path_ix)?.diagnostic_groups.as_slice();
new_excerpt_ids_by_selection_id = editor.refresh_selections(cx);
selections = editor.local_selections::<usize>(cx);
new_excerpt_ids_by_selection_id =
editor.change_selections(Some(Autoscroll::Fit), cx, |s| s.refresh());
selections = editor.selections.all::<usize>(cx);
}
// If any selection has lost its position, move it to start of the next primary diagnostic.
@ -441,7 +442,9 @@ impl ProjectDiagnosticsEditor {
}
}
}
editor.update_selections(selections, None, cx);
editor.change_selections(None, cx, |s| {
s.select(selections);
});
Some(())
});
@ -894,7 +897,7 @@ mod tests {
// Cursor is at the first diagnostic
view.editor.update(cx, |editor, cx| {
assert_eq!(
editor.selected_display_ranges(cx),
editor.selections.display_ranges(cx),
[DisplayPoint::new(12, 6)..DisplayPoint::new(12, 6)]
);
});
@ -995,7 +998,7 @@ mod tests {
// Cursor keeps its position.
view.editor.update(cx, |editor, cx| {
assert_eq!(
editor.selected_display_ranges(cx),
editor.selections.display_ranges(cx),
[DisplayPoint::new(19, 6)..DisplayPoint::new(19, 6)]
);
});

View file

@ -58,9 +58,7 @@ impl DiagnosticIndicator {
fn update(&mut self, editor: ViewHandle<Editor>, cx: &mut ViewContext<Self>) {
let editor = editor.read(cx);
let buffer = editor.buffer().read(cx);
let cursor_position = editor
.newest_selection_with_snapshot::<usize>(&buffer.read(cx))
.head();
let cursor_position = editor.selections.newest::<usize>(cx).head();
let new_diagnostic = buffer
.read(cx)
.diagnostics_in_range::<_, usize>(cursor_position..cursor_position, false)

File diff suppressed because it is too large Load diff

View file

@ -957,8 +957,10 @@ impl Element for EditorElement {
selections.extend(remote_selections);
if view.show_local_selections {
let local_selections =
view.local_selections_in_range(start_anchor..end_anchor, &display_map);
let mut local_selections = view
.selections
.disjoint_in_range(start_anchor..end_anchor, cx);
local_selections.extend(view.selections.pending(cx));
for selection in &local_selections {
let is_empty = selection.start == selection.end;
let selection_start = snapshot.prev_line_boundary(selection.start).1;
@ -1041,7 +1043,8 @@ impl Element for EditorElement {
}
let newest_selection_head = view
.newest_selection_with_snapshot::<usize>(&snapshot.buffer_snapshot)
.selections
.newest::<usize>(cx)
.head()
.to_display_point(&snapshot);

View file

@ -102,7 +102,7 @@ impl FollowableItem for Editor {
} else {
self.buffer.update(cx, |buffer, cx| {
if self.focused {
buffer.set_active_selections(&self.selections, cx);
buffer.set_active_selections(&self.selections.disjoint_anchors(), cx);
}
});
}
@ -118,7 +118,12 @@ impl FollowableItem for Editor {
)),
scroll_x: self.scroll_position.x(),
scroll_y: self.scroll_position.y(),
selections: self.selections.iter().map(serialize_selection).collect(),
selections: self
.selections
.disjoint_anchors()
.iter()
.map(serialize_selection)
.collect(),
}))
}
@ -144,8 +149,9 @@ impl FollowableItem for Editor {
Event::SelectionsChanged { .. } => {
update.selections = self
.selections
.disjoint_anchors()
.iter()
.chain(self.pending_selection.as_ref().map(|p| &p.selection))
.chain(self.selections.pending_anchor().as_ref())
.map(serialize_selection)
.collect();
true
@ -246,13 +252,13 @@ fn deserialize_selection(
impl Item for Editor {
fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool {
if let Ok(data) = data.downcast::<NavigationData>() {
let newest_selection = self.selections.newest::<Point>(cx);
let buffer = self.buffer.read(cx).read(cx);
let offset = if buffer.can_resolve(&data.cursor_anchor) {
data.cursor_anchor.to_point(&buffer)
} else {
buffer.clip_point(data.cursor_position, Bias::Left)
};
let newest_selection = self.newest_selection_with_snapshot::<Point>(&buffer);
let scroll_top_anchor = if buffer.can_resolve(&data.scroll_top_anchor) {
data.scroll_top_anchor
@ -270,7 +276,9 @@ impl Item for Editor {
let nav_history = self.nav_history.take();
self.scroll_position = data.scroll_position;
self.scroll_top_anchor = scroll_top_anchor;
self.select_ranges([offset..offset], Some(Autoscroll::Fit), cx);
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
s.select_ranges([offset..offset])
});
self.nav_history = nav_history;
true
}
@ -307,7 +315,7 @@ impl Item for Editor {
}
fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
let selection = self.newest_anchor_selection();
let selection = self.selections.newest_anchor();
self.push_to_nav_history(selection.head(), None, cx);
}
@ -457,7 +465,7 @@ impl CursorPosition {
self.selected_count = 0;
let mut last_selection: Option<Selection<usize>> = None;
for selection in editor.local_selections::<usize>(cx) {
for selection in editor.selections.all::<usize>(cx) {
self.selected_count += selection.end - selection.start;
if last_selection
.as_ref()

View file

@ -0,0 +1,729 @@
use std::{
cell::Ref,
cmp, iter, mem,
ops::{Deref, Range, Sub},
sync::Arc,
};
use collections::HashMap;
use gpui::{AppContext, ModelHandle, MutableAppContext};
use itertools::Itertools;
use language::{rope::TextDimension, Bias, Point, Selection, SelectionGoal, ToPoint};
use util::post_inc;
use crate::{
display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint},
Anchor, DisplayPoint, ExcerptId, MultiBuffer, MultiBufferSnapshot, SelectMode, ToOffset,
};
#[derive(Clone)]
pub struct PendingSelection {
pub selection: Selection<Anchor>,
pub mode: SelectMode,
}
pub struct SelectionsCollection {
display_map: ModelHandle<DisplayMap>,
buffer: ModelHandle<MultiBuffer>,
pub next_selection_id: usize,
disjoint: Arc<[Selection<Anchor>]>,
pending: Option<PendingSelection>,
}
impl SelectionsCollection {
pub fn new(display_map: ModelHandle<DisplayMap>, buffer: ModelHandle<MultiBuffer>) -> Self {
Self {
display_map,
buffer,
next_selection_id: 1,
disjoint: Arc::from([]),
pending: Some(PendingSelection {
selection: Selection {
id: 0,
start: Anchor::min(),
end: Anchor::min(),
reversed: false,
goal: SelectionGoal::None,
},
mode: SelectMode::Character,
}),
}
}
fn display_map(&self, cx: &mut MutableAppContext) -> DisplaySnapshot {
self.display_map.update(cx, |map, cx| map.snapshot(cx))
}
fn buffer<'a>(&self, cx: &'a AppContext) -> Ref<'a, MultiBufferSnapshot> {
self.buffer.read(cx).read(cx)
}
pub fn count<'a>(&self) -> usize {
let mut count = self.disjoint.len();
if self.pending.is_some() {
count += 1;
}
count
}
pub fn disjoint_anchors(&self) -> Arc<[Selection<Anchor>]> {
self.disjoint.clone()
}
pub fn pending_anchor(&self) -> Option<Selection<Anchor>> {
self.pending
.as_ref()
.map(|pending| pending.selection.clone())
}
pub fn pending<D: TextDimension + Ord + Sub<D, Output = D>>(
&self,
cx: &AppContext,
) -> Option<Selection<D>> {
self.pending_anchor()
.as_ref()
.map(|pending| pending.map(|p| p.summary::<D>(&self.buffer(cx))))
}
pub fn pending_mode(&self) -> Option<SelectMode> {
self.pending.as_ref().map(|pending| pending.mode.clone())
}
pub fn all<'a, D>(&self, cx: &AppContext) -> Vec<Selection<D>>
where
D: 'a + TextDimension + Ord + Sub<D, Output = D> + std::fmt::Debug,
{
let disjoint_anchors = &self.disjoint;
let mut disjoint =
resolve_multiple::<D, _>(disjoint_anchors.iter(), &self.buffer(cx)).peekable();
let mut pending_opt = self.pending::<D>(cx);
iter::from_fn(move || {
if let Some(pending) = pending_opt.as_mut() {
while let Some(next_selection) = disjoint.peek() {
if pending.start <= next_selection.end && pending.end >= next_selection.start {
let next_selection = disjoint.next().unwrap();
if next_selection.start < pending.start {
pending.start = next_selection.start;
}
if next_selection.end > pending.end {
pending.end = next_selection.end;
}
} else if next_selection.end < pending.start {
return disjoint.next();
} else {
break;
}
}
pending_opt.take()
} else {
disjoint.next()
}
})
.collect()
}
pub fn disjoint_in_range<'a, D>(
&self,
range: Range<Anchor>,
cx: &AppContext,
) -> Vec<Selection<D>>
where
D: 'a + TextDimension + Ord + Sub<D, Output = D> + std::fmt::Debug,
{
let buffer = self.buffer(cx);
let start_ix = match self
.disjoint
.binary_search_by(|probe| probe.end.cmp(&range.start, &buffer))
{
Ok(ix) | Err(ix) => ix,
};
let end_ix = match self
.disjoint
.binary_search_by(|probe| probe.start.cmp(&range.end, &buffer))
{
Ok(ix) => ix + 1,
Err(ix) => ix,
};
resolve_multiple(&self.disjoint[start_ix..end_ix], &buffer).collect()
}
pub fn all_display(
&mut self,
cx: &mut MutableAppContext,
) -> (DisplaySnapshot, Vec<Selection<DisplayPoint>>) {
let display_map = self.display_map(cx);
let selections = self
.all::<Point>(cx)
.into_iter()
.map(|selection| selection.map(|point| point.to_display_point(&display_map)))
.collect();
(display_map, selections)
}
pub fn newest_anchor(&self) -> &Selection<Anchor> {
self.pending
.as_ref()
.map(|s| &s.selection)
.or_else(|| self.disjoint.iter().max_by_key(|s| s.id))
.unwrap()
}
pub fn newest<D: TextDimension + Ord + Sub<D, Output = D>>(
&self,
cx: &AppContext,
) -> Selection<D> {
resolve(self.newest_anchor(), &self.buffer(cx))
}
pub fn oldest_anchor(&self) -> &Selection<Anchor> {
self.disjoint
.iter()
.min_by_key(|s| s.id)
.or_else(|| self.pending.as_ref().map(|p| &p.selection))
.unwrap()
}
pub fn oldest<D: TextDimension + Ord + Sub<D, Output = D>>(
&self,
cx: &AppContext,
) -> Selection<D> {
resolve(self.oldest_anchor(), &self.buffer(cx))
}
pub fn first<D: TextDimension + Ord + Sub<D, Output = D>>(
&self,
cx: &AppContext,
) -> Selection<D> {
self.all(cx).first().unwrap().clone()
}
pub fn last<D: TextDimension + Ord + Sub<D, Output = D>>(
&self,
cx: &AppContext,
) -> Selection<D> {
self.all(cx).last().unwrap().clone()
}
#[cfg(any(test, feature = "test-support"))]
pub fn ranges<D: TextDimension + Ord + Sub<D, Output = D> + std::fmt::Debug>(
&self,
cx: &AppContext,
) -> Vec<Range<D>> {
self.all::<D>(cx)
.iter()
.map(|s| {
if s.reversed {
s.end.clone()..s.start.clone()
} else {
s.start.clone()..s.end.clone()
}
})
.collect()
}
#[cfg(any(test, feature = "test-support"))]
pub fn display_ranges(&self, cx: &mut MutableAppContext) -> Vec<Range<DisplayPoint>> {
let display_map = self.display_map(cx);
self.disjoint_anchors()
.iter()
.chain(self.pending_anchor().as_ref())
.map(|s| {
if s.reversed {
s.end.to_display_point(&display_map)..s.start.to_display_point(&display_map)
} else {
s.start.to_display_point(&display_map)..s.end.to_display_point(&display_map)
}
})
.collect()
}
pub fn build_columnar_selection(
&mut self,
display_map: &DisplaySnapshot,
row: u32,
columns: &Range<u32>,
reversed: bool,
) -> Option<Selection<Point>> {
let is_empty = columns.start == columns.end;
let line_len = display_map.line_len(row);
if columns.start < line_len || (is_empty && columns.start == line_len) {
let start = DisplayPoint::new(row, columns.start);
let end = DisplayPoint::new(row, cmp::min(columns.end, line_len));
Some(Selection {
id: post_inc(&mut self.next_selection_id),
start: start.to_point(display_map),
end: end.to_point(display_map),
reversed,
goal: SelectionGoal::ColumnRange {
start: columns.start,
end: columns.end,
},
})
} else {
None
}
}
pub(crate) fn change_with<R>(
&mut self,
cx: &mut MutableAppContext,
change: impl FnOnce(&mut MutableSelectionsCollection) -> R,
) -> R {
let mut mutable_collection = MutableSelectionsCollection {
collection: self,
cx,
};
let result = change(&mut mutable_collection);
assert!(
!mutable_collection.disjoint.is_empty() || mutable_collection.pending.is_some(),
"There must be at least one selection"
);
result
}
}
pub struct MutableSelectionsCollection<'a> {
collection: &'a mut SelectionsCollection,
cx: &'a mut MutableAppContext,
}
impl<'a> MutableSelectionsCollection<'a> {
fn display_map(&mut self) -> DisplaySnapshot {
self.collection.display_map(self.cx)
}
fn buffer(&self) -> Ref<MultiBufferSnapshot> {
self.collection.buffer(self.cx)
}
pub fn clear_disjoint(&mut self) {
self.collection.disjoint = Arc::from([]);
}
pub fn delete(&mut self, selection_id: usize) {
self.collection.disjoint = self
.disjoint
.into_iter()
.filter(|selection| selection.id != selection_id)
.cloned()
.collect();
}
pub fn clear_pending(&mut self) {
self.collection.pending = None;
}
pub fn set_pending_range(&mut self, range: Range<Anchor>, mode: SelectMode) {
self.collection.pending = Some(PendingSelection {
selection: Selection {
id: post_inc(&mut self.collection.next_selection_id),
start: range.start,
end: range.end,
reversed: false,
goal: SelectionGoal::None,
},
mode,
})
}
pub fn set_pending(&mut self, selection: Selection<Anchor>, mode: SelectMode) {
self.collection.pending = Some(PendingSelection { selection, mode });
}
pub fn try_cancel(&mut self) -> bool {
if let Some(pending) = self.collection.pending.take() {
if self.disjoint.is_empty() {
self.collection.disjoint = Arc::from([pending.selection]);
}
return true;
}
let mut oldest = self.oldest_anchor().clone();
if self.count() > 1 {
self.collection.disjoint = Arc::from([oldest]);
return true;
}
if !oldest.start.cmp(&oldest.end, &self.buffer()).is_eq() {
let head = oldest.head();
oldest.start = head.clone();
oldest.end = head;
self.collection.disjoint = Arc::from([oldest]);
return true;
}
return false;
}
pub fn reset_biases(&mut self) {
let buffer = self.buffer.read(self.cx).snapshot(self.cx);
self.collection.disjoint = self
.collection
.disjoint
.into_iter()
.cloned()
.map(|selection| reset_biases(selection, &buffer))
.collect();
if let Some(pending) = self.collection.pending.as_mut() {
pending.selection = reset_biases(pending.selection.clone(), &buffer);
}
}
pub fn insert_range<T>(&mut self, range: Range<T>)
where
T: 'a + ToOffset + ToPoint + TextDimension + Ord + Sub<T, Output = T> + std::marker::Copy,
{
let mut selections = self.all(self.cx);
let mut start = range.start.to_offset(&self.buffer());
let mut end = range.end.to_offset(&self.buffer());
let reversed = if start > end {
mem::swap(&mut start, &mut end);
true
} else {
false
};
selections.push(Selection {
id: post_inc(&mut self.collection.next_selection_id),
start,
end,
reversed,
goal: SelectionGoal::None,
});
self.select(selections);
}
pub fn select<T>(&mut self, mut selections: Vec<Selection<T>>)
where
T: ToOffset + ToPoint + Ord + std::marker::Copy + std::fmt::Debug,
{
let buffer = self.buffer.read(self.cx).snapshot(self.cx);
selections.sort_unstable_by_key(|s| s.start);
// Merge overlapping selections.
let mut i = 1;
while i < selections.len() {
if selections[i - 1].end >= selections[i].start {
let removed = selections.remove(i);
if removed.start < selections[i - 1].start {
selections[i - 1].start = removed.start;
}
if removed.end > selections[i - 1].end {
selections[i - 1].end = removed.end;
}
} else {
i += 1;
}
}
self.collection.disjoint = Arc::from_iter(selections.into_iter().map(|selection| {
let end_bias = if selection.end > selection.start {
Bias::Left
} else {
Bias::Right
};
Selection {
id: selection.id,
start: buffer.anchor_after(selection.start),
end: buffer.anchor_at(selection.end, end_bias),
reversed: selection.reversed,
goal: selection.goal,
}
}));
self.collection.pending = None;
}
pub fn select_anchors(&mut self, selections: Vec<Selection<Anchor>>) {
let buffer = self.buffer.read(self.cx).snapshot(self.cx);
let resolved_selections =
resolve_multiple::<usize, _>(&selections, &buffer).collect::<Vec<_>>();
self.select(resolved_selections);
}
pub fn select_ranges<I, T>(&mut self, ranges: I)
where
I: IntoIterator<Item = Range<T>>,
T: ToOffset,
{
let buffer = self.buffer.read(self.cx).snapshot(self.cx);
let selections = ranges
.into_iter()
.map(|range| {
let mut start = range.start.to_offset(&buffer);
let mut end = range.end.to_offset(&buffer);
let reversed = if start > end {
mem::swap(&mut start, &mut end);
true
} else {
false
};
Selection {
id: post_inc(&mut self.collection.next_selection_id),
start,
end,
reversed,
goal: SelectionGoal::None,
}
})
.collect::<Vec<_>>();
self.select(selections)
}
pub fn select_anchor_ranges<I: IntoIterator<Item = Range<Anchor>>>(&mut self, ranges: I) {
let buffer = self.buffer.read(self.cx).snapshot(self.cx);
let selections = ranges
.into_iter()
.map(|range| {
let mut start = range.start;
let mut end = range.end;
let reversed = if start.cmp(&end, &buffer).is_gt() {
mem::swap(&mut start, &mut end);
true
} else {
false
};
Selection {
id: post_inc(&mut self.collection.next_selection_id),
start,
end,
reversed,
goal: SelectionGoal::None,
}
})
.collect::<Vec<_>>();
self.select_anchors(selections)
}
#[cfg(any(test, feature = "test-support"))]
pub fn select_display_ranges<T>(&mut self, ranges: T)
where
T: IntoIterator<Item = Range<DisplayPoint>>,
{
let display_map = self.display_map();
let selections = ranges
.into_iter()
.map(|range| {
let mut start = range.start;
let mut end = range.end;
let reversed = if start > end {
mem::swap(&mut start, &mut end);
true
} else {
false
};
Selection {
id: post_inc(&mut self.collection.next_selection_id),
start: start.to_point(&display_map),
end: end.to_point(&display_map),
reversed,
goal: SelectionGoal::None,
}
})
.collect();
self.select(selections);
}
pub fn move_with(
&mut self,
mut move_selection: impl FnMut(&DisplaySnapshot, &mut Selection<DisplayPoint>),
) {
let display_map = self.display_map();
let selections = self
.all::<Point>(self.cx)
.into_iter()
.map(|selection| {
let mut selection = selection.map(|point| point.to_display_point(&display_map));
move_selection(&display_map, &mut selection);
selection.map(|display_point| display_point.to_point(&display_map))
})
.collect();
self.select(selections)
}
pub fn move_heads_with(
&mut self,
mut update_head: impl FnMut(
&DisplaySnapshot,
DisplayPoint,
SelectionGoal,
) -> (DisplayPoint, SelectionGoal),
) {
self.move_with(|map, selection| {
let (new_head, new_goal) = update_head(map, selection.head(), selection.goal);
selection.set_head(new_head, new_goal);
});
}
pub fn move_cursors_with(
&mut self,
mut update_cursor_position: impl FnMut(
&DisplaySnapshot,
DisplayPoint,
SelectionGoal,
) -> (DisplayPoint, SelectionGoal),
) {
self.move_with(|map, selection| {
let (cursor, new_goal) = update_cursor_position(map, selection.head(), selection.goal);
selection.collapse_to(cursor, new_goal)
});
}
pub fn replace_cursors_with(
&mut self,
mut find_replacement_cursors: impl FnMut(&DisplaySnapshot) -> Vec<DisplayPoint>,
) {
let display_map = self.display_map();
let new_selections = find_replacement_cursors(&display_map)
.into_iter()
.map(|cursor| {
let cursor_point = cursor.to_point(&display_map);
Selection {
id: post_inc(&mut self.collection.next_selection_id),
start: cursor_point,
end: cursor_point,
reversed: false,
goal: SelectionGoal::None,
}
})
.collect();
self.select(new_selections);
}
/// Compute new ranges for any selections that were located in excerpts that have
/// since been removed.
///
/// Returns a `HashMap` indicating which selections whose former head position
/// was no longer present. The keys of the map are selection ids. The values are
/// the id of the new excerpt where the head of the selection has been moved.
pub fn refresh(&mut self) -> HashMap<usize, ExcerptId> {
let mut pending = self.collection.pending.take();
let mut selections_with_lost_position = HashMap::default();
let anchors_with_status = {
let buffer = self.buffer();
let disjoint_anchors = self
.disjoint
.iter()
.flat_map(|selection| [&selection.start, &selection.end]);
buffer.refresh_anchors(disjoint_anchors)
};
let adjusted_disjoint: Vec<_> = anchors_with_status
.chunks(2)
.map(|selection_anchors| {
let (anchor_ix, start, kept_start) = selection_anchors[0].clone();
let (_, end, kept_end) = selection_anchors[1].clone();
let selection = &self.disjoint[anchor_ix / 2];
let kept_head = if selection.reversed {
kept_start
} else {
kept_end
};
if !kept_head {
selections_with_lost_position
.insert(selection.id, selection.head().excerpt_id.clone());
}
Selection {
id: selection.id,
start,
end,
reversed: selection.reversed,
goal: selection.goal,
}
})
.collect();
if !adjusted_disjoint.is_empty() {
let resolved_selections =
resolve_multiple(adjusted_disjoint.iter(), &self.buffer()).collect();
self.select::<usize>(resolved_selections);
}
if let Some(pending) = pending.as_mut() {
let buffer = self.buffer();
let anchors =
buffer.refresh_anchors([&pending.selection.start, &pending.selection.end]);
let (_, start, kept_start) = anchors[0].clone();
let (_, end, kept_end) = anchors[1].clone();
let kept_head = if pending.selection.reversed {
kept_start
} else {
kept_end
};
if !kept_head {
selections_with_lost_position.insert(
pending.selection.id,
pending.selection.head().excerpt_id.clone(),
);
}
pending.selection.start = start;
pending.selection.end = end;
}
self.collection.pending = pending;
selections_with_lost_position
}
}
impl<'a> Deref for MutableSelectionsCollection<'a> {
type Target = SelectionsCollection;
fn deref(&self) -> &Self::Target {
self.collection
}
}
// Panics if passed selections are not in order
pub fn resolve_multiple<'a, D, I>(
selections: I,
snapshot: &MultiBufferSnapshot,
) -> impl 'a + Iterator<Item = Selection<D>>
where
D: TextDimension + Ord + Sub<D, Output = D> + std::fmt::Debug,
I: 'a + IntoIterator<Item = &'a Selection<Anchor>>,
{
let (to_summarize, selections) = selections.into_iter().tee();
let mut summaries = snapshot
.summaries_for_anchors::<D, _>(
to_summarize
.flat_map(|s| [&s.start, &s.end])
.collect::<Vec<_>>(),
)
.into_iter();
selections.map(move |s| Selection {
id: s.id,
start: summaries.next().unwrap(),
end: summaries.next().unwrap(),
reversed: s.reversed,
goal: s.goal,
})
}
fn resolve<D: TextDimension + Ord + Sub<D, Output = D>>(
selection: &Selection<Anchor>,
buffer: &MultiBufferSnapshot,
) -> Selection<D> {
selection.map(|p| p.summary::<D>(&buffer))
}
fn reset_biases(
mut selection: Selection<Anchor>,
buffer: &MultiBufferSnapshot,
) -> Selection<Anchor> {
let end_bias = if selection.end.to_offset(buffer) > selection.start.to_offset(buffer) {
Bias::Left
} else {
Bias::Right
};
selection.start = buffer.anchor_after(selection.start);
selection.end = buffer.anchor_at(selection.end, end_bias);
selection
}

View file

@ -43,7 +43,7 @@ pub fn marked_display_snapshot(
pub fn select_ranges(editor: &mut Editor, marked_text: &str, cx: &mut ViewContext<Editor>) {
let (umarked_text, text_ranges) = marked_text_ranges(marked_text);
assert_eq!(editor.text(cx), umarked_text);
editor.select_ranges(text_ranges, None, cx);
editor.change_selections(None, cx, |s| s.select_ranges(text_ranges));
}
pub fn assert_text_with_selections(
@ -54,5 +54,5 @@ pub fn assert_text_with_selections(
let (unmarked_text, text_ranges) = marked_text_ranges(marked_text);
assert_eq!(editor.text(cx), unmarked_text);
assert_eq!(editor.selected_ranges(cx), text_ranges);
assert_eq!(editor.selections.ranges(cx), text_ranges);
}

View file

@ -43,7 +43,7 @@ impl GoToLine {
let buffer = editor.buffer().read(cx).read(cx);
(
Some(scroll_position),
editor.newest_selection_with_snapshot(&buffer).head(),
editor.selections.newest(cx).head(),
buffer.max_point(),
)
});
@ -80,7 +80,9 @@ impl GoToLine {
if let Some(rows) = active_editor.highlighted_rows() {
let snapshot = active_editor.snapshot(cx).display_snapshot;
let position = DisplayPoint::new(rows.start, 0).to_point(&snapshot);
active_editor.select_ranges([position..position], Some(Autoscroll::Center), cx);
active_editor.change_selections(Some(Autoscroll::Center), cx, |s| {
s.select_ranges([position..position])
});
}
});
cx.emit(Event::Dismissed);

View file

@ -57,7 +57,9 @@ pub fn new_journal_entry(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
if let Some(editor) = item.downcast::<Editor>() {
editor.update(&mut cx, |editor, cx| {
let len = editor.buffer().read(cx).read(cx).len();
editor.select_ranges([len..len], Some(Autoscroll::Center), cx);
editor.change_selections(Some(Autoscroll::Center), cx, |s| {
s.select_ranges([len..len])
});
if len > 0 {
editor.insert("\n\n", cx);
}

View file

@ -172,9 +172,7 @@ impl PickerDelegate for OutlineView {
let editor = self.active_editor.read(cx);
let buffer = editor.buffer().read(cx).read(cx);
let cursor_offset = editor
.newest_selection_with_snapshot::<usize>(&buffer)
.head();
let cursor_offset = editor.selections.newest::<usize>(cx).head();
selected_index = self
.outline
.items
@ -217,7 +215,9 @@ impl PickerDelegate for OutlineView {
if let Some(rows) = active_editor.highlighted_rows() {
let snapshot = active_editor.snapshot(cx).display_snapshot;
let position = DisplayPoint::new(rows.start, 0).to_point(&snapshot);
active_editor.select_ranges([position..position], Some(Autoscroll::Center), cx);
active_editor.change_selections(Some(Autoscroll::Center), cx, |s| {
s.select_ranges([position..position])
});
}
});
cx.emit(Event::Dismissed);

View file

@ -145,11 +145,9 @@ impl ProjectSymbolsView {
let editor = workspace.open_project_item::<Editor>(buffer, cx);
editor.update(cx, |editor, cx| {
editor.select_ranges(
[position..position],
Some(Autoscroll::Center),
cx,
);
editor.change_selections(Some(Autoscroll::Center), cx, |s| {
s.select_ranges([position..position])
});
});
});
Ok::<_, anyhow::Error>(())

View file

@ -225,9 +225,7 @@ impl BufferSearchBar {
let display_map = editor
.update(cx, |editor, cx| editor.snapshot(cx))
.display_snapshot;
let selection = editor
.read(cx)
.newest_selection_with_snapshot::<usize>(&display_map.buffer_snapshot);
let selection = editor.read(cx).selections.newest::<usize>(cx);
let mut text: String;
if selection.start == selection.end {
@ -387,14 +385,16 @@ impl BufferSearchBar {
if let Some(ranges) = self.editors_with_matches.get(&cx.weak_handle()) {
let new_index = match_index_for_direction(
ranges,
&editor.newest_anchor_selection().head(),
&editor.selections.newest_anchor().head(),
index,
direction,
&editor.buffer().read(cx).read(cx),
);
let range_to_select = ranges[new_index].clone();
editor.unfold_ranges([range_to_select.clone()], false, cx);
editor.select_ranges([range_to_select], Some(Autoscroll::Fit), cx);
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
s.select_ranges([range_to_select])
});
}
});
}
@ -535,10 +535,10 @@ impl BufferSearchBar {
editor.update(cx, |editor, cx| {
if select_closest_match {
if let Some(match_ix) = this.active_match_index {
editor.select_ranges(
[ranges[match_ix].clone()],
editor.change_selections(
Some(Autoscroll::Fit),
cx,
|s| s.select_ranges([ranges[match_ix].clone()]),
);
}
}
@ -564,7 +564,7 @@ impl BufferSearchBar {
let editor = editor.read(cx);
active_match_index(
&ranges,
&editor.newest_anchor_selection().head(),
&editor.selections.newest_anchor().head(),
&editor.buffer().read(cx).read(cx),
)
});
@ -721,13 +721,15 @@ mod tests {
});
editor.update(cx, |editor, cx| {
editor.select_display_ranges(&[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)], cx);
editor.change_selections(None, cx, |s| {
s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)])
});
});
search_bar.update(cx, |search_bar, cx| {
assert_eq!(search_bar.active_match_index, Some(0));
search_bar.select_next_match(&SelectNextMatch, cx);
assert_eq!(
editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
[DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
);
});
@ -738,7 +740,7 @@ mod tests {
search_bar.update(cx, |search_bar, cx| {
search_bar.select_next_match(&SelectNextMatch, cx);
assert_eq!(
editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
[DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
);
});
@ -749,7 +751,7 @@ mod tests {
search_bar.update(cx, |search_bar, cx| {
search_bar.select_next_match(&SelectNextMatch, cx);
assert_eq!(
editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
[DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
);
});
@ -760,7 +762,7 @@ mod tests {
search_bar.update(cx, |search_bar, cx| {
search_bar.select_next_match(&SelectNextMatch, cx);
assert_eq!(
editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
[DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
);
});
@ -771,7 +773,7 @@ mod tests {
search_bar.update(cx, |search_bar, cx| {
search_bar.select_prev_match(&SelectPrevMatch, cx);
assert_eq!(
editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
[DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
);
});
@ -782,7 +784,7 @@ mod tests {
search_bar.update(cx, |search_bar, cx| {
search_bar.select_prev_match(&SelectPrevMatch, cx);
assert_eq!(
editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
[DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
);
});
@ -793,7 +795,7 @@ mod tests {
search_bar.update(cx, |search_bar, cx| {
search_bar.select_prev_match(&SelectPrevMatch, cx);
assert_eq!(
editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
[DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
);
});
@ -804,13 +806,15 @@ mod tests {
// Park the cursor in between matches and ensure that going to the previous match selects
// the closest match to the left.
editor.update(cx, |editor, cx| {
editor.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx);
editor.change_selections(None, cx, |s| {
s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
});
});
search_bar.update(cx, |search_bar, cx| {
assert_eq!(search_bar.active_match_index, Some(1));
search_bar.select_prev_match(&SelectPrevMatch, cx);
assert_eq!(
editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
[DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
);
});
@ -821,13 +825,15 @@ mod tests {
// Park the cursor in between matches and ensure that going to the next match selects the
// closest match to the right.
editor.update(cx, |editor, cx| {
editor.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx);
editor.change_selections(None, cx, |s| {
s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
});
});
search_bar.update(cx, |search_bar, cx| {
assert_eq!(search_bar.active_match_index, Some(1));
search_bar.select_next_match(&SelectNextMatch, cx);
assert_eq!(
editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
[DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
);
});
@ -838,13 +844,15 @@ mod tests {
// Park the cursor after the last match and ensure that going to the previous match selects
// the last match.
editor.update(cx, |editor, cx| {
editor.select_display_ranges(&[DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)], cx);
editor.change_selections(None, cx, |s| {
s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)])
});
});
search_bar.update(cx, |search_bar, cx| {
assert_eq!(search_bar.active_match_index, Some(2));
search_bar.select_prev_match(&SelectPrevMatch, cx);
assert_eq!(
editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
[DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
);
});
@ -855,13 +863,15 @@ mod tests {
// Park the cursor after the last match and ensure that going to the next match selects the
// first match.
editor.update(cx, |editor, cx| {
editor.select_display_ranges(&[DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)], cx);
editor.change_selections(None, cx, |s| {
s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)])
});
});
search_bar.update(cx, |search_bar, cx| {
assert_eq!(search_bar.active_match_index, Some(2));
search_bar.select_next_match(&SelectNextMatch, cx);
assert_eq!(
editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
[DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
);
});
@ -872,13 +882,15 @@ mod tests {
// Park the cursor before the first match and ensure that going to the previous match
// selects the last match.
editor.update(cx, |editor, cx| {
editor.select_display_ranges(&[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)], cx);
editor.change_selections(None, cx, |s| {
s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)])
});
});
search_bar.update(cx, |search_bar, cx| {
assert_eq!(search_bar.active_match_index, Some(0));
search_bar.select_prev_match(&SelectPrevMatch, cx);
assert_eq!(
editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
[DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
);
});

View file

@ -454,7 +454,7 @@ impl ProjectSearchView {
let results_editor = self.results_editor.read(cx);
let new_index = match_index_for_direction(
&model.match_ranges,
&results_editor.newest_anchor_selection().head(),
&results_editor.selections.newest_anchor().head(),
index,
direction,
&results_editor.buffer().read(cx).read(cx),
@ -462,7 +462,9 @@ impl ProjectSearchView {
let range_to_select = model.match_ranges[new_index].clone();
self.results_editor.update(cx, |editor, cx| {
editor.unfold_ranges([range_to_select.clone()], false, cx);
editor.select_ranges([range_to_select], Some(Autoscroll::Fit), cx);
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
s.select_ranges([range_to_select])
});
});
}
}
@ -476,8 +478,8 @@ impl ProjectSearchView {
fn focus_results_editor(&self, cx: &mut ViewContext<Self>) {
self.query_editor.update(cx, |query_editor, cx| {
let cursor = query_editor.newest_anchor_selection().head();
query_editor.select_ranges([cursor.clone()..cursor], None, cx);
let cursor = query_editor.selections.newest_anchor().head();
query_editor.change_selections(None, cx, |s| s.select_ranges([cursor.clone()..cursor]));
});
cx.focus(&self.results_editor);
}
@ -489,7 +491,9 @@ impl ProjectSearchView {
} else {
self.results_editor.update(cx, |editor, cx| {
if reset_selections {
editor.select_ranges(match_ranges.first().cloned(), Some(Autoscroll::Fit), cx);
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
s.select_ranges(match_ranges.first().cloned())
});
}
editor.highlight_background::<Self>(
match_ranges,
@ -510,7 +514,7 @@ impl ProjectSearchView {
let results_editor = self.results_editor.read(cx);
let new_index = active_match_index(
&self.model.read(cx).match_ranges,
&results_editor.newest_anchor_selection().head(),
&results_editor.selections.newest_anchor().head(),
&results_editor.buffer().read(cx).read(cx),
);
if self.active_match_index != new_index {
@ -887,7 +891,7 @@ mod tests {
assert_eq!(
search_view
.results_editor
.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
[DisplayPoint::new(2, 32)..DisplayPoint::new(2, 35)]
);
@ -899,7 +903,7 @@ mod tests {
assert_eq!(
search_view
.results_editor
.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
[DisplayPoint::new(2, 37)..DisplayPoint::new(2, 40)]
);
search_view.select_match(Direction::Next, cx);
@ -910,7 +914,7 @@ mod tests {
assert_eq!(
search_view
.results_editor
.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
[DisplayPoint::new(5, 6)..DisplayPoint::new(5, 9)]
);
search_view.select_match(Direction::Next, cx);
@ -921,7 +925,7 @@ mod tests {
assert_eq!(
search_view
.results_editor
.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
[DisplayPoint::new(2, 32)..DisplayPoint::new(2, 35)]
);
search_view.select_match(Direction::Prev, cx);
@ -932,7 +936,7 @@ mod tests {
assert_eq!(
search_view
.results_editor
.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
[DisplayPoint::new(5, 6)..DisplayPoint::new(5, 9)]
);
search_view.select_match(Direction::Prev, cx);
@ -943,7 +947,7 @@ mod tests {
assert_eq!(
search_view
.results_editor
.update(cx, |editor, cx| editor.selected_display_ranges(cx)),
.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
[DisplayPoint::new(2, 37)..DisplayPoint::new(2, 40)]
);
});

View file

@ -1,5 +1,5 @@
use crate::{state::Mode, Vim};
use editor::Bias;
use editor::{Autoscroll, Bias};
use gpui::{actions, MutableAppContext, ViewContext};
use language::SelectionGoal;
use workspace::Workspace;
@ -13,9 +13,11 @@ pub fn init(cx: &mut MutableAppContext) {
fn normal_before(_: &mut Workspace, _: &NormalBefore, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |state, cx| {
state.update_active_editor(cx, |editor, cx| {
editor.move_cursors(cx, |map, mut cursor, _| {
*cursor.column_mut() = cursor.column().saturating_sub(1);
(map.clip_point(cursor, Bias::Left), SelectionGoal::None)
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
s.move_cursors_with(|map, mut cursor, _| {
*cursor.column_mut() = cursor.column().saturating_sub(1);
(map.clip_point(cursor, Bias::Left), SelectionGoal::None)
});
});
});
state.switch_mode(Mode::Normal, cx);

View file

@ -8,7 +8,7 @@ use crate::{
};
use change::init as change_init;
use collections::HashSet;
use editor::{Bias, DisplayPoint};
use editor::{Autoscroll, Bias, DisplayPoint};
use gpui::{actions, MutableAppContext, ViewContext};
use language::SelectionGoal;
use workspace::Workspace;
@ -76,7 +76,9 @@ pub fn normal_motion(motion: Motion, cx: &mut MutableAppContext) {
fn move_cursor(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
vim.update_active_editor(cx, |editor, cx| {
editor.move_cursors(cx, |map, cursor, goal| motion.move_point(map, cursor, goal))
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
s.move_cursors_with(|map, cursor, goal| motion.move_point(map, cursor, goal))
})
});
}
@ -84,8 +86,10 @@ fn insert_after(_: &mut Workspace, _: &InsertAfter, cx: &mut ViewContext<Workspa
Vim::update(cx, |vim, cx| {
vim.switch_mode(Mode::Insert, cx);
vim.update_active_editor(cx, |editor, cx| {
editor.move_cursors(cx, |map, cursor, goal| {
Motion::Right.move_point(map, cursor, goal)
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
s.move_cursors_with(|map, cursor, goal| {
Motion::Right.move_point(map, cursor, goal)
});
});
});
});
@ -99,8 +103,10 @@ fn insert_first_non_whitespace(
Vim::update(cx, |vim, cx| {
vim.switch_mode(Mode::Insert, cx);
vim.update_active_editor(cx, |editor, cx| {
editor.move_cursors(cx, |map, cursor, goal| {
Motion::FirstNonWhitespace.move_point(map, cursor, goal)
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
s.move_cursors_with(|map, cursor, goal| {
Motion::FirstNonWhitespace.move_point(map, cursor, goal)
});
});
});
});
@ -110,8 +116,10 @@ fn insert_end_of_line(_: &mut Workspace, _: &InsertEndOfLine, cx: &mut ViewConte
Vim::update(cx, |vim, cx| {
vim.switch_mode(Mode::Insert, cx);
vim.update_active_editor(cx, |editor, cx| {
editor.move_cursors(cx, |map, cursor, goal| {
Motion::EndOfLine.move_point(map, cursor, goal)
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
s.move_cursors_with(|map, cursor, goal| {
Motion::EndOfLine.move_point(map, cursor, goal)
});
});
});
});
@ -122,7 +130,7 @@ fn insert_line_above(_: &mut Workspace, _: &InsertLineAbove, cx: &mut ViewContex
vim.switch_mode(Mode::Insert, cx);
vim.update_active_editor(cx, |editor, cx| {
editor.transact(cx, |editor, cx| {
let (map, old_selections) = editor.display_selections(cx);
let (map, old_selections) = editor.selections.all_display(cx);
let selection_start_rows: HashSet<u32> = old_selections
.into_iter()
.map(|selection| selection.start.row())
@ -137,10 +145,12 @@ fn insert_line_above(_: &mut Workspace, _: &InsertLineAbove, cx: &mut ViewContex
(start_of_line..start_of_line, new_text)
});
editor.edit_with_autoindent(edits, cx);
editor.move_cursors(cx, |map, mut cursor, _| {
*cursor.row_mut() -= 1;
*cursor.column_mut() = map.line_len(cursor.row());
(map.clip_point(cursor, Bias::Left), SelectionGoal::None)
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
s.move_cursors_with(|map, mut cursor, _| {
*cursor.row_mut() -= 1;
*cursor.column_mut() = map.line_len(cursor.row());
(map.clip_point(cursor, Bias::Left), SelectionGoal::None)
});
});
});
});
@ -152,7 +162,7 @@ fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContex
vim.switch_mode(Mode::Insert, cx);
vim.update_active_editor(cx, |editor, cx| {
editor.transact(cx, |editor, cx| {
let (map, old_selections) = editor.display_selections(cx);
let (map, old_selections) = editor.selections.all_display(cx);
let selection_end_rows: HashSet<u32> = old_selections
.into_iter()
.map(|selection| selection.end.row())
@ -166,8 +176,10 @@ fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContex
new_text.push_str(&" ".repeat(indent as usize));
(end_of_line..end_of_line, new_text)
});
editor.move_cursors(cx, |map, cursor, goal| {
Motion::EndOfLine.move_point(map, cursor, goal)
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
s.move_cursors_with(|map, cursor, goal| {
Motion::EndOfLine.move_point(map, cursor, goal)
});
});
editor.edit_with_autoindent(edits, cx);
});

View file

@ -1,5 +1,5 @@
use crate::{motion::Motion, state::Mode, Vim};
use editor::{char_kind, movement};
use editor::{char_kind, movement, Autoscroll};
use gpui::{impl_actions, MutableAppContext, ViewContext};
use serde::Deserialize;
use workspace::Workspace;
@ -22,8 +22,10 @@ pub fn change_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
editor.transact(cx, |editor, cx| {
// We are swapping to insert mode anyway. Just set the line end clipping behavior now
editor.set_clip_at_line_ends(false, cx);
editor.move_selections(cx, |map, selection| {
motion.expand_selection(map, selection, false);
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
s.move_with(|map, selection| {
motion.expand_selection(map, selection, false);
});
});
editor.insert(&"", cx);
});
@ -46,16 +48,21 @@ fn change_word(
editor.transact(cx, |editor, cx| {
// We are swapping to insert mode anyway. Just set the line end clipping behavior now
editor.set_clip_at_line_ends(false, cx);
editor.move_selections(cx, |map, selection| {
if selection.end.column() == map.line_len(selection.end.row()) {
return;
}
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
s.move_with(|map, selection| {
if selection.end.column() == map.line_len(selection.end.row()) {
return;
}
selection.end = movement::find_boundary(map, selection.end, |left, right| {
let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
selection.end =
movement::find_boundary(map, selection.end, |left, right| {
let left_kind =
char_kind(left).coerce_punctuation(ignore_punctuation);
let right_kind =
char_kind(right).coerce_punctuation(ignore_punctuation);
left_kind != right_kind || left == '\n' || right == '\n'
left_kind != right_kind || left == '\n' || right == '\n'
});
});
});
editor.insert(&"", cx);

View file

@ -1,6 +1,6 @@
use crate::{motion::Motion, Vim};
use collections::HashMap;
use editor::Bias;
use editor::{Autoscroll, Bias};
use gpui::MutableAppContext;
pub fn delete_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
@ -8,24 +8,28 @@ pub fn delete_over(vim: &mut Vim, motion: Motion, cx: &mut MutableAppContext) {
editor.transact(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, cx);
let mut original_columns: HashMap<_, _> = Default::default();
editor.move_selections(cx, |map, selection| {
let original_head = selection.head();
motion.expand_selection(map, selection, true);
original_columns.insert(selection.id, original_head.column());
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
s.move_with(|map, selection| {
let original_head = selection.head();
motion.expand_selection(map, selection, true);
original_columns.insert(selection.id, original_head.column());
});
});
editor.insert(&"", cx);
// Fixup cursor position after the deletion
editor.set_clip_at_line_ends(true, cx);
editor.move_selections(cx, |map, selection| {
let mut cursor = selection.head();
if motion.linewise() {
if let Some(column) = original_columns.get(&selection.id) {
*cursor.column_mut() = *column
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
s.move_with(|map, selection| {
let mut cursor = selection.head();
if motion.linewise() {
if let Some(column) = original_columns.get(&selection.id) {
*cursor.column_mut() = *column
}
}
}
cursor = map.clip_point(cursor, Bias::Left);
selection.collapse_to(cursor, selection.goal)
cursor = map.clip_point(cursor, Bias::Left);
selection.collapse_to(cursor, selection.goal)
});
});
});
});

View file

@ -3,7 +3,7 @@ use std::ops::{Deref, Range};
use collections::BTreeMap;
use itertools::{Either, Itertools};
use editor::display_map::ToDisplayPoint;
use editor::{display_map::ToDisplayPoint, Autoscroll};
use gpui::{json::json, keymap::Keystroke, ViewHandle};
use indoc::indoc;
use language::Selection;
@ -128,7 +128,9 @@ impl<'a> VimTestContext<'a> {
let (unmarked_text, markers) = marked_text(&text);
editor.set_text(unmarked_text, cx);
let cursor_offset = markers[0];
editor.replace_selections_with(cx, |map| cursor_offset.to_display_point(map));
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
s.replace_cursors_with(|map| vec![cursor_offset.to_display_point(map)])
});
})
}
@ -197,7 +199,8 @@ impl<'a> VimTestContext<'a> {
let (empty_selections, reverse_selections, forward_selections) =
self.editor.read_with(self.cx, |editor, cx| {
let (empty_selections, non_empty_selections): (Vec<_>, Vec<_>) = editor
.local_selections::<usize>(cx)
.selections
.all::<usize>(cx)
.into_iter()
.partition_map(|selection| {
if selection.is_empty() {

View file

@ -1,4 +1,4 @@
use editor::Bias;
use editor::{Autoscroll, Bias};
use gpui::{actions, MutableAppContext, ViewContext};
use workspace::Workspace;
@ -14,23 +14,25 @@ pub fn init(cx: &mut MutableAppContext) {
pub fn visual_motion(motion: Motion, cx: &mut MutableAppContext) {
Vim::update(cx, |vim, cx| {
vim.update_active_editor(cx, |editor, cx| {
editor.move_selections(cx, |map, selection| {
let (new_head, goal) = motion.move_point(map, selection.head(), selection.goal);
let new_head = map.clip_at_line_end(new_head);
let was_reversed = selection.reversed;
selection.set_head(new_head, goal);
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
s.move_with(|map, selection| {
let (new_head, goal) = motion.move_point(map, selection.head(), selection.goal);
let new_head = map.clip_at_line_end(new_head);
let was_reversed = selection.reversed;
selection.set_head(new_head, goal);
if was_reversed && !selection.reversed {
// Head was at the start of the selection, and now is at the end. We need to move the start
// back by one if possible in order to compensate for this change.
*selection.start.column_mut() = selection.start.column().saturating_sub(1);
selection.start = map.clip_point(selection.start, Bias::Left);
} else if !was_reversed && selection.reversed {
// Head was at the end of the selection, and now is at the start. We need to move the end
// forward by one if possible in order to compensate for this change.
*selection.end.column_mut() = selection.end.column() + 1;
selection.end = map.clip_point(selection.end, Bias::Left);
}
if was_reversed && !selection.reversed {
// Head was at the start of the selection, and now is at the end. We need to move the start
// back by one if possible in order to compensate for this change.
*selection.start.column_mut() = selection.start.column().saturating_sub(1);
selection.start = map.clip_point(selection.start, Bias::Left);
} else if !was_reversed && selection.reversed {
// Head was at the end of the selection, and now is at the start. We need to move the end
// forward by one if possible in order to compensate for this change.
*selection.end.column_mut() = selection.end.column() + 1;
selection.end = map.clip_point(selection.end, Bias::Left);
}
});
});
});
});
@ -40,13 +42,15 @@ pub fn change(_: &mut Workspace, _: &VisualChange, cx: &mut ViewContext<Workspac
Vim::update(cx, |vim, cx| {
vim.update_active_editor(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, cx);
editor.move_selections(cx, |map, selection| {
if !selection.reversed {
// Head was at the end of the selection, and now is at the start. We need to move the end
// forward by one if possible in order to compensate for this change.
*selection.end.column_mut() = selection.end.column() + 1;
selection.end = map.clip_point(selection.end, Bias::Left);
}
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
s.move_with(|map, selection| {
if !selection.reversed {
// Head was at the end of the selection, and now is at the start. We need to move the end
// forward by one if possible in order to compensate for this change.
*selection.end.column_mut() = selection.end.column() + 1;
selection.end = map.clip_point(selection.end, Bias::Left);
}
});
});
editor.insert("", cx);
});
@ -59,22 +63,26 @@ pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext<Workspac
vim.switch_mode(Mode::Normal, cx);
vim.update_active_editor(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, cx);
editor.move_selections(cx, |map, selection| {
if !selection.reversed {
// Head was at the end of the selection, and now is at the start. We need to move the end
// forward by one if possible in order to compensate for this change.
*selection.end.column_mut() = selection.end.column() + 1;
selection.end = map.clip_point(selection.end, Bias::Left);
}
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
s.move_with(|map, selection| {
if !selection.reversed {
// Head was at the end of the selection, and now is at the start. We need to move the end
// forward by one if possible in order to compensate for this change.
*selection.end.column_mut() = selection.end.column() + 1;
selection.end = map.clip_point(selection.end, Bias::Left);
}
});
});
editor.insert("", cx);
// Fixup cursor position after the deletion
editor.set_clip_at_line_ends(true, cx);
editor.move_selections(cx, |map, selection| {
let mut cursor = selection.head();
cursor = map.clip_point(cursor, Bias::Left);
selection.collapse_to(cursor, selection.goal)
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
s.move_with(|map, selection| {
let mut cursor = selection.head();
cursor = map.clip_point(cursor, Bias::Left);
selection.collapse_to(cursor, selection.goal)
});
});
});
});

View file

@ -310,7 +310,7 @@ fn open_config_file(
mod tests {
use super::*;
use assets::Assets;
use editor::{DisplayPoint, Editor};
use editor::{Autoscroll, DisplayPoint, Editor};
use gpui::{AssetSource, MutableAppContext, TestAppContext, ViewHandle};
use project::{Fs, ProjectPath};
use serde_json::json;
@ -963,7 +963,9 @@ mod tests {
.downcast::<Editor>()
.unwrap();
editor1.update(cx, |editor, cx| {
editor.select_display_ranges(&[DisplayPoint::new(10, 0)..DisplayPoint::new(10, 0)], cx);
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
s.select_display_ranges([DisplayPoint::new(10, 0)..DisplayPoint::new(10, 0)])
});
});
let editor2 = workspace
.update(cx, |w, cx| w.open_path(file2.clone(), true, cx))
@ -980,10 +982,9 @@ mod tests {
editor3
.update(cx, |editor, cx| {
editor.select_display_ranges(
&[DisplayPoint::new(12, 0)..DisplayPoint::new(12, 0)],
cx,
);
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
s.select_display_ranges([DisplayPoint::new(12, 0)..DisplayPoint::new(12, 0)])
});
editor.newline(&Default::default(), cx);
editor.newline(&Default::default(), cx);
editor.move_down(&Default::default(), cx);
@ -1124,34 +1125,37 @@ mod tests {
// Modify file to collapse multiple nav history entries into the same location.
// Ensure we don't visit the same location twice when navigating.
editor1.update(cx, |editor, cx| {
editor.select_display_ranges(&[DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)], cx)
editor.change_selections(None, cx, |s| {
s.select_display_ranges([DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)])
})
});
for _ in 0..5 {
editor1.update(cx, |editor, cx| {
editor
.select_display_ranges(&[DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)], cx);
editor.change_selections(None, cx, |s| {
s.select_display_ranges([DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)])
});
});
editor1.update(cx, |editor, cx| {
editor.select_display_ranges(
&[DisplayPoint::new(13, 0)..DisplayPoint::new(13, 0)],
cx,
)
editor.change_selections(None, cx, |s| {
s.select_display_ranges([DisplayPoint::new(13, 0)..DisplayPoint::new(13, 0)])
})
});
}
editor1.update(cx, |editor, cx| {
editor.transact(cx, |editor, cx| {
editor.select_display_ranges(
&[DisplayPoint::new(2, 0)..DisplayPoint::new(14, 0)],
cx,
);
editor.change_selections(None, cx, |s| {
s.select_display_ranges([DisplayPoint::new(2, 0)..DisplayPoint::new(14, 0)])
});
editor.insert("", cx);
})
});
editor1.update(cx, |editor, cx| {
editor.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx)
editor.change_selections(None, cx, |s| {
s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
})
});
workspace
.update(cx, |w, cx| Pane::go_back(w, None, cx))
@ -1177,7 +1181,7 @@ mod tests {
let editor = item.downcast::<Editor>().unwrap();
let (selections, scroll_position) = editor.update(cx, |editor, cx| {
(
editor.selected_display_ranges(cx),
editor.selections.display_ranges(cx),
editor.scroll_position(cx),
)
});