Filter and sort suggestions in autocomplete

Antonio Scandurra and Nathan Sobo created

Co-Authored-By: Nathan Sobo <nathan@zed.dev>

Change summary

Cargo.lock                          |   2 
crates/editor/Cargo.toml            |   2 
crates/editor/src/editor.rs         | 197 ++++++++++++++++++++----------
crates/editor/src/movement.rs       | 122 ++++--------------
crates/editor/src/multi_buffer.rs   |  49 +++++++
crates/find/src/find.rs             |   2 
crates/fuzzy/src/fuzzy.rs           |   4 
crates/gpui/src/fonts.rs            |  15 +
crates/language/src/buffer.rs       |  32 ++++
crates/theme/src/theme.rs           |   1 
crates/zed/assets/themes/_base.toml |   3 
11 files changed, 258 insertions(+), 171 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -1547,12 +1547,14 @@ dependencies = [
  "collections",
  "ctor",
  "env_logger",
+ "fuzzy",
  "gpui",
  "itertools",
  "language",
  "lazy_static",
  "log",
  "lsp",
+ "ordered-float",
  "parking_lot",
  "postage",
  "project",

crates/editor/Cargo.toml 🔗

@@ -19,6 +19,7 @@ test-support = [
 text = { path = "../text" }
 clock = { path = "../clock" }
 collections = { path = "../collections" }
+fuzzy = { path = "../fuzzy" }
 gpui = { path = "../gpui" }
 language = { path = "../language" }
 project = { path = "../project" }
@@ -31,6 +32,7 @@ anyhow = "1.0"
 itertools = "0.10"
 lazy_static = "1.4"
 log = "0.4"
+ordered-float = "2.1.1"
 parking_lot = "0.11"
 postage = { version = "0.4", features = ["futures-traits"] }
 rand = { version = "0.8.3", optional = true }

crates/editor/src/editor.rs 🔗

@@ -13,6 +13,7 @@ use collections::{BTreeMap, HashMap, HashSet};
 pub use display_map::DisplayPoint;
 use display_map::*;
 pub use element::*;
+use fuzzy::{StringMatch, StringMatchCandidate};
 use gpui::{
     action,
     color::Color,
@@ -31,16 +32,17 @@ use language::{
 };
 use multi_buffer::MultiBufferChunks;
 pub use multi_buffer::{
-    Anchor, AnchorRangeExt, ExcerptId, ExcerptProperties, MultiBuffer, MultiBufferSnapshot,
-    ToOffset, ToPoint,
+    char_kind, Anchor, AnchorRangeExt, CharKind, ExcerptId, ExcerptProperties, MultiBuffer,
+    MultiBufferSnapshot, ToOffset, ToPoint,
 };
+use ordered_float::OrderedFloat;
 use postage::watch;
 use serde::{Deserialize, Serialize};
 use smallvec::SmallVec;
 use smol::Timer;
 use std::{
     any::TypeId,
-    cmp::{self, Ordering},
+    cmp::{self, Ordering, Reverse},
     iter::{self, FromIterator},
     mem,
     ops::{Deref, Range, RangeInclusive, Sub},
@@ -50,7 +52,7 @@ use std::{
 pub use sum_tree::Bias;
 use text::rope::TextDimension;
 use theme::{DiagnosticStyle, EditorStyle};
-use util::post_inc;
+use util::{post_inc, ResultExt};
 use workspace::{ItemNavHistory, PathOpener, Workspace};
 
 const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
@@ -430,6 +432,7 @@ struct BracketPairState {
 struct CompletionState {
     initial_position: Anchor,
     completions: Arc<[Completion<Anchor>]>,
+    matches: Arc<[StringMatch]>,
     selected_item: usize,
     list: UniformListState,
 }
@@ -453,14 +456,6 @@ pub struct NavigationData {
     offset: usize,
 }
 
-#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Debug)]
-pub enum CharKind {
-    Newline,
-    Punctuation,
-    Whitespace,
-    Word,
-}
-
 impl Editor {
     pub fn single_line(build_settings: BuildSettings, cx: &mut ViewContext<Self>) -> Self {
         let buffer = cx.add_model(|cx| Buffer::new(0, String::new(), cx));
@@ -888,7 +883,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());
@@ -991,7 +986,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 {
@@ -1538,27 +1533,90 @@ impl Editor {
             return;
         };
 
+        let query = {
+            let buffer = self.buffer.read(cx).read(cx);
+            let offset = position.to_offset(&buffer);
+            let (word_range, kind) = buffer.surrounding_word(offset);
+            if offset > word_range.start && kind == Some(CharKind::Word) {
+                Some(
+                    buffer
+                        .text_for_range(word_range.start..offset)
+                        .collect::<String>(),
+                )
+            } else {
+                None
+            }
+        };
+
         let completions = self
             .buffer
             .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| {
-                        if this.focused {
-                            this.completion_state = Some(CompletionState {
-                                initial_position: position,
-                                completions: completions.into(),
-                                selected_item: 0,
-                                list: Default::default(),
-                            });
-                            cx.notify();
-                        }
-                    });
+            let candidates = completions
+                .iter()
+                .enumerate()
+                .map(|(id, completion)| {
+                    StringMatchCandidate::new(
+                        id,
+                        completion.label()[completion.filter_range()].into(),
+                    )
+                })
+                .collect::<Vec<_>>();
+            let mut matches = if let Some(query) = query.as_ref() {
+                fuzzy::match_strings(
+                    &candidates,
+                    query,
+                    false,
+                    100,
+                    &Default::default(),
+                    cx.background(),
+                )
+                .await
+            } else {
+                candidates
+                    .into_iter()
+                    .enumerate()
+                    .map(|(candidate_id, candidate)| StringMatch {
+                        candidate_id,
+                        score: Default::default(),
+                        positions: Default::default(),
+                        string: candidate.string,
+                    })
+                    .collect()
+            };
+            matches.sort_unstable_by_key(|mat| {
+                (
+                    Reverse(OrderedFloat(mat.score)),
+                    completions[mat.candidate_id].sort_key(),
+                )
+            });
+
+            for mat in &mut matches {
+                let filter_start = completions[mat.candidate_id].filter_range().start;
+                for position in &mut mat.positions {
+                    *position += filter_start;
                 }
             }
+
+            if let Some(this) = cx.read(|cx| this.upgrade(cx)) {
+                this.update(&mut cx, |this, cx| {
+                    if matches.is_empty() {
+                        this.completion_state.take();
+                    } else if this.focused {
+                        this.completion_state = Some(CompletionState {
+                            initial_position: position,
+                            completions: completions.into(),
+                            matches: matches.into(),
+                            selected_item: 0,
+                            list: Default::default(),
+                        });
+                    }
+
+                    cx.notify();
+                });
+            }
             Ok::<_, anyhow::Error>(())
         })
         .detach_and_log_err(cx);
@@ -1590,21 +1648,33 @@ impl Editor {
             let build_settings = self.build_settings.clone();
             let settings = build_settings(cx);
             let completions = state.completions.clone();
+            let matches = state.matches.clone();
             let selected_item = state.selected_item;
             UniformList::new(
                 state.list.clone(),
-                state.completions.len(),
+                matches.len(),
                 move |range, items, cx| {
                     let settings = build_settings(cx);
                     let start_ix = range.start;
-                    for (ix, completion) in completions[range].iter().enumerate() {
+                    let label_style = LabelStyle {
+                        text: settings.style.text.clone(),
+                        highlight_text: settings
+                            .style
+                            .text
+                            .clone()
+                            .highlight(settings.style.autocomplete.match_highlight, cx.font_cache())
+                            .log_err(),
+                    };
+                    for (ix, mat) in matches[range].iter().enumerate() {
                         let item_style = if start_ix + ix == selected_item {
                             settings.style.autocomplete.selected_item
                         } else {
                             settings.style.autocomplete.item
                         };
+                        let completion = &completions[mat.candidate_id];
                         items.push(
-                            Label::new(completion.label().to_string(), settings.style.text.clone())
+                            Label::new(completion.label().to_string(), label_style.clone())
+                                .with_highlights(mat.positions.clone())
                                 .contained()
                                 .with_style(item_style)
                                 .boxed(),
@@ -1614,10 +1684,12 @@ impl Editor {
             )
             .with_width_from_item(
                 state
-                    .completions
+                    .matches
                     .iter()
                     .enumerate()
-                    .max_by_key(|(_, completion)| completion.label().chars().count())
+                    .max_by_key(|(_, mat)| {
+                        state.completions[mat.candidate_id].label().chars().count()
+                    })
                     .map(|(ix, _)| ix),
             )
             .contained()
@@ -2914,7 +2986,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),
                 );
@@ -3534,8 +3606,7 @@ impl Editor {
     ) where
         T: ToOffset + ToPoint + Ord + std::marker::Copy + std::fmt::Debug,
     {
-        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
-        let buffer = &display_map.buffer_snapshot;
+        let buffer = self.buffer.read(cx).snapshot(cx);
         let old_cursor_position = self.newest_anchor_selection().map(|s| s.head());
         selections.sort_unstable_by_key(|s| s.start);
 
@@ -3580,29 +3651,14 @@ impl Editor {
             }
         }
 
-        let new_cursor_position = selections
-            .iter()
-            .max_by_key(|s| s.id)
-            .map(|s| s.head().to_point(&buffer));
+        let new_cursor_position = selections.iter().max_by_key(|s| s.id).map(|s| s.head());
         if let Some(old_cursor_position) = old_cursor_position {
-            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(new_cursor_position) = new_cursor_position {
+                self.push_to_nav_history(
+                    old_cursor_position,
+                    Some(new_cursor_position.to_point(&buffer)),
+                    cx,
+                );
             }
         }
 
@@ -3628,6 +3684,21 @@ impl Editor {
             })),
             cx,
         );
+
+        if let Some((completion_state, cursor_position)) =
+            self.completion_state.as_ref().zip(new_cursor_position)
+        {
+            let cursor_position = cursor_position.to_offset(&buffer);
+            let (word_range, kind) =
+                buffer.surrounding_word(completion_state.initial_position.clone());
+            if kind == Some(CharKind::Word) && word_range.to_inclusive().contains(&cursor_position)
+            {
+                self.show_completions(&ShowCompletions, cx);
+            } else {
+                self.completion_state.take();
+                cx.notify();
+            }
+        }
     }
 
     /// Compute new ranges for any selections that were located in excerpts that have
@@ -4424,18 +4495,6 @@ pub fn settings_builder(
     })
 }
 
-pub fn char_kind(c: char) -> CharKind {
-    if c == '\n' {
-        CharKind::Newline
-    } else if c.is_whitespace() {
-        CharKind::Whitespace
-    } else if c.is_alphanumeric() || c == '_' {
-        CharKind::Word
-    } else {
-        CharKind::Punctuation
-    }
-}
-
 #[cfg(test)]
 mod tests {
     use super::*;

crates/editor/src/movement.rs 🔗

@@ -1,7 +1,7 @@
 use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint};
 use crate::{char_kind, CharKind, ToPoint};
 use anyhow::Result;
-use std::{cmp, ops::Range};
+use std::ops::Range;
 
 pub fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> Result<DisplayPoint> {
     if point.column() > 0 {
@@ -183,42 +183,20 @@ 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>, Option<CharKind>) {
-    let mut start = map.clip_point(point, Bias::Left).to_offset(map, Bias::Left);
-    let mut end = start;
-
-    let text = &map.buffer_snapshot;
-    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),
-        word_kind,
-    )
+pub fn surrounding_word(map: &DisplaySnapshot, position: DisplayPoint) -> Range<DisplayPoint> {
+    let position = map
+        .clip_point(position, Bias::Left)
+        .to_offset(map, Bias::Left);
+    let (range, _) = map.buffer_snapshot.surrounding_word(position);
+    let start = range
+        .start
+        .to_point(&map.buffer_snapshot)
+        .to_display_point(map);
+    let end = range
+        .end
+        .to_point(&map.buffer_snapshot)
+        .to_display_point(map);
+    start..end
 }
 
 #[cfg(test)]
@@ -412,101 +390,59 @@ mod tests {
 
         assert_eq!(
             surrounding_word(&snapshot, DisplayPoint::new(0, 0)),
-            (
-                DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5),
-                Some(CharKind::Word)
-            )
+            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),
-                Some(CharKind::Word)
-            )
+            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),
-                Some(CharKind::Word)
-            )
+            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),
-                Some(CharKind::Word)
-            )
+            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),
-                Some(CharKind::Word)
-            )
+            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),
-                Some(CharKind::Word)
-            )
+            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),
-                Some(CharKind::Whitespace)
-            )
+            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),
-                Some(CharKind::Word)
-            )
+            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),
-                Some(CharKind::Word)
-            )
+            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),
-                Some(CharKind::Word)
-            )
+            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),
-                Some(CharKind::Whitespace)
-            )
+            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),
-                Some(CharKind::Whitespace)
-            )
+            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),
-                Some(CharKind::Word)
-            )
+            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),
-                Some(CharKind::Word)
-            )
+            DisplayPoint::new(1, 4)..DisplayPoint::new(1, 7),
         );
     }
 }

crates/editor/src/multi_buffer.rs 🔗

@@ -50,6 +50,14 @@ struct History {
     group_interval: Duration,
 }
 
+#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Debug)]
+pub enum CharKind {
+    Newline,
+    Punctuation,
+    Whitespace,
+    Word,
+}
+
 struct Transaction {
     id: usize,
     buffer_transactions: HashSet<(usize, text::TransactionId)>,
@@ -1155,6 +1163,35 @@ impl MultiBufferSnapshot {
                 .eq(needle.bytes())
     }
 
+    pub fn surrounding_word<T: ToOffset>(&self, start: T) -> (Range<usize>, Option<CharKind>) {
+        let mut start = start.to_offset(self);
+        let mut end = start;
+        let mut next_chars = self.chars_at(start).peekable();
+        let mut prev_chars = self.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..end, word_kind)
+    }
+
     fn as_singleton(&self) -> Option<&Excerpt> {
         if self.singleton {
             self.excerpts.iter().next()
@@ -2418,6 +2455,18 @@ impl ToPoint for Point {
     }
 }
 
+pub fn char_kind(c: char) -> CharKind {
+    if c == '\n' {
+        CharKind::Newline
+    } else if c.is_whitespace() {
+        CharKind::Whitespace
+    } else if c.is_alphanumeric() || c == '_' {
+        CharKind::Word
+    } else {
+        CharKind::Punctuation
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;

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();

crates/fuzzy/src/fuzzy.rs 🔗

@@ -181,6 +181,10 @@ pub async fn match_strings(
     cancel_flag: &AtomicBool,
     background: Arc<executor::Background>,
 ) -> Vec<StringMatch> {
+    if candidates.is_empty() {
+        return Default::default();
+    }
+
     let lowercase_query = query.to_lowercase().chars().collect::<Vec<_>>();
     let query = query.chars().collect::<Vec<_>>();
 

crates/gpui/src/fonts.rs 🔗

@@ -5,7 +5,7 @@ use crate::{
     text_layout::RunStyle,
     FontCache,
 };
-use anyhow::anyhow;
+use anyhow::{anyhow, Result};
 pub use font_kit::{
     metrics::Metrics,
     properties::{Properties, Stretch, Style, Weight},
@@ -107,7 +107,7 @@ impl TextStyle {
         underline: Option<Underline>,
         color: Color,
         font_cache: &FontCache,
-    ) -> anyhow::Result<Self> {
+    ) -> Result<Self> {
         let font_family_name = font_family_name.into();
         let font_family_id = font_cache.load_family(&[&font_family_name])?;
         let font_id = font_cache.select_font(font_family_id, &font_properties)?;
@@ -127,6 +127,15 @@ impl TextStyle {
         self
     }
 
+    pub fn highlight(mut self, style: HighlightStyle, font_cache: &FontCache) -> Result<Self> {
+        if self.font_properties != style.font_properties {
+            self.font_id = font_cache.select_font(self.font_family_id, &style.font_properties)?;
+        }
+        self.color = style.color;
+        self.underline = style.underline;
+        Ok(self)
+    }
+
     pub fn to_run(&self) -> RunStyle {
         RunStyle {
             font_id: self.font_id,
@@ -135,7 +144,7 @@ impl TextStyle {
         }
     }
 
-    fn from_json(json: TextStyleJson) -> anyhow::Result<Self> {
+    fn from_json(json: TextStyleJson) -> Result<Self> {
         FONT_CACHE.with(|font_cache| {
             if let Some(font_cache) = font_cache.borrow().as_ref() {
                 let font_properties = properties_from_json(json.weight, json.italic);

crates/language/src/buffer.rs 🔗

@@ -114,6 +114,7 @@ pub struct Diagnostic {
     pub is_disk_based: bool,
 }
 
+#[derive(Debug)]
 pub struct Completion<T> {
     pub old_range: Range<T>,
     pub new_text: String,
@@ -230,7 +231,7 @@ impl File for FakeFile {
     }
 
     fn path(&self) -> &Arc<Path> {
-        &self.path        
+        &self.path
     }
 
     fn full_path(&self, _: &AppContext) -> PathBuf {
@@ -255,8 +256,11 @@ impl File for FakeFile {
         cx.spawn(|_| async move { Ok((Default::default(), SystemTime::UNIX_EPOCH)) })
     }
 
-    fn format_remote(&self, buffer_id: u64, cx: &mut MutableAppContext)
-        -> Option<Task<Result<()>>> {
+    fn format_remote(
+        &self,
+        buffer_id: u64,
+        cx: &mut MutableAppContext,
+    ) -> Option<Task<Result<()>>> {
         None
     }
 
@@ -1759,7 +1763,7 @@ impl Buffer {
                         };
 
                         let old_range = this.anchor_before(old_range.start)..this.anchor_after(old_range.end);
-    
+
                         Some(Completion {
                             old_range,
                             new_text,
@@ -2511,6 +2515,26 @@ impl<T> Completion<T> {
     pub fn label(&self) -> &str {
         &self.lsp_completion.label
     }
+
+    pub fn filter_range(&self) -> Range<usize> {
+        if let Some(filter_text) = self.lsp_completion.filter_text.as_deref() {
+            if let Some(start) = self.label().find(filter_text) {
+                start..start + filter_text.len()
+            } else {
+                0..self.label().len()
+            }
+        } else {
+            0..self.label().len()
+        }
+    }
+
+    pub fn sort_key(&self) -> (usize, &str) {
+        let kind_key = match self.lsp_completion.kind {
+            Some(lsp::CompletionItemKind::VARIABLE) => 0,
+            _ => 1,
+        };
+        (kind_key, &self.label()[self.filter_range()])
+    }
 }
 
 pub fn contiguous_ranges(

crates/theme/src/theme.rs 🔗

@@ -328,6 +328,7 @@ pub struct AutocompleteStyle {
     pub container: ContainerStyle,
     pub item: ContainerStyle,
     pub selected_item: ContainerStyle,
+    pub match_highlight: HighlightStyle,
 }
 
 #[derive(Clone, Copy, Default, Deserialize)]

crates/zed/assets/themes/_base.toml 🔗

@@ -188,7 +188,7 @@ corner_radius = 6
 
 [project_panel]
 extends = "$panel"
-padding.top = 6    # ($workspace.tab.height - $project_panel.entry.height) / 2
+padding.top = 6 # ($workspace.tab.height - $project_panel.entry.height) / 2
 
 [project_panel.entry]
 text = "$text.1"
@@ -318,6 +318,7 @@ message.highlight_text.color = "$text.3.color"
 background = "$surface.2"
 border = { width = 1, color = "$border.1" }
 item.padding = 2
+match_highlight = { color = "$editor.syntax.keyword.color", weight = "$editor.syntax.keyword.weight" }
 
 [editor.autocomplete.selected_item]
 extends = "$editor.autocomplete.item"