Dismiss autocomplete when moving outside of a word

Antonio Scandurra created

Change summary

crates/editor/src/editor.rs   | 65 ++++++++++++++++++++--------
crates/editor/src/movement.rs | 82 +++++++++++++++++++++++++++++-------
crates/find/src/find.rs       |  2 
3 files changed, 112 insertions(+), 37 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -428,6 +428,7 @@ struct BracketPairState {
 }
 
 struct CompletionState {
+    initial_position: Anchor,
     completions: Arc<[Completion<Anchor>]>,
     selected_item: usize,
     list: UniformListState,
@@ -452,7 +453,7 @@ pub struct NavigationData {
     offset: usize,
 }
 
-#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord)]
+#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Debug)]
 pub enum CharKind {
     Newline,
     Punctuation,
@@ -887,7 +888,7 @@ impl Editor {
                 mode = SelectMode::Character;
             }
             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));
                 end = buffer.anchor_before(range.end.to_point(&display_map));
                 mode = SelectMode::Word(start.clone()..end.clone());
@@ -990,7 +991,7 @@ impl Editor {
                     if movement::is_inside_word(&display_map, position)
                         || original_display_range.contains(&position)
                     {
-                        let word_range = movement::surrounding_word(&display_map, 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 {
@@ -1397,6 +1398,9 @@ impl Editor {
                 .is_completion_trigger(selection.head(), text, cx)
             {
                 self.show_completions(&ShowCompletions, cx);
+            } else {
+                self.completion_state.take();
+                cx.notify();
             }
         }
     }
@@ -1528,25 +1532,30 @@ impl Editor {
     }
 
     fn show_completions(&mut self, _: &ShowCompletions, cx: &mut ViewContext<Self>) {
-        let position = self
-            .newest_selection::<usize>(&self.buffer.read(cx).read(cx))
-            .head();
+        let position = if let Some(selection) = self.newest_anchor_selection() {
+            selection.head()
+        } else {
+            return;
+        };
 
         let completions = self
             .buffer
-            .update(cx, |buffer, cx| buffer.completions(position, cx));
+            .update(cx, |buffer, cx| buffer.completions(position.clone(), cx));
 
         cx.spawn_weak(|this, mut cx| async move {
             let completions = completions.await?;
             if !completions.is_empty() {
                 if let Some(this) = cx.read(|cx| this.upgrade(cx)) {
                     this.update(&mut cx, |this, cx| {
-                        this.completion_state = Some(CompletionState {
-                            completions: completions.into(),
-                            selected_item: 0,
-                            list: Default::default(),
-                        });
-                        cx.notify();
+                        if this.focused {
+                            this.completion_state = Some(CompletionState {
+                                initial_position: position,
+                                completions: completions.into(),
+                                selected_item: 0,
+                                list: Default::default(),
+                            });
+                            cx.notify();
+                        }
                     });
                 }
             }
@@ -2905,7 +2914,7 @@ impl Editor {
         } else if selections.len() == 1 {
             let selection = selections.last_mut().unwrap();
             if selection.start == selection.end {
-                let word_range = movement::surrounding_word(
+                let (word_range, _) = movement::surrounding_word(
                     &display_map,
                     selection.start.to_display_point(&display_map),
                 );
@@ -3525,7 +3534,8 @@ impl Editor {
     ) where
         T: ToOffset + ToPoint + Ord + std::marker::Copy + std::fmt::Debug,
     {
-        let buffer = self.buffer.read(cx).snapshot(cx);
+        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+        let buffer = &display_map.buffer_snapshot;
         let old_cursor_position = self.newest_anchor_selection().map(|s| s.head());
         selections.sort_unstable_by_key(|s| s.start);
 
@@ -3570,16 +3580,32 @@ impl Editor {
             }
         }
 
+        let new_cursor_position = selections
+            .iter()
+            .max_by_key(|s| s.id)
+            .map(|s| s.head().to_point(&buffer));
         if let Some(old_cursor_position) = old_cursor_position {
-            let new_cursor_position = selections
-                .iter()
-                .max_by_key(|s| s.id)
-                .map(|s| s.head().to_point(&buffer));
             if new_cursor_position.is_some() {
                 self.push_to_nav_history(old_cursor_position, new_cursor_position, cx);
             }
         }
 
+        if let Some((completion_state, cursor_position)) =
+            self.completion_state.as_ref().zip(new_cursor_position)
+        {
+            let cursor_position = cursor_position.to_display_point(&display_map);
+            let initial_position = completion_state
+                .initial_position
+                .to_display_point(&display_map);
+
+            let (word_range, kind) = movement::surrounding_word(&display_map, initial_position);
+            if kind != Some(CharKind::Word) || !word_range.to_inclusive().contains(&cursor_position)
+            {
+                self.completion_state.take();
+                cx.notify();
+            }
+        }
+
         if let Some(autoscroll) = autoscroll {
             self.request_autoscroll(autoscroll, cx);
         }
@@ -4207,6 +4233,7 @@ impl View for Editor {
         self.show_local_cursors = false;
         self.buffer
             .update(cx, |buffer, cx| buffer.remove_active_selections(cx));
+        self.completion_state.take();
         cx.emit(Event::Blurred);
         cx.notify();
     }

crates/editor/src/movement.rs 🔗

@@ -183,7 +183,10 @@ pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool {
     prev_char_kind.zip(next_char_kind) == Some((CharKind::Word, CharKind::Word))
 }
 
-pub fn surrounding_word(map: &DisplaySnapshot, point: DisplayPoint) -> Range<DisplayPoint> {
+pub fn surrounding_word(
+    map: &DisplaySnapshot,
+    point: DisplayPoint,
+) -> (Range<DisplayPoint>, Option<CharKind>) {
     let mut start = map.clip_point(point, Bias::Left).to_offset(map, Bias::Left);
     let mut end = start;
 
@@ -211,8 +214,11 @@ pub fn surrounding_word(map: &DisplaySnapshot, point: DisplayPoint) -> Range<Dis
         }
     }
 
-    start.to_point(&map.buffer_snapshot).to_display_point(map)
-        ..end.to_point(&map.buffer_snapshot).to_display_point(map)
+    (
+        start.to_point(&map.buffer_snapshot).to_display_point(map)
+            ..end.to_point(&map.buffer_snapshot).to_display_point(map),
+        word_kind,
+    )
 }
 
 #[cfg(test)]
@@ -406,59 +412,101 @@ mod tests {
 
         assert_eq!(
             surrounding_word(&snapshot, DisplayPoint::new(0, 0)),
-            DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5)
+            (
+                DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5),
+                Some(CharKind::Word)
+            )
         );
         assert_eq!(
             surrounding_word(&snapshot, DisplayPoint::new(0, 2)),
-            DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5)
+            (
+                DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5),
+                Some(CharKind::Word)
+            )
         );
         assert_eq!(
             surrounding_word(&snapshot, DisplayPoint::new(0, 5)),
-            DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5)
+            (
+                DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5),
+                Some(CharKind::Word)
+            )
         );
         assert_eq!(
             surrounding_word(&snapshot, DisplayPoint::new(0, 6)),
-            DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11)
+            (
+                DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11),
+                Some(CharKind::Word)
+            )
         );
         assert_eq!(
             surrounding_word(&snapshot, DisplayPoint::new(0, 7)),
-            DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11)
+            (
+                DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11),
+                Some(CharKind::Word)
+            )
         );
         assert_eq!(
             surrounding_word(&snapshot, DisplayPoint::new(0, 11)),
-            DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11)
+            (
+                DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11),
+                Some(CharKind::Word)
+            )
         );
         assert_eq!(
             surrounding_word(&snapshot, DisplayPoint::new(0, 13)),
-            DisplayPoint::new(0, 11)..DisplayPoint::new(0, 14)
+            (
+                DisplayPoint::new(0, 11)..DisplayPoint::new(0, 14),
+                Some(CharKind::Whitespace)
+            )
         );
         assert_eq!(
             surrounding_word(&snapshot, DisplayPoint::new(0, 14)),
-            DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19)
+            (
+                DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19),
+                Some(CharKind::Word)
+            )
         );
         assert_eq!(
             surrounding_word(&snapshot, DisplayPoint::new(0, 17)),
-            DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19)
+            (
+                DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19),
+                Some(CharKind::Word)
+            )
         );
         assert_eq!(
             surrounding_word(&snapshot, DisplayPoint::new(0, 19)),
-            DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19)
+            (
+                DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19),
+                Some(CharKind::Word)
+            )
         );
         assert_eq!(
             surrounding_word(&snapshot, DisplayPoint::new(1, 0)),
-            DisplayPoint::new(1, 0)..DisplayPoint::new(1, 4)
+            (
+                DisplayPoint::new(1, 0)..DisplayPoint::new(1, 4),
+                Some(CharKind::Whitespace)
+            )
         );
         assert_eq!(
             surrounding_word(&snapshot, DisplayPoint::new(1, 1)),
-            DisplayPoint::new(1, 0)..DisplayPoint::new(1, 4)
+            (
+                DisplayPoint::new(1, 0)..DisplayPoint::new(1, 4),
+                Some(CharKind::Whitespace)
+            )
         );
         assert_eq!(
             surrounding_word(&snapshot, DisplayPoint::new(1, 6)),
-            DisplayPoint::new(1, 4)..DisplayPoint::new(1, 7)
+            (
+                DisplayPoint::new(1, 4)..DisplayPoint::new(1, 7),
+                Some(CharKind::Word)
+            )
         );
         assert_eq!(
             surrounding_word(&snapshot, DisplayPoint::new(1, 7)),
-            DisplayPoint::new(1, 4)..DisplayPoint::new(1, 7)
+            (
+                DisplayPoint::new(1, 4)..DisplayPoint::new(1, 7),
+                Some(CharKind::Word)
+            )
         );
     }
 }

crates/find/src/find.rs 🔗

@@ -296,7 +296,7 @@ impl FindBar {
                     let mut text: String;
                     if selection.start == selection.end {
                         let point = selection.start.to_display_point(&display_map);
-                        let range = editor::movement::surrounding_word(&display_map, point);
+                        let (range, _) = editor::movement::surrounding_word(&display_map, point);
                         let range = range.start.to_offset(&display_map, Bias::Left)
                             ..range.end.to_offset(&display_map, Bias::Right);
                         text = display_map.buffer_snapshot.text_for_range(range).collect();