Start work on syntax highlighting completions

Max Brunsfeld created

Change summary

crates/editor/src/editor.rs          | 178 +++++++++++++++++++++++++++--
crates/language/src/buffer.rs        |  24 ---
crates/language/src/highlight_map.rs |   2 
crates/language/src/language.rs      |  64 ++++++++--
crates/language/src/proto.rs         |   8 +
crates/outline/src/outline.rs        | 158 --------------------------
crates/zed/src/language.rs           | 136 ++++++++++++++++++++--
7 files changed, 349 insertions(+), 221 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -20,7 +20,7 @@ use gpui::{
     color::Color,
     elements::*,
     executor,
-    fonts::TextStyle,
+    fonts::{self, HighlightStyle, TextStyle},
     geometry::vector::{vec2f, Vector2F},
     keymap::Binding,
     text_layout, AppContext, ClipboardItem, Element, ElementBox, Entity, ModelHandle,
@@ -489,7 +489,7 @@ impl CompletionState {
         });
 
         for mat in &mut matches {
-            let filter_start = self.completions[mat.candidate_id].filter_range().start;
+            let filter_start = self.completions[mat.candidate_id].label.filter_range.start;
             for position in &mut mat.positions {
                 *position += filter_start;
             }
@@ -1628,7 +1628,7 @@ impl Editor {
                         .map(|(id, completion)| {
                             StringMatchCandidate::new(
                                 id,
-                                completion.lsp_completion.label[completion.filter_range()].into(),
+                                completion.label.text[completion.label.filter_range.clone()].into(),
                             )
                         })
                         .collect(),
@@ -1710,15 +1710,6 @@ impl Editor {
                 move |range, items, cx| {
                     let settings = build_settings(cx);
                     let start_ix = range.start;
-                    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
@@ -1727,8 +1718,20 @@ impl Editor {
                         };
                         let completion = &completions[mat.candidate_id];
                         items.push(
-                            Label::new(completion.label().to_string(), label_style.clone())
-                                .with_highlights(mat.positions.clone())
+                            Text::new(completion.label.text.clone(), settings.style.text.clone())
+                                .with_soft_wrap(false)
+                                .with_highlights(combine_syntax_and_fuzzy_match_highlights(
+                                    &completion.label.text,
+                                    settings.style.text.color.into(),
+                                    completion.label.runs.iter().filter_map(
+                                        |(range, highlight_id)| {
+                                            highlight_id
+                                                .style(&settings.style.syntax)
+                                                .map(|style| (range.clone(), style))
+                                        },
+                                    ),
+                                    &mat.positions,
+                                ))
                                 .contained()
                                 .with_style(item_style)
                                 .boxed(),
@@ -1742,7 +1745,11 @@ impl Editor {
                     .iter()
                     .enumerate()
                     .max_by_key(|(_, mat)| {
-                        state.completions[mat.candidate_id].label().chars().count()
+                        state.completions[mat.candidate_id]
+                            .label
+                            .text
+                            .chars()
+                            .count()
                     })
                     .map(|(ix, _)| ix),
             )
@@ -4699,6 +4706,77 @@ pub fn settings_builder(
     })
 }
 
+pub fn combine_syntax_and_fuzzy_match_highlights(
+    text: &str,
+    default_style: HighlightStyle,
+    syntax_ranges: impl Iterator<Item = (Range<usize>, HighlightStyle)>,
+    match_indices: &[usize],
+) -> Vec<(Range<usize>, HighlightStyle)> {
+    let mut result = Vec::new();
+    let mut match_indices = match_indices.iter().copied().peekable();
+
+    for (range, mut syntax_highlight) in syntax_ranges.chain([(usize::MAX..0, Default::default())])
+    {
+        syntax_highlight.font_properties.weight(Default::default());
+
+        // Add highlights for any fuzzy match characters before the next
+        // syntax highlight range.
+        while let Some(&match_index) = match_indices.peek() {
+            if match_index >= range.start {
+                break;
+            }
+            match_indices.next();
+            let end_index = char_ix_after(match_index, text);
+            let mut match_style = default_style;
+            match_style.font_properties.weight(fonts::Weight::BOLD);
+            result.push((match_index..end_index, match_style));
+        }
+
+        if range.start == usize::MAX {
+            break;
+        }
+
+        // Add highlights for any fuzzy match characters within the
+        // syntax highlight range.
+        let mut offset = range.start;
+        while let Some(&match_index) = match_indices.peek() {
+            if match_index >= range.end {
+                break;
+            }
+
+            match_indices.next();
+            if match_index > offset {
+                result.push((offset..match_index, syntax_highlight));
+            }
+
+            let mut end_index = char_ix_after(match_index, text);
+            while let Some(&next_match_index) = match_indices.peek() {
+                if next_match_index == end_index && next_match_index < range.end {
+                    end_index = char_ix_after(next_match_index, text);
+                    match_indices.next();
+                } else {
+                    break;
+                }
+            }
+
+            let mut match_style = syntax_highlight;
+            match_style.font_properties.weight(fonts::Weight::BOLD);
+            result.push((match_index..end_index, match_style));
+            offset = end_index;
+        }
+
+        if offset < range.end {
+            result.push((offset..range.end, syntax_highlight));
+        }
+    }
+
+    fn char_ix_after(ix: usize, text: &str) -> usize {
+        ix + text[ix..].chars().next().unwrap().len_utf8()
+    }
+
+    result
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -7327,6 +7405,76 @@ mod tests {
         });
     }
 
+    #[test]
+    fn test_combine_syntax_and_fuzzy_match_highlights() {
+        let string = "abcdefghijklmnop";
+        let default = HighlightStyle::default();
+        let syntax_ranges = [
+            (
+                0..3,
+                HighlightStyle {
+                    color: Color::red(),
+                    ..default
+                },
+            ),
+            (
+                4..8,
+                HighlightStyle {
+                    color: Color::green(),
+                    ..default
+                },
+            ),
+        ];
+        let match_indices = [4, 6, 7, 8];
+        assert_eq!(
+            combine_syntax_and_fuzzy_match_highlights(
+                &string,
+                default,
+                syntax_ranges.into_iter(),
+                &match_indices,
+            ),
+            &[
+                (
+                    0..3,
+                    HighlightStyle {
+                        color: Color::red(),
+                        ..default
+                    },
+                ),
+                (
+                    4..5,
+                    HighlightStyle {
+                        color: Color::green(),
+                        font_properties: *fonts::Properties::default().weight(fonts::Weight::BOLD),
+                        ..default
+                    },
+                ),
+                (
+                    5..6,
+                    HighlightStyle {
+                        color: Color::green(),
+                        ..default
+                    },
+                ),
+                (
+                    6..8,
+                    HighlightStyle {
+                        color: Color::green(),
+                        font_properties: *fonts::Properties::default().weight(fonts::Weight::BOLD),
+                        ..default
+                    },
+                ),
+                (
+                    8..9,
+                    HighlightStyle {
+                        font_properties: *fonts::Properties::default().weight(fonts::Weight::BOLD),
+                        ..default
+                    },
+                ),
+            ]
+        );
+    }
+
     fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
         let point = DisplayPoint::new(row as u32, column as u32);
         point..point

crates/language/src/buffer.rs 🔗

@@ -7,7 +7,7 @@ pub use crate::{
 use crate::{
     diagnostic_set::{DiagnosticEntry, DiagnosticGroup},
     outline::OutlineItem,
-    range_from_lsp, Outline, ToLspPosition,
+    range_from_lsp, CompletionLabel, Outline, ToLspPosition,
 };
 use anyhow::{anyhow, Result};
 use clock::ReplicaId;
@@ -114,7 +114,7 @@ pub struct Diagnostic {
 pub struct Completion<T> {
     pub old_range: Range<T>,
     pub new_text: String,
-    pub label: Option<String>,
+    pub label: CompletionLabel,
     pub lsp_completion: lsp::CompletionItem,
 }
 
@@ -1829,7 +1829,7 @@ impl Buffer {
                             Some(Completion {
                                 old_range: this.anchor_before(old_range.start)..this.anchor_after(old_range.end),
                                 new_text,
-                                label: language.as_ref().and_then(|l| l.label_for_completion(&lsp_completion)),
+                                label: language.as_ref().and_then(|l| l.label_for_completion(&lsp_completion)).unwrap_or_else(|| CompletionLabel::plain(&lsp_completion)),
                                 lsp_completion,
                             })
                         } else {
@@ -2664,28 +2664,12 @@ impl Default for Diagnostic {
 }
 
 impl<T> Completion<T> {
-    pub fn label(&self) -> &str {
-        self.label.as_deref().unwrap_or(&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()])
+        (kind_key, &self.label.text[self.label.filter_range.clone()])
     }
 
     pub fn is_snippet(&self) -> bool {

crates/language/src/highlight_map.rs 🔗

@@ -5,7 +5,7 @@ use theme::SyntaxTheme;
 #[derive(Clone, Debug)]
 pub struct HighlightMap(Arc<[HighlightId]>);
 
-#[derive(Clone, Copy, Debug)]
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
 pub struct HighlightId(pub u32);
 
 const DEFAULT_HIGHLIGHT_ID: HighlightId = HighlightId(u32::MAX);

crates/language/src/language.rs 🔗

@@ -49,11 +49,23 @@ pub trait ToLspPosition {
 
 pub trait LspPostProcessor: 'static + Send + Sync {
     fn process_diagnostics(&self, diagnostics: &mut lsp::PublishDiagnosticsParams);
-    fn label_for_completion(&self, _completion: &lsp::CompletionItem) -> Option<String> {
+    fn label_for_completion(
+        &self,
+        _: &lsp::CompletionItem,
+        _: &Language,
+    ) -> Option<CompletionLabel> {
         None
     }
 }
 
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct CompletionLabel {
+    pub text: String,
+    pub runs: Vec<(Range<usize>, HighlightId)>,
+    pub filter_range: Range<usize>,
+    pub left_aligned_len: usize,
+}
+
 #[derive(Default, Deserialize)]
 pub struct LanguageConfig {
     pub name: String,
@@ -253,24 +265,26 @@ impl Language {
         }
     }
 
-    pub fn label_for_completion(&self, completion: &lsp::CompletionItem) -> Option<String> {
+    pub fn label_for_completion(
+        &self,
+        completion: &lsp::CompletionItem,
+    ) -> Option<CompletionLabel> {
         self.lsp_post_processor
-            .as_ref()
-            .and_then(|p| p.label_for_completion(completion))
+            .as_ref()?
+            .label_for_completion(completion, self)
     }
 
-    pub fn highlight_text<'a>(&'a self, text: &'a Rope) -> Vec<(Range<usize>, HighlightId)> {
+    pub fn highlight_text<'a>(
+        &'a self,
+        text: &'a Rope,
+        range: Range<usize>,
+    ) -> Vec<(Range<usize>, HighlightId)> {
         let mut result = Vec::new();
         if let Some(grammar) = &self.grammar {
             let tree = grammar.parse_text(text, None);
             let mut offset = 0;
-            for chunk in BufferChunks::new(
-                text,
-                0..text.len(),
-                Some(&tree),
-                self.grammar.as_ref(),
-                vec![],
-            ) {
+            for chunk in BufferChunks::new(text, range, Some(&tree), self.grammar.as_ref(), vec![])
+            {
                 let end_offset = offset + chunk.text.len();
                 if let Some(highlight_id) = chunk.highlight_id {
                     result.push((offset..end_offset, highlight_id));
@@ -291,6 +305,10 @@ impl Language {
                 HighlightMap::new(grammar.highlights_query.capture_names(), theme);
         }
     }
+
+    pub fn grammar(&self) -> Option<&Arc<Grammar>> {
+        self.grammar.as_ref()
+    }
 }
 
 impl Grammar {
@@ -316,6 +334,28 @@ impl Grammar {
     pub fn highlight_map(&self) -> HighlightMap {
         self.highlight_map.lock().clone()
     }
+
+    pub fn highlight_id_for_name(&self, name: &str) -> Option<HighlightId> {
+        let capture_id = self.highlights_query.capture_index_for_name(name)?;
+        Some(self.highlight_map.lock().get(capture_id))
+    }
+}
+
+impl CompletionLabel {
+    fn plain(completion: &lsp::CompletionItem) -> Self {
+        let mut result = Self {
+            text: completion.label.clone(),
+            runs: Vec::new(),
+            left_aligned_len: completion.label.len(),
+            filter_range: 0..completion.label.len(),
+        };
+        if let Some(filter_text) = &completion.filter_text {
+            if let Some(ix) = completion.label.find(filter_text) {
+                result.filter_range = ix..ix + filter_text.len();
+            }
+        }
+        result
+    }
 }
 
 #[cfg(any(test, feature = "test-support"))]

crates/language/src/proto.rs 🔗

@@ -1,4 +1,6 @@
-use crate::{diagnostic_set::DiagnosticEntry, Completion, Diagnostic, Language, Operation};
+use crate::{
+    diagnostic_set::DiagnosticEntry, Completion, CompletionLabel, Diagnostic, Language, Operation,
+};
 use anyhow::{anyhow, Result};
 use clock::ReplicaId;
 use collections::HashSet;
@@ -403,7 +405,9 @@ pub fn deserialize_completion(
     Ok(Completion {
         old_range: old_start..old_end,
         new_text: completion.new_text,
-        label: language.and_then(|l| l.label_for_completion(&lsp_completion)),
+        label: language
+            .and_then(|l| l.label_for_completion(&lsp_completion))
+            .unwrap_or(CompletionLabel::plain(&lsp_completion)),
         lsp_completion,
     })
 }

crates/outline/src/outline.rs 🔗

@@ -1,12 +1,11 @@
 use editor::{
-    display_map::ToDisplayPoint, Anchor, AnchorRangeExt, Autoscroll, DisplayPoint, Editor,
-    EditorSettings, ToPoint,
+    combine_syntax_and_fuzzy_match_highlights, display_map::ToDisplayPoint, Anchor, AnchorRangeExt,
+    Autoscroll, DisplayPoint, Editor, EditorSettings, ToPoint,
 };
 use fuzzy::StringMatch;
 use gpui::{
     action,
     elements::*,
-    fonts::{self, HighlightStyle},
     geometry::vector::Vector2F,
     keymap::{self, Binding},
     AppContext, Axis, Entity, MutableAppContext, RenderContext, View, ViewContext, ViewHandle,
@@ -17,7 +16,6 @@ use ordered_float::OrderedFloat;
 use postage::watch;
 use std::{
     cmp::{self, Reverse},
-    ops::Range,
     sync::Arc,
 };
 use workspace::{
@@ -362,7 +360,7 @@ impl OutlineView {
             .with_highlights(combine_syntax_and_fuzzy_match_highlights(
                 &outline_item.text,
                 style.label.text.clone().into(),
-                &outline_item.highlight_ranges,
+                outline_item.highlight_ranges.iter().cloned(),
                 &string_match.positions,
             ))
             .contained()
@@ -372,153 +370,3 @@ impl OutlineView {
             .boxed()
     }
 }
-
-fn combine_syntax_and_fuzzy_match_highlights(
-    text: &str,
-    default_style: HighlightStyle,
-    syntax_ranges: &[(Range<usize>, HighlightStyle)],
-    match_indices: &[usize],
-) -> Vec<(Range<usize>, HighlightStyle)> {
-    let mut result = Vec::new();
-    let mut match_indices = match_indices.iter().copied().peekable();
-
-    for (range, mut syntax_highlight) in syntax_ranges
-        .iter()
-        .cloned()
-        .chain([(usize::MAX..0, Default::default())])
-    {
-        syntax_highlight.font_properties.weight(Default::default());
-
-        // Add highlights for any fuzzy match characters before the next
-        // syntax highlight range.
-        while let Some(&match_index) = match_indices.peek() {
-            if match_index >= range.start {
-                break;
-            }
-            match_indices.next();
-            let end_index = char_ix_after(match_index, text);
-            let mut match_style = default_style;
-            match_style.font_properties.weight(fonts::Weight::BOLD);
-            result.push((match_index..end_index, match_style));
-        }
-
-        if range.start == usize::MAX {
-            break;
-        }
-
-        // Add highlights for any fuzzy match characters within the
-        // syntax highlight range.
-        let mut offset = range.start;
-        while let Some(&match_index) = match_indices.peek() {
-            if match_index >= range.end {
-                break;
-            }
-
-            match_indices.next();
-            if match_index > offset {
-                result.push((offset..match_index, syntax_highlight));
-            }
-
-            let mut end_index = char_ix_after(match_index, text);
-            while let Some(&next_match_index) = match_indices.peek() {
-                if next_match_index == end_index && next_match_index < range.end {
-                    end_index = char_ix_after(next_match_index, text);
-                    match_indices.next();
-                } else {
-                    break;
-                }
-            }
-
-            let mut match_style = syntax_highlight;
-            match_style.font_properties.weight(fonts::Weight::BOLD);
-            result.push((match_index..end_index, match_style));
-            offset = end_index;
-        }
-
-        if offset < range.end {
-            result.push((offset..range.end, syntax_highlight));
-        }
-    }
-
-    result
-}
-
-fn char_ix_after(ix: usize, text: &str) -> usize {
-    ix + text[ix..].chars().next().unwrap().len_utf8()
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use gpui::{color::Color, fonts::HighlightStyle};
-
-    #[test]
-    fn test_combine_syntax_and_fuzzy_match_highlights() {
-        let string = "abcdefghijklmnop";
-        let default = HighlightStyle::default();
-        let syntax_ranges = [
-            (
-                0..3,
-                HighlightStyle {
-                    color: Color::red(),
-                    ..default
-                },
-            ),
-            (
-                4..8,
-                HighlightStyle {
-                    color: Color::green(),
-                    ..default
-                },
-            ),
-        ];
-        let match_indices = [4, 6, 7, 8];
-        assert_eq!(
-            combine_syntax_and_fuzzy_match_highlights(
-                &string,
-                default,
-                &syntax_ranges,
-                &match_indices,
-            ),
-            &[
-                (
-                    0..3,
-                    HighlightStyle {
-                        color: Color::red(),
-                        ..default
-                    },
-                ),
-                (
-                    4..5,
-                    HighlightStyle {
-                        color: Color::green(),
-                        font_properties: *fonts::Properties::default().weight(fonts::Weight::BOLD),
-                        ..default
-                    },
-                ),
-                (
-                    5..6,
-                    HighlightStyle {
-                        color: Color::green(),
-                        ..default
-                    },
-                ),
-                (
-                    6..8,
-                    HighlightStyle {
-                        color: Color::green(),
-                        font_properties: *fonts::Properties::default().weight(fonts::Weight::BOLD),
-                        ..default
-                    },
-                ),
-                (
-                    8..9,
-                    HighlightStyle {
-                        font_properties: *fonts::Properties::default().weight(fonts::Weight::BOLD),
-                        ..default
-                    },
-                ),
-            ]
-        );
-    }
-}

crates/zed/src/language.rs 🔗

@@ -32,18 +32,36 @@ impl LspPostProcessor for RustPostProcessor {
         }
     }
 
-    fn label_for_completion(&self, completion: &lsp::CompletionItem) -> Option<String> {
+    fn label_for_completion(
+        &self,
+        completion: &lsp::CompletionItem,
+        language: &Language,
+    ) -> Option<CompletionLabel> {
         let detail = completion.detail.as_ref()?;
         match completion.kind {
-            Some(
-                lsp::CompletionItemKind::CONSTANT
-                | lsp::CompletionItemKind::FIELD
-                | lsp::CompletionItemKind::VARIABLE,
-            ) => {
-                let mut label = completion.label.clone();
-                label.push_str(": ");
-                label.push_str(detail);
-                Some(label)
+            Some(lsp::CompletionItemKind::FIELD) => {
+                let name = &completion.label;
+                let text = format!("{}: {}", name, detail);
+                let source = Rope::from(format!("struct S {{ {} }}", text).as_str());
+                let runs = language.highlight_text(&source, 11..11 + text.len());
+                return Some(CompletionLabel {
+                    text,
+                    runs,
+                    filter_range: 0..name.len(),
+                    left_aligned_len: name.len(),
+                });
+            }
+            Some(lsp::CompletionItemKind::CONSTANT | lsp::CompletionItemKind::VARIABLE) => {
+                let name = &completion.label;
+                let text = format!("{}: {}", name, detail);
+                let source = Rope::from(format!("let {} = ();", text).as_str());
+                let runs = language.highlight_text(&source, 4..4 + text.len());
+                return Some(CompletionLabel {
+                    text,
+                    runs,
+                    filter_range: 0..name.len(),
+                    left_aligned_len: name.len(),
+                });
             }
             Some(lsp::CompletionItemKind::FUNCTION | lsp::CompletionItemKind::METHOD) => {
                 lazy_static! {
@@ -51,13 +69,20 @@ impl LspPostProcessor for RustPostProcessor {
                 }
 
                 if detail.starts_with("fn(") {
-                    Some(REGEX.replace(&completion.label, &detail[2..]).to_string())
-                } else {
-                    None
+                    let text = REGEX.replace(&completion.label, &detail[2..]).to_string();
+                    let source = Rope::from(format!("fn {} {{}}", text).as_str());
+                    let runs = language.highlight_text(&source, 3..3 + text.len());
+                    return Some(CompletionLabel {
+                        left_aligned_len: text.find("->").unwrap_or(text.len()),
+                        filter_range: 0..completion.label.find('(').unwrap_or(text.len()),
+                        text,
+                        runs,
+                    });
                 }
             }
-            _ => None,
+            _ => {}
         }
+        None
     }
 }
 
@@ -100,9 +125,10 @@ fn load_query(path: &str) -> Cow<'static, str> {
 
 #[cfg(test)]
 mod tests {
+    use super::*;
+    use gpui::color::Color;
     use language::LspPostProcessor;
-
-    use super::RustPostProcessor;
+    use theme::SyntaxTheme;
 
     #[test]
     fn test_process_rust_diagnostics() {
@@ -144,4 +170,82 @@ mod tests {
             "cannot borrow `self.d` as mutable\n`self` is a `&` reference"
         );
     }
+
+    #[test]
+    fn test_process_rust_completions() {
+        let language = rust();
+        let grammar = language.grammar().unwrap();
+        let theme = SyntaxTheme::new(vec![
+            ("type".into(), Color::green().into()),
+            ("keyword".into(), Color::blue().into()),
+            ("function".into(), Color::red().into()),
+            ("property".into(), Color::white().into()),
+        ]);
+
+        language.set_theme(&theme);
+
+        let highlight_function = grammar.highlight_id_for_name("function").unwrap();
+        let highlight_type = grammar.highlight_id_for_name("type").unwrap();
+        let highlight_keyword = grammar.highlight_id_for_name("keyword").unwrap();
+        let highlight_field = grammar.highlight_id_for_name("property").unwrap();
+
+        assert_eq!(
+            language.label_for_completion(&lsp::CompletionItem {
+                kind: Some(lsp::CompletionItemKind::FUNCTION),
+                label: "hello(…)".to_string(),
+                detail: Some("fn(&mut Option<T>) -> Vec<T>".to_string()),
+                ..Default::default()
+            }),
+            Some(CompletionLabel {
+                text: "hello(&mut Option<T>) -> Vec<T>".to_string(),
+                filter_range: 0..5,
+                runs: vec![
+                    (0..5, highlight_function),
+                    (7..10, highlight_keyword),
+                    (11..17, highlight_type),
+                    (18..19, highlight_type),
+                    (25..28, highlight_type),
+                    (29..30, highlight_type),
+                ],
+                left_aligned_len: 22,
+            })
+        );
+
+        assert_eq!(
+            language.label_for_completion(&lsp::CompletionItem {
+                kind: Some(lsp::CompletionItemKind::FIELD),
+                label: "len".to_string(),
+                detail: Some("usize".to_string()),
+                ..Default::default()
+            }),
+            Some(CompletionLabel {
+                text: "len: usize".to_string(),
+                filter_range: 0..3,
+                runs: vec![(0..3, highlight_field), (5..10, highlight_type),],
+                left_aligned_len: 3,
+            })
+        );
+
+        assert_eq!(
+            language.label_for_completion(&lsp::CompletionItem {
+                kind: Some(lsp::CompletionItemKind::FUNCTION),
+                label: "hello(…)".to_string(),
+                detail: Some("fn(&mut Option<T>) -> Vec<T>".to_string()),
+                ..Default::default()
+            }),
+            Some(CompletionLabel {
+                text: "hello(&mut Option<T>) -> Vec<T>".to_string(),
+                filter_range: 0..5,
+                runs: vec![
+                    (0..5, highlight_function),
+                    (7..10, highlight_keyword),
+                    (11..17, highlight_type),
+                    (18..19, highlight_type),
+                    (25..28, highlight_type),
+                    (29..30, highlight_type),
+                ],
+                left_aligned_len: 22,
+            })
+        );
+    }
 }