Merge pull request #244 from zed-industries/mouse-selections

Nathan Sobo created

Improve support for selecting text via the mouse

Change summary

crates/editor/src/display_map.rs      |   2 
crates/editor/src/element.rs          |  43 +++-
crates/editor/src/lib.rs              | 273 ++++++++++++++++++++++------
crates/editor/src/movement.rs         | 166 ++++++++++++++---
crates/gpui/src/app.rs                |   4 
crates/gpui/src/platform/event.rs     |   4 
crates/gpui/src/platform/mac/event.rs |   9 
7 files changed, 400 insertions(+), 101 deletions(-)

Detailed changes

crates/editor/src/display_map.rs 🔗

@@ -169,7 +169,7 @@ impl DisplayMap {
 }
 
 pub struct DisplayMapSnapshot {
-    buffer_snapshot: language::Snapshot,
+    pub buffer_snapshot: language::Snapshot,
     folds_snapshot: fold_map::Snapshot,
     tabs_snapshot: tab_map::Snapshot,
     wraps_snapshot: wrap_map::Snapshot,

crates/editor/src/element.rs 🔗

@@ -55,19 +55,36 @@ impl EditorElement {
     fn mouse_down(
         &self,
         position: Vector2F,
-        cmd: bool,
+        alt: bool,
+        shift: bool,
+        mut click_count: usize,
         layout: &mut LayoutState,
         paint: &mut PaintState,
         cx: &mut EventContext,
     ) -> bool {
-        if paint.text_bounds.contains_point(position) {
-            let snapshot = self.snapshot(cx.app);
-            let position = paint.point_for_position(&snapshot, layout, position);
-            cx.dispatch_action(Select(SelectPhase::Begin { position, add: cmd }));
-            true
+        if paint.gutter_bounds.contains_point(position) {
+            click_count = 3; // Simulate triple-click when clicking the gutter to select lines
+        } else if !paint.text_bounds.contains_point(position) {
+            return false;
+        }
+
+        let snapshot = self.snapshot(cx.app);
+        let position = paint.point_for_position(&snapshot, layout, position);
+
+        if shift {
+            cx.dispatch_action(Select(SelectPhase::Extend {
+                position,
+                click_count,
+            }));
         } else {
-            false
+            cx.dispatch_action(Select(SelectPhase::Begin {
+                position,
+                add: alt,
+                click_count,
+            }));
         }
+
+        true
     }
 
     fn mouse_up(&self, _position: Vector2F, cx: &mut EventContext) -> bool {
@@ -824,6 +841,7 @@ impl Element for EditorElement {
 
             Some(PaintState {
                 bounds,
+                gutter_bounds,
                 text_bounds,
             })
         } else {
@@ -841,9 +859,13 @@ impl Element for EditorElement {
     ) -> bool {
         if let (Some(layout), Some(paint)) = (layout, paint) {
             match event {
-                Event::LeftMouseDown { position, cmd } => {
-                    self.mouse_down(*position, *cmd, layout, paint, cx)
-                }
+                Event::LeftMouseDown {
+                    position,
+                    alt,
+                    shift,
+                    click_count,
+                    ..
+                } => self.mouse_down(*position, *alt, *shift, *click_count, layout, paint, cx),
                 Event::LeftMouseUp { position } => self.mouse_up(*position, cx),
                 Event::LeftMouseDragged { position } => {
                     self.mouse_dragged(*position, layout, paint, cx)
@@ -948,6 +970,7 @@ impl LayoutState {
 
 pub struct PaintState {
     bounds: RectF,
+    gutter_bounds: RectF,
     text_bounds: RectF,
 }
 

crates/editor/src/lib.rs 🔗

@@ -23,7 +23,7 @@ use smallvec::SmallVec;
 use smol::Timer;
 use std::{
     cell::RefCell,
-    cmp::{self, Ordering},
+    cmp,
     collections::HashMap,
     iter, mem,
     ops::{Range, RangeInclusive},
@@ -280,6 +280,11 @@ pub enum SelectPhase {
     Begin {
         position: DisplayPoint,
         add: bool,
+        click_count: usize,
+    },
+    Extend {
+        position: DisplayPoint,
+        click_count: usize,
     },
     Update {
         position: DisplayPoint,
@@ -288,6 +293,14 @@ pub enum SelectPhase {
     End,
 }
 
+#[derive(Clone, Debug)]
+enum SelectMode {
+    Character,
+    Word(Range<Anchor>),
+    Line(Range<Anchor>),
+    All,
+}
+
 #[derive(Copy, Clone, PartialEq, Eq)]
 pub enum EditorMode {
     SingleLine,
@@ -306,7 +319,7 @@ pub struct Editor {
     buffer: ModelHandle<Buffer>,
     display_map: ModelHandle<DisplayMap>,
     selection_set_id: SelectionSetId,
-    pending_selection: Option<Selection<Anchor>>,
+    pending_selection: Option<PendingSelection>,
     next_selection_id: usize,
     add_selections_state: Option<AddSelectionsState>,
     autoclose_stack: Vec<BracketPairState>,
@@ -333,6 +346,11 @@ pub struct Snapshot {
     scroll_top_anchor: Anchor,
 }
 
+struct PendingSelection {
+    selection: Selection<Anchor>,
+    mode: SelectMode,
+}
+
 struct AddSelectionsState {
     above: bool,
     stack: Vec<usize>,
@@ -633,7 +651,15 @@ impl Editor {
 
     fn select(&mut self, Select(phase): &Select, cx: &mut ViewContext<Self>) {
         match phase {
-            SelectPhase::Begin { position, add } => self.begin_selection(*position, *add, cx),
+            SelectPhase::Begin {
+                position,
+                add,
+                click_count,
+            } => self.begin_selection(*position, *add, *click_count, cx),
+            SelectPhase::Extend {
+                position,
+                click_count,
+            } => self.extend_selection(*position, *click_count, cx),
             SelectPhase::Update {
                 position,
                 scroll_position,
@@ -642,7 +668,44 @@ impl Editor {
         }
     }
 
-    fn begin_selection(&mut self, position: DisplayPoint, add: bool, cx: &mut ViewContext<Self>) {
+    fn extend_selection(
+        &mut self,
+        position: DisplayPoint,
+        click_count: usize,
+        cx: &mut ViewContext<Self>,
+    ) {
+        let tail = self.newest_selection::<usize>(cx).tail();
+
+        self.begin_selection(position, false, click_count, cx);
+
+        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+        let buffer = self.buffer.read(cx);
+        let position = position.to_offset(&display_map, Bias::Left);
+        let tail_anchor = buffer.anchor_before(tail);
+        let pending = self.pending_selection.as_mut().unwrap();
+
+        if position >= tail {
+            pending.selection.start = tail_anchor.clone();
+        } else {
+            pending.selection.end = tail_anchor.clone();
+            pending.selection.reversed = true;
+        }
+
+        match &mut pending.mode {
+            SelectMode::Word(range) | SelectMode::Line(range) => {
+                *range = tail_anchor.clone()..tail_anchor
+            }
+            _ => {}
+        }
+    }
+
+    fn begin_selection(
+        &mut self,
+        position: DisplayPoint,
+        add: bool,
+        click_count: usize,
+        cx: &mut ViewContext<Self>,
+    ) {
         if !self.focused {
             cx.focus_self();
             cx.emit(Event::Activate);
@@ -650,19 +713,63 @@ impl Editor {
 
         let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
         let buffer = self.buffer.read(cx);
-        let cursor = buffer.anchor_before(position.to_point(&display_map));
+        let start;
+        let end;
+        let mode;
+        match click_count {
+            1 => {
+                start = buffer.anchor_before(position.to_point(&display_map));
+                end = start.clone();
+                mode = SelectMode::Character;
+            }
+            2 => {
+                let range = movement::surrounding_word(&display_map, position);
+                start = buffer.anchor_before(range.start.to_point(&display_map));
+                end = buffer.anchor_before(range.end.to_point(&display_map));
+                mode = SelectMode::Word(start.clone()..end.clone());
+            }
+            3 => {
+                let position = display_map.clip_point(position, Bias::Left);
+                let line_start = movement::line_beginning(&display_map, position, false);
+                let mut next_line_start = line_start.clone();
+                *next_line_start.row_mut() += 1;
+                *next_line_start.column_mut() = 0;
+                next_line_start = display_map.clip_point(next_line_start, Bias::Right);
+
+                start = buffer.anchor_before(line_start.to_point(&display_map));
+                end = buffer.anchor_before(next_line_start.to_point(&display_map));
+                mode = SelectMode::Line(start.clone()..end.clone());
+            }
+            _ => {
+                start = buffer.anchor_before(0);
+                end = buffer.anchor_before(buffer.len());
+                mode = SelectMode::All;
+            }
+        }
+
         let selection = Selection {
             id: post_inc(&mut self.next_selection_id),
-            start: cursor.clone(),
-            end: cursor,
+            start,
+            end,
             reversed: false,
             goal: SelectionGoal::None,
         };
 
         if !add {
             self.update_selections::<usize>(Vec::new(), false, cx);
+        } else if click_count > 1 {
+            // Remove the newest selection since it was only added as part of this multi-click.
+            let newest_selection = self.newest_selection::<usize>(cx);
+            self.update_selections::<usize>(
+                self.selections(cx)
+                    .filter(|selection| selection.id != newest_selection.id)
+                    .collect(),
+                false,
+                cx,
+            )
         }
-        self.pending_selection = Some(selection);
+
+        self.pending_selection = Some(PendingSelection { selection, mode });
 
         cx.notify();
     }
@@ -674,21 +781,75 @@ impl Editor {
         cx: &mut ViewContext<Self>,
     ) {
         let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
-        if let Some(pending_selection) = self.pending_selection.as_mut() {
+        if let Some(PendingSelection { selection, mode }) = self.pending_selection.as_mut() {
             let buffer = self.buffer.read(cx);
-            let cursor = buffer.anchor_before(position.to_point(&display_map));
-            if cursor.cmp(&pending_selection.tail(), buffer).unwrap() < Ordering::Equal {
-                if !pending_selection.reversed {
-                    pending_selection.end = pending_selection.start.clone();
-                    pending_selection.reversed = true;
+            let head;
+            let tail;
+            match mode {
+                SelectMode::Character => {
+                    head = position.to_point(&display_map);
+                    tail = selection.tail().to_point(buffer);
                 }
-                pending_selection.start = cursor;
-            } else {
-                if pending_selection.reversed {
-                    pending_selection.start = pending_selection.end.clone();
-                    pending_selection.reversed = false;
+                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 {
+                        head = position.to_point(&display_map);
+                    }
+
+                    if head <= original_buffer_range.start {
+                        tail = original_buffer_range.end;
+                    } else {
+                        tail = original_buffer_range.start;
+                    }
                 }
-                pending_selection.end = cursor;
+                SelectMode::Line(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);
+                    let line_start = movement::line_beginning(&display_map, position, false);
+                    let mut next_line_start = line_start.clone();
+                    *next_line_start.row_mut() += 1;
+                    *next_line_start.column_mut() = 0;
+                    next_line_start = display_map.clip_point(next_line_start, Bias::Right);
+
+                    if line_start < original_display_range.start {
+                        head = line_start.to_point(&display_map);
+                    } else {
+                        head = next_line_start.to_point(&display_map);
+                    }
+
+                    if head <= original_buffer_range.start {
+                        tail = original_buffer_range.end;
+                    } else {
+                        tail = original_buffer_range.start;
+                    }
+                }
+                SelectMode::All => {
+                    return;
+                }
+            };
+
+            if head < tail {
+                selection.start = buffer.anchor_before(head);
+                selection.end = buffer.anchor_before(tail);
+                selection.reversed = true;
+            } else {
+                selection.start = buffer.anchor_before(tail);
+                selection.end = buffer.anchor_before(head);
+                selection.reversed = false;
             }
         } else {
             log::error!("update_selection dispatched with no pending selection");
@@ -713,17 +874,17 @@ impl Editor {
     pub fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
         if self.active_diagnostics.is_some() {
             self.dismiss_diagnostics(cx);
-        } else if let Some(pending_selection) = self.pending_selection.take() {
+        } else if let Some(PendingSelection { selection, .. }) = self.pending_selection.take() {
             let buffer = self.buffer.read(cx);
-            let pending_selection = Selection {
-                id: pending_selection.id,
-                start: pending_selection.start.to_point(buffer),
-                end: pending_selection.end.to_point(buffer),
-                reversed: pending_selection.reversed,
-                goal: pending_selection.goal,
+            let selection = Selection {
+                id: selection.id,
+                start: selection.start.to_point(buffer),
+                end: selection.end.to_point(buffer),
+                reversed: selection.reversed,
+                goal: selection.goal,
             };
             if self.selections::<Point>(cx).next().is_none() {
-                self.update_selections(vec![pending_selection], true, cx);
+                self.update_selections(vec![selection], true, cx);
             }
         } else {
             let mut oldest_selection = self.oldest_selection::<usize>(cx);
@@ -1814,8 +1975,7 @@ impl Editor {
         let mut selections = self.selections::<Point>(cx).collect::<Vec<_>>();
         for selection in &mut selections {
             let head = selection.head().to_display_point(&display_map);
-            let new_head = movement::prev_word_boundary(&display_map, head).unwrap();
-            let cursor = new_head.to_point(&display_map);
+            let cursor = movement::prev_word_boundary(&display_map, head).to_point(&display_map);
             selection.start = cursor.clone();
             selection.end = cursor;
             selection.reversed = false;
@@ -1833,8 +1993,7 @@ impl Editor {
         let mut selections = self.selections::<Point>(cx).collect::<Vec<_>>();
         for selection in &mut selections {
             let head = selection.head().to_display_point(&display_map);
-            let new_head = movement::prev_word_boundary(&display_map, head).unwrap();
-            let cursor = new_head.to_point(&display_map);
+            let cursor = movement::prev_word_boundary(&display_map, head).to_point(&display_map);
             selection.set_head(cursor);
             selection.goal = SelectionGoal::None;
         }
@@ -1852,8 +2011,8 @@ impl Editor {
         for selection in &mut selections {
             if selection.is_empty() {
                 let head = selection.head().to_display_point(&display_map);
-                let new_head = movement::prev_word_boundary(&display_map, head).unwrap();
-                let cursor = new_head.to_point(&display_map);
+                let cursor =
+                    movement::prev_word_boundary(&display_map, head).to_point(&display_map);
                 selection.set_head(cursor);
                 selection.goal = SelectionGoal::None;
             }
@@ -1872,8 +2031,7 @@ impl Editor {
         let mut selections = self.selections::<Point>(cx).collect::<Vec<_>>();
         for selection in &mut selections {
             let head = selection.head().to_display_point(&display_map);
-            let new_head = movement::next_word_boundary(&display_map, head).unwrap();
-            let cursor = new_head.to_point(&display_map);
+            let cursor = movement::next_word_boundary(&display_map, head).to_point(&display_map);
             selection.start = cursor;
             selection.end = cursor;
             selection.reversed = false;
@@ -1891,8 +2049,7 @@ impl Editor {
         let mut selections = self.selections::<Point>(cx).collect::<Vec<_>>();
         for selection in &mut selections {
             let head = selection.head().to_display_point(&display_map);
-            let new_head = movement::next_word_boundary(&display_map, head).unwrap();
-            let cursor = new_head.to_point(&display_map);
+            let cursor = movement::next_word_boundary(&display_map, head).to_point(&display_map);
             selection.set_head(cursor);
             selection.goal = SelectionGoal::None;
         }
@@ -1910,8 +2067,8 @@ impl Editor {
         for selection in &mut selections {
             if selection.is_empty() {
                 let head = selection.head().to_display_point(&display_map);
-                let new_head = movement::next_word_boundary(&display_map, head).unwrap();
-                let cursor = new_head.to_point(&display_map);
+                let cursor =
+                    movement::next_word_boundary(&display_map, head).to_point(&display_map);
                 selection.set_head(cursor);
                 selection.goal = SelectionGoal::None;
             }
@@ -1930,7 +2087,7 @@ impl Editor {
         let mut selections = self.selections::<Point>(cx).collect::<Vec<_>>();
         for selection in &mut selections {
             let head = selection.head().to_display_point(&display_map);
-            let new_head = movement::line_beginning(&display_map, head, true).unwrap();
+            let new_head = movement::line_beginning(&display_map, head, true);
             let cursor = new_head.to_point(&display_map);
             selection.start = cursor;
             selection.end = cursor;
@@ -1949,7 +2106,7 @@ impl Editor {
         let mut selections = self.selections::<Point>(cx).collect::<Vec<_>>();
         for selection in &mut selections {
             let head = selection.head().to_display_point(&display_map);
-            let new_head = movement::line_beginning(&display_map, head, *toggle_indent).unwrap();
+            let new_head = movement::line_beginning(&display_map, head, *toggle_indent);
             selection.set_head(new_head.to_point(&display_map));
             selection.goal = SelectionGoal::None;
         }
@@ -1973,7 +2130,7 @@ impl Editor {
         {
             for selection in &mut selections {
                 let head = selection.head().to_display_point(&display_map);
-                let new_head = movement::line_end(&display_map, head).unwrap();
+                let new_head = movement::line_end(&display_map, head);
                 let anchor = new_head.to_point(&display_map);
                 selection.start = anchor.clone();
                 selection.end = anchor;
@@ -1989,7 +2146,7 @@ impl Editor {
         let mut selections = self.selections::<Point>(cx).collect::<Vec<_>>();
         for selection in &mut selections {
             let head = selection.head().to_display_point(&display_map);
-            let new_head = movement::line_end(&display_map, head).unwrap();
+            let new_head = movement::line_end(&display_map, head);
             selection.set_head(new_head.to_point(&display_map));
             selection.goal = SelectionGoal::None;
         }
@@ -2632,9 +2789,9 @@ impl Editor {
         let start_index = self.selection_insertion_index(&selections, start);
         let pending_selection = if set_id == self.selection_set_id {
             self.pending_selection.as_ref().and_then(|pending| {
-                let mut selection_start = pending.start.to_display_point(&display_map);
-                let mut selection_end = pending.end.to_display_point(&display_map);
-                if pending.reversed {
+                let mut selection_start = pending.selection.start.to_display_point(&display_map);
+                let mut selection_end = pending.selection.end.to_display_point(&display_map);
+                if pending.selection.reversed {
                     mem::swap(&mut selection_start, &mut selection_end);
                 }
                 if selection_start <= range.end || selection_end <= range.end {
@@ -2704,12 +2861,12 @@ impl Editor {
         D: 'a + TextDimension<'a>,
     {
         let buffer = self.buffer.read(cx);
-        self.pending_selection.as_ref().map(|selection| Selection {
-            id: selection.id,
-            start: selection.start.summary::<D, _>(buffer),
-            end: selection.end.summary::<D, _>(buffer),
-            reversed: selection.reversed,
-            goal: selection.goal,
+        self.pending_selection.as_ref().map(|pending| Selection {
+            id: pending.selection.id,
+            start: pending.selection.start.summary::<D, _>(buffer),
+            end: pending.selection.end.summary::<D, _>(buffer),
+            reversed: pending.selection.reversed,
+            goal: pending.selection.goal,
         })
     }
 
@@ -3317,7 +3474,7 @@ mod tests {
             cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
 
         editor.update(cx, |view, cx| {
-            view.begin_selection(DisplayPoint::new(2, 2), false, cx);
+            view.begin_selection(DisplayPoint::new(2, 2), false, 1, cx);
         });
 
         assert_eq!(
@@ -3354,7 +3511,7 @@ mod tests {
         );
 
         editor.update(cx, |view, cx| {
-            view.begin_selection(DisplayPoint::new(3, 3), true, cx);
+            view.begin_selection(DisplayPoint::new(3, 3), true, 1, cx);
             view.update_selection(DisplayPoint::new(0, 0), Vector2F::zero(), cx);
         });
 
@@ -3383,7 +3540,7 @@ mod tests {
         let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
 
         view.update(cx, |view, cx| {
-            view.begin_selection(DisplayPoint::new(2, 2), false, cx);
+            view.begin_selection(DisplayPoint::new(2, 2), false, 1, cx);
             assert_eq!(
                 view.selection_ranges(cx),
                 [DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2)]
@@ -3415,11 +3572,11 @@ mod tests {
         let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, settings, cx));
 
         view.update(cx, |view, cx| {
-            view.begin_selection(DisplayPoint::new(3, 4), false, cx);
+            view.begin_selection(DisplayPoint::new(3, 4), false, 1, cx);
             view.update_selection(DisplayPoint::new(1, 1), Vector2F::zero(), cx);
             view.end_selection(cx);
 
-            view.begin_selection(DisplayPoint::new(0, 1), true, cx);
+            view.begin_selection(DisplayPoint::new(0, 1), true, 1, cx);
             view.update_selection(DisplayPoint::new(0, 3), Vector2F::zero(), cx);
             view.end_selection(cx);
             assert_eq!(

crates/editor/src/movement.rs 🔗

@@ -1,5 +1,7 @@
-use super::{Bias, DisplayMapSnapshot, DisplayPoint, SelectionGoal};
+use super::{Bias, DisplayMapSnapshot, DisplayPoint, SelectionGoal, ToDisplayPoint};
 use anyhow::Result;
+use buffer::ToPoint;
+use std::{cmp, ops::Range};
 
 pub fn left(map: &DisplayMapSnapshot, mut point: DisplayPoint) -> Result<DisplayPoint> {
     if point.column() > 0 {
@@ -99,24 +101,21 @@ pub fn line_beginning(
     map: &DisplayMapSnapshot,
     point: DisplayPoint,
     toggle_indent: bool,
-) -> Result<DisplayPoint> {
+) -> DisplayPoint {
     let (indent, is_blank) = map.line_indent(point.row());
     if toggle_indent && !is_blank && point.column() != indent {
-        Ok(DisplayPoint::new(point.row(), indent))
+        DisplayPoint::new(point.row(), indent)
     } else {
-        Ok(DisplayPoint::new(point.row(), 0))
+        DisplayPoint::new(point.row(), 0)
     }
 }
 
-pub fn line_end(map: &DisplayMapSnapshot, point: DisplayPoint) -> Result<DisplayPoint> {
+pub fn line_end(map: &DisplayMapSnapshot, point: DisplayPoint) -> DisplayPoint {
     let line_end = DisplayPoint::new(point.row(), map.line_len(point.row()));
-    Ok(map.clip_point(line_end, Bias::Left))
+    map.clip_point(line_end, Bias::Left)
 }
 
-pub fn prev_word_boundary(
-    map: &DisplayMapSnapshot,
-    mut point: DisplayPoint,
-) -> Result<DisplayPoint> {
+pub fn prev_word_boundary(map: &DisplayMapSnapshot, mut point: DisplayPoint) -> DisplayPoint {
     let mut line_start = 0;
     if point.row() > 0 {
         if let Some(indent) = map.soft_wrap_indent(point.row() - 1) {
@@ -126,7 +125,7 @@ pub fn prev_word_boundary(
 
     if point.column() == line_start {
         if point.row() == 0 {
-            return Ok(DisplayPoint::new(0, 0));
+            return DisplayPoint::new(0, 0);
         } else {
             let row = point.row() - 1;
             point = map.clip_point(DisplayPoint::new(row, map.line_len(row)), Bias::Left);
@@ -152,13 +151,10 @@ pub fn prev_word_boundary(
         prev_char_kind = char_kind;
         column += c.len_utf8() as u32;
     }
-    Ok(boundary)
+    boundary
 }
 
-pub fn next_word_boundary(
-    map: &DisplayMapSnapshot,
-    mut point: DisplayPoint,
-) -> Result<DisplayPoint> {
+pub fn next_word_boundary(map: &DisplayMapSnapshot, mut point: DisplayPoint) -> DisplayPoint {
     let mut prev_char_kind = None;
     for c in map.chars_at(point) {
         let char_kind = char_kind(c);
@@ -182,14 +178,54 @@ pub fn next_word_boundary(
         }
         prev_char_kind = Some(char_kind);
     }
-    Ok(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> {
+    let mut start = map.clip_point(point, Bias::Left).to_offset(map, Bias::Left);
+    let mut end = start;
+
+    let text = map.buffer_snapshot.text();
+    let mut next_chars = text.chars_at(start).peekable();
+    let mut prev_chars = text.reversed_chars_at(start).peekable();
+    let word_kind = cmp::max(
+        prev_chars.peek().copied().map(char_kind),
+        next_chars.peek().copied().map(char_kind),
+    );
+
+    for ch in prev_chars {
+        if Some(char_kind(ch)) == word_kind {
+            start -= ch.len_utf8();
+        } else {
+            break;
+        }
+    }
+
+    for ch in next_chars {
+        if Some(char_kind(ch)) == word_kind {
+            end += ch.len_utf8();
+        } else {
+            break;
+        }
+    }
+
+    start.to_point(&map.buffer_snapshot).to_display_point(map)
+        ..end.to_point(&map.buffer_snapshot).to_display_point(map)
 }
 
-#[derive(Copy, Clone, Eq, PartialEq)]
+#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord)]
 enum CharKind {
     Newline,
-    Whitespace,
     Punctuation,
+    Whitespace,
     Word,
 }
 
@@ -225,45 +261,117 @@ mod tests {
             cx.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, cx));
         let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
         assert_eq!(
-            prev_word_boundary(&snapshot, DisplayPoint::new(0, 12)).unwrap(),
+            prev_word_boundary(&snapshot, DisplayPoint::new(0, 12)),
             DisplayPoint::new(0, 7)
         );
         assert_eq!(
-            prev_word_boundary(&snapshot, DisplayPoint::new(0, 7)).unwrap(),
+            prev_word_boundary(&snapshot, DisplayPoint::new(0, 7)),
             DisplayPoint::new(0, 2)
         );
         assert_eq!(
-            prev_word_boundary(&snapshot, DisplayPoint::new(0, 6)).unwrap(),
+            prev_word_boundary(&snapshot, DisplayPoint::new(0, 6)),
             DisplayPoint::new(0, 2)
         );
         assert_eq!(
-            prev_word_boundary(&snapshot, DisplayPoint::new(0, 2)).unwrap(),
+            prev_word_boundary(&snapshot, DisplayPoint::new(0, 2)),
             DisplayPoint::new(0, 0)
         );
         assert_eq!(
-            prev_word_boundary(&snapshot, DisplayPoint::new(0, 1)).unwrap(),
+            prev_word_boundary(&snapshot, DisplayPoint::new(0, 1)),
             DisplayPoint::new(0, 0)
         );
 
         assert_eq!(
-            next_word_boundary(&snapshot, DisplayPoint::new(0, 0)).unwrap(),
+            next_word_boundary(&snapshot, DisplayPoint::new(0, 0)),
             DisplayPoint::new(0, 1)
         );
         assert_eq!(
-            next_word_boundary(&snapshot, DisplayPoint::new(0, 1)).unwrap(),
+            next_word_boundary(&snapshot, DisplayPoint::new(0, 1)),
             DisplayPoint::new(0, 6)
         );
         assert_eq!(
-            next_word_boundary(&snapshot, DisplayPoint::new(0, 2)).unwrap(),
+            next_word_boundary(&snapshot, DisplayPoint::new(0, 2)),
             DisplayPoint::new(0, 6)
         );
         assert_eq!(
-            next_word_boundary(&snapshot, DisplayPoint::new(0, 6)).unwrap(),
+            next_word_boundary(&snapshot, DisplayPoint::new(0, 6)),
             DisplayPoint::new(0, 12)
         );
         assert_eq!(
-            next_word_boundary(&snapshot, DisplayPoint::new(0, 7)).unwrap(),
+            next_word_boundary(&snapshot, DisplayPoint::new(0, 7)),
             DisplayPoint::new(0, 12)
         );
     }
+
+    #[gpui::test]
+    fn test_surrounding_word(cx: &mut gpui::MutableAppContext) {
+        let tab_size = 4;
+        let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
+        let font_id = cx
+            .font_cache()
+            .select_font(family_id, &Default::default())
+            .unwrap();
+        let font_size = 14.0;
+        let buffer = cx.add_model(|cx| Buffer::new(0, "lorem ipsum   dolor\n    sit", cx));
+        let display_map =
+            cx.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, cx));
+        let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
+
+        assert_eq!(
+            surrounding_word(&snapshot, DisplayPoint::new(0, 0)),
+            DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5)
+        );
+        assert_eq!(
+            surrounding_word(&snapshot, DisplayPoint::new(0, 2)),
+            DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5)
+        );
+        assert_eq!(
+            surrounding_word(&snapshot, DisplayPoint::new(0, 5)),
+            DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5)
+        );
+        assert_eq!(
+            surrounding_word(&snapshot, DisplayPoint::new(0, 6)),
+            DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11)
+        );
+        assert_eq!(
+            surrounding_word(&snapshot, DisplayPoint::new(0, 7)),
+            DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11)
+        );
+        assert_eq!(
+            surrounding_word(&snapshot, DisplayPoint::new(0, 11)),
+            DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11)
+        );
+        assert_eq!(
+            surrounding_word(&snapshot, DisplayPoint::new(0, 13)),
+            DisplayPoint::new(0, 11)..DisplayPoint::new(0, 14)
+        );
+        assert_eq!(
+            surrounding_word(&snapshot, DisplayPoint::new(0, 14)),
+            DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19)
+        );
+        assert_eq!(
+            surrounding_word(&snapshot, DisplayPoint::new(0, 17)),
+            DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19)
+        );
+        assert_eq!(
+            surrounding_word(&snapshot, DisplayPoint::new(0, 19)),
+            DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19)
+        );
+        assert_eq!(
+            surrounding_word(&snapshot, DisplayPoint::new(1, 0)),
+            DisplayPoint::new(1, 0)..DisplayPoint::new(1, 4)
+        );
+        assert_eq!(
+            surrounding_word(&snapshot, DisplayPoint::new(1, 1)),
+            DisplayPoint::new(1, 0)..DisplayPoint::new(1, 4)
+        );
+        assert_eq!(
+            surrounding_word(&snapshot, DisplayPoint::new(1, 6)),
+            DisplayPoint::new(1, 4)..DisplayPoint::new(1, 7)
+        );
+        assert_eq!(
+            surrounding_word(&snapshot, DisplayPoint::new(1, 7)),
+            DisplayPoint::new(1, 4)..DisplayPoint::new(1, 7)
+        );
+    }
 }

crates/gpui/src/app.rs 🔗

@@ -3650,7 +3650,11 @@ mod tests {
         presenter.borrow_mut().dispatch_event(
             Event::LeftMouseDown {
                 position: Default::default(),
+                ctrl: false,
+                alt: false,
+                shift: false,
                 cmd: false,
+                click_count: 1,
             },
             cx,
         );

crates/gpui/src/platform/event.rs 🔗

@@ -14,7 +14,11 @@ pub enum Event {
     },
     LeftMouseDown {
         position: Vector2F,
+        ctrl: bool,
+        alt: bool,
+        shift: bool,
         cmd: bool,
+        click_count: usize,
     },
     LeftMouseUp {
         position: Vector2F,

crates/gpui/src/platform/mac/event.rs 🔗

@@ -88,14 +88,17 @@ impl Event {
                 })
             }
             NSEventType::NSLeftMouseDown => {
+                let modifiers = native_event.modifierFlags();
                 window_height.map(|window_height| Self::LeftMouseDown {
                     position: vec2f(
                         native_event.locationInWindow().x as f32,
                         window_height - native_event.locationInWindow().y as f32,
                     ),
-                    cmd: native_event
-                        .modifierFlags()
-                        .contains(NSEventModifierFlags::NSCommandKeyMask),
+                    ctrl: modifiers.contains(NSEventModifierFlags::NSControlKeyMask),
+                    alt: modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask),
+                    shift: modifiers.contains(NSEventModifierFlags::NSShiftKeyMask),
+                    cmd: modifiers.contains(NSEventModifierFlags::NSCommandKeyMask),
+                    click_count: native_event.clickCount() as usize,
                 })
             }
             NSEventType::NSLeftMouseUp => window_height.map(|window_height| Self::LeftMouseUp {