Implement word-wise mouse selection

Co-Authored-By: Nathan Sobo <nathan@zed.dev>
This commit is contained in:
Antonio Scandurra 2021-11-23 18:50:17 +01:00
parent 3269b9925f
commit bcf38e6bb5
6 changed files with 77 additions and 51 deletions

View File

@ -1,6 +1,6 @@
use super::{ use super::{
DisplayPoint, DisplayRow, Editor, EditorMode, EditorSettings, EditorStyle, Input, Scroll, DisplayPoint, DisplayRow, Editor, EditorMode, EditorSettings, EditorStyle, Input, Scroll,
Select, SelectMode, SelectPhase, Snapshot, MAX_LINE_LEN, Select, SelectPhase, Snapshot, MAX_LINE_LEN,
}; };
use clock::ReplicaId; use clock::ReplicaId;
use gpui::{ use gpui::{
@ -56,7 +56,7 @@ impl EditorElement {
&self, &self,
position: Vector2F, position: Vector2F,
cmd: bool, cmd: bool,
count: usize, click_count: usize,
layout: &mut LayoutState, layout: &mut LayoutState,
paint: &mut PaintState, paint: &mut PaintState,
cx: &mut EventContext, cx: &mut EventContext,
@ -64,16 +64,10 @@ impl EditorElement {
if paint.text_bounds.contains_point(position) { if paint.text_bounds.contains_point(position) {
let snapshot = self.snapshot(cx.app); let snapshot = self.snapshot(cx.app);
let position = paint.point_for_position(&snapshot, layout, position); let position = paint.point_for_position(&snapshot, layout, position);
let mode = match count {
1 => SelectMode::Character,
2 => SelectMode::Word,
3 => SelectMode::Line,
_ => SelectMode::All,
};
cx.dispatch_action(Select(SelectPhase::Begin { cx.dispatch_action(Select(SelectPhase::Begin {
position, position,
add: cmd, add: cmd,
mode, click_count,
})); }));
true true
} else { } else {
@ -855,8 +849,8 @@ impl Element for EditorElement {
Event::LeftMouseDown { Event::LeftMouseDown {
position, position,
cmd, cmd,
count, click_count,
} => self.mouse_down(*position, *cmd, *count, layout, paint, cx), } => self.mouse_down(*position, *cmd, *click_count, layout, paint, cx),
Event::LeftMouseUp { position } => self.mouse_up(*position, cx), Event::LeftMouseUp { position } => self.mouse_up(*position, cx),
Event::LeftMouseDragged { position } => { Event::LeftMouseDragged { position } => {
self.mouse_dragged(*position, layout, paint, cx) self.mouse_dragged(*position, layout, paint, cx)

View File

@ -274,7 +274,7 @@ pub enum SelectPhase {
Begin { Begin {
position: DisplayPoint, position: DisplayPoint,
add: bool, add: bool,
mode: SelectMode, click_count: usize,
}, },
Update { Update {
position: DisplayPoint, position: DisplayPoint,
@ -283,11 +283,11 @@ pub enum SelectPhase {
End, End,
} }
#[derive(Copy, Clone, Debug)] #[derive(Clone, Debug)]
pub enum SelectMode { enum SelectMode {
Character, Character,
Word, Word(Range<Anchor>),
Line, Line(Range<Anchor>),
All, All,
} }
@ -644,8 +644,8 @@ impl Editor {
SelectPhase::Begin { SelectPhase::Begin {
position, position,
add, add,
mode, click_count,
} => self.begin_selection(*position, *add, *mode, cx), } => self.begin_selection(*position, *add, *click_count, cx),
SelectPhase::Update { SelectPhase::Update {
position, position,
scroll_position, scroll_position,
@ -658,7 +658,7 @@ impl Editor {
&mut self, &mut self,
position: DisplayPoint, position: DisplayPoint,
add: bool, add: bool,
mode: SelectMode, click_count: usize,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) { ) {
if !self.focused { if !self.focused {
@ -670,20 +670,24 @@ impl Editor {
let buffer = self.buffer.read(cx); let buffer = self.buffer.read(cx);
let start; let start;
let end; let end;
match mode { let mode;
SelectMode::Character => { match click_count {
1 => {
start = buffer.anchor_before(position.to_point(&display_map)); start = buffer.anchor_before(position.to_point(&display_map));
end = start.clone(); end = start.clone();
mode = SelectMode::Character;
} }
SelectMode::Word => { 2 => {
let range = movement::surrounding_word(&display_map, position); let range = movement::surrounding_word(&display_map, position);
start = buffer.anchor_before(range.start.to_point(&display_map)); start = buffer.anchor_before(range.start.to_point(&display_map));
end = buffer.anchor_before(range.end.to_point(&display_map)); end = buffer.anchor_before(range.end.to_point(&display_map));
mode = SelectMode::Word(start.clone()..end.clone());
} }
SelectMode::Line => todo!(), 3 => todo!(),
SelectMode::All => { _ => {
start = buffer.anchor_before(0); start = buffer.anchor_before(0);
end = buffer.anchor_before(buffer.len()); end = buffer.anchor_before(buffer.len());
mode = SelectMode::All;
} }
} }
let selection = Selection { let selection = Selection {
@ -711,32 +715,51 @@ impl Editor {
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
if let Some(PendingSelection { selection, mode }) = self.pending_selection.as_mut() { if let Some(PendingSelection { selection, mode }) = self.pending_selection.as_mut() {
let buffer = self.buffer.read(cx); let buffer = self.buffer.read(cx);
let cursor = match mode { let head;
SelectMode::Character => buffer.anchor_before(position.to_point(&display_map)), let tail;
SelectMode::Word => { match mode {
let word_range = movement::surrounding_word(&display_map, position); SelectMode::Character => {
if word_range.start < selection.start.to_display_point(&display_map) { head = position.to_point(&display_map);
buffer.anchor_before(word_range.start.to_point(&display_map)) tail = selection.tail().to_point(buffer);
}
SelectMode::Word(original_range) => {
let original_display_range = original_range.start.to_display_point(&display_map)
..original_range.end.to_display_point(&display_map);
let original_buffer_range = original_display_range.start.to_point(&display_map)
..original_display_range.end.to_point(&display_map);
if movement::is_inside_word(&display_map, position)
|| original_display_range.contains(&position)
{
let word_range = movement::surrounding_word(&display_map, position);
if word_range.start < original_display_range.start {
head = word_range.start.to_point(&display_map);
} else {
head = word_range.end.to_point(&display_map);
}
} else { } else {
buffer.anchor_before(word_range.end.to_point(&display_map)) head = position.to_point(&display_map);
}
if head <= original_buffer_range.start {
tail = original_buffer_range.end;
} else {
tail = original_buffer_range.start;
} }
} }
SelectMode::Line => todo!(), SelectMode::Line(_) => todo!(),
SelectMode::All => selection.head(), SelectMode::All => {
return;
}
}; };
if cursor.cmp(&selection.tail(), buffer).unwrap() < Ordering::Equal { if head < tail {
if !selection.reversed { selection.start = buffer.anchor_before(head);
selection.end = selection.start.clone(); selection.end = buffer.anchor_before(tail);
selection.reversed = true; selection.reversed = true;
}
selection.start = cursor;
} else { } else {
if selection.reversed { selection.start = buffer.anchor_before(tail);
selection.start = selection.end.clone(); selection.end = buffer.anchor_before(head);
selection.reversed = false; selection.reversed = false;
}
selection.end = cursor;
} }
} else { } else {
log::error!("update_selection dispatched with no pending selection"); log::error!("update_selection dispatched with no pending selection");
@ -3198,7 +3221,7 @@ mod tests {
cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
editor.update(cx, |view, cx| { editor.update(cx, |view, cx| {
view.begin_selection(DisplayPoint::new(2, 2), false, SelectMode::Character, cx); view.begin_selection(DisplayPoint::new(2, 2), false, 1, cx);
}); });
assert_eq!( assert_eq!(
@ -3235,7 +3258,7 @@ mod tests {
); );
editor.update(cx, |view, cx| { editor.update(cx, |view, cx| {
view.begin_selection(DisplayPoint::new(3, 3), true, SelectMode::Character, cx); view.begin_selection(DisplayPoint::new(3, 3), true, 1, cx);
view.update_selection(DisplayPoint::new(0, 0), Vector2F::zero(), cx); view.update_selection(DisplayPoint::new(0, 0), Vector2F::zero(), cx);
}); });
@ -3264,7 +3287,7 @@ mod tests {
let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
view.update(cx, |view, cx| { view.update(cx, |view, cx| {
view.begin_selection(DisplayPoint::new(2, 2), false, SelectMode::Character, cx); view.begin_selection(DisplayPoint::new(2, 2), false, 1, cx);
assert_eq!( assert_eq!(
view.selection_ranges(cx), view.selection_ranges(cx),
[DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2)] [DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2)]
@ -3296,11 +3319,11 @@ mod tests {
let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx)); let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
view.update(cx, |view, cx| { view.update(cx, |view, cx| {
view.begin_selection(DisplayPoint::new(3, 4), false, SelectMode::Character, cx); view.begin_selection(DisplayPoint::new(3, 4), false, 1, cx);
view.update_selection(DisplayPoint::new(1, 1), Vector2F::zero(), cx); view.update_selection(DisplayPoint::new(1, 1), Vector2F::zero(), cx);
view.end_selection(cx); view.end_selection(cx);
view.begin_selection(DisplayPoint::new(0, 1), true, SelectMode::Character, cx); view.begin_selection(DisplayPoint::new(0, 1), true, 1, cx);
view.update_selection(DisplayPoint::new(0, 3), Vector2F::zero(), cx); view.update_selection(DisplayPoint::new(0, 3), Vector2F::zero(), cx);
view.end_selection(cx); view.end_selection(cx);
assert_eq!( assert_eq!(

View File

@ -181,6 +181,14 @@ pub fn next_word_boundary(map: &DisplayMapSnapshot, mut point: DisplayPoint) ->
point point
} }
pub fn is_inside_word(map: &DisplayMapSnapshot, point: DisplayPoint) -> bool {
let ix = map.clip_point(point, Bias::Left).to_offset(map, Bias::Left);
let text = map.buffer_snapshot.text();
let next_char_kind = text.chars_at(ix).next().map(char_kind);
let prev_char_kind = text.reversed_chars_at(ix).next().map(char_kind);
prev_char_kind.zip(next_char_kind) == Some((CharKind::Word, CharKind::Word))
}
pub fn surrounding_word(map: &DisplayMapSnapshot, point: DisplayPoint) -> Range<DisplayPoint> { pub fn surrounding_word(map: &DisplayMapSnapshot, point: DisplayPoint) -> Range<DisplayPoint> {
let mut start = map.clip_point(point, Bias::Left).to_offset(map, Bias::Left); let mut start = map.clip_point(point, Bias::Left).to_offset(map, Bias::Left);
let mut end = start; let mut end = start;

View File

@ -3651,6 +3651,7 @@ mod tests {
Event::LeftMouseDown { Event::LeftMouseDown {
position: Default::default(), position: Default::default(),
cmd: false, cmd: false,
click_count: 1,
}, },
cx, cx,
); );

View File

@ -15,7 +15,7 @@ pub enum Event {
LeftMouseDown { LeftMouseDown {
position: Vector2F, position: Vector2F,
cmd: bool, cmd: bool,
count: usize, click_count: usize,
}, },
LeftMouseUp { LeftMouseUp {
position: Vector2F, position: Vector2F,

View File

@ -94,7 +94,7 @@ impl Event {
cmd: native_event cmd: native_event
.modifierFlags() .modifierFlags()
.contains(NSEventModifierFlags::NSCommandKeyMask), .contains(NSEventModifierFlags::NSCommandKeyMask),
count: native_event.clickCount() as usize, click_count: native_event.clickCount() as usize,
}) })
} }
NSEventType::NSLeftMouseUp => window_height.map(|window_height| Self::LeftMouseUp { NSEventType::NSLeftMouseUp => window_height.map(|window_height| Self::LeftMouseUp {