language: Assert `CodeLabel` text ranges are correct (#40242)

Lukas Wirth created

Release Notes:

- N/A *or* Added/Fixed/Improved ...

Change summary

crates/agent_ui/src/acp/completion_provider.rs                |   8 
crates/agent_ui/src/context_picker/completion_provider.rs     |  13 
crates/assistant_slash_command/src/assistant_slash_command.rs |   7 
crates/assistant_slash_commands/src/file_command.rs           |   8 
crates/assistant_slash_commands/src/tab_command.rs            |   8 
crates/debugger_ui/src/session/running/console.rs             |  12 
crates/editor/src/code_context_menus.rs                       |   6 
crates/editor/src/editor.rs                                   |   6 
crates/editor/src/editor_tests.rs                             |   7 
crates/file_finder/src/open_path_prompt.rs                    |   4 
crates/gpui/src/elements/text.rs                              |  20 
crates/gpui/src/text_system/line_wrapper.rs                   |   6 
crates/language/src/language.rs                               |  72 ++
crates/language_extension/src/extension_lsp_adapter.rs        |  12 
crates/languages/src/c.rs                                     |  26 
crates/languages/src/go.rs                                    |  76 +--
crates/languages/src/python.rs                                |  71 +-
crates/languages/src/rust.rs                                  | 114 ++--
crates/languages/src/typescript.rs                            |  13 
crates/languages/src/vtsls.rs                                 |  13 
crates/project/src/lsp_store.rs                               |  32 
21 files changed, 245 insertions(+), 289 deletions(-)

Detailed changes

crates/agent_ui/src/acp/completion_provider.rs 🔗

@@ -12,7 +12,7 @@ use anyhow::Result;
 use editor::{CompletionProvider, Editor, ExcerptId};
 use fuzzy::{StringMatch, StringMatchCandidate};
 use gpui::{App, Entity, Task, WeakEntity};
-use language::{Buffer, CodeLabel, HighlightId};
+use language::{Buffer, CodeLabel, CodeLabelBuilder, HighlightId};
 use lsp::CompletionContext;
 use project::lsp_store::{CompletionDocumentation, SymbolLocation};
 use project::{
@@ -673,7 +673,7 @@ impl ContextPickerCompletionProvider {
 
 fn build_code_label_for_full_path(file_name: &str, directory: Option<&str>, cx: &App) -> CodeLabel {
     let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
-    let mut label = CodeLabel::default();
+    let mut label = CodeLabelBuilder::default();
 
     label.push_str(file_name, None);
     label.push_str(" ", None);
@@ -682,9 +682,7 @@ fn build_code_label_for_full_path(file_name: &str, directory: Option<&str>, cx:
         label.push_str(directory, comment_id);
     }
 
-    label.filter_range = 0..label.text().len();
-
-    label
+    label.build()
 }
 
 impl CompletionProvider for ContextPickerCompletionProvider {

crates/agent_ui/src/context_picker/completion_provider.rs 🔗

@@ -11,7 +11,7 @@ use fuzzy::{StringMatch, StringMatchCandidate};
 use gpui::{App, Entity, Task, WeakEntity};
 use http_client::HttpClientWithUrl;
 use itertools::Itertools;
-use language::{Buffer, CodeLabel, HighlightId};
+use language::{Buffer, CodeLabel, CodeLabelBuilder, HighlightId};
 use lsp::CompletionContext;
 use project::lsp_store::SymbolLocation;
 use project::{
@@ -686,7 +686,8 @@ impl ContextPickerCompletionProvider {
         };
 
         let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
-        let mut label = CodeLabel::plain(symbol.name.clone(), None);
+        let mut label = CodeLabelBuilder::default();
+        label.push_str(&symbol.name, None);
         label.push_str(" ", None);
         label.push_str(&file_name, comment_id);
         label.push_str(&format!(" L{}", symbol.range.start.0.row + 1), comment_id);
@@ -696,7 +697,7 @@ impl ContextPickerCompletionProvider {
         Some(Completion {
             replace_range: source_range.clone(),
             new_text,
-            label,
+            label: label.build(),
             documentation: None,
             source: project::CompletionSource::Custom,
             icon_path: Some(IconName::Code.path().into()),
@@ -729,7 +730,7 @@ impl ContextPickerCompletionProvider {
 
 fn build_code_label_for_full_path(file_name: &str, directory: Option<&str>, cx: &App) -> CodeLabel {
     let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
-    let mut label = CodeLabel::default();
+    let mut label = CodeLabelBuilder::default();
 
     label.push_str(file_name, None);
     label.push_str(" ", None);
@@ -738,9 +739,7 @@ fn build_code_label_for_full_path(file_name: &str, directory: Option<&str>, cx:
         label.push_str(directory, comment_id);
     }
 
-    label.filter_range = 0..label.text().len();
-
-    label
+    label.build()
 }
 
 impl CompletionProvider for ContextPickerCompletionProvider {

crates/assistant_slash_command/src/assistant_slash_command.rs 🔗

@@ -9,6 +9,7 @@ use anyhow::Result;
 use futures::StreamExt;
 use futures::stream::{self, BoxStream};
 use gpui::{App, SharedString, Task, WeakEntity, Window};
+use language::CodeLabelBuilder;
 use language::HighlightId;
 use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate, OffsetRangeExt};
 pub use language_model::Role;
@@ -328,15 +329,15 @@ impl SlashCommandLine {
 }
 
 pub fn create_label_for_command(command_name: &str, arguments: &[&str], cx: &App) -> CodeLabel {
-    let mut label = CodeLabel::default();
+    let mut label = CodeLabelBuilder::default();
     label.push_str(command_name, None);
+    label.respan_filter_range(None);
     label.push_str(" ", None);
     label.push_str(
         &arguments.join(" "),
         cx.theme().syntax().highlight_id("comment").map(HighlightId),
     );
-    label.filter_range = 0..command_name.len();
-    label
+    label.build()
 }
 
 #[cfg(test)]

crates/assistant_slash_commands/src/file_command.rs 🔗

@@ -7,7 +7,7 @@ use futures::Stream;
 use futures::channel::mpsc;
 use fuzzy::PathMatch;
 use gpui::{App, Entity, Task, WeakEntity};
-use language::{BufferSnapshot, CodeLabel, HighlightId, LineEnding, LspAdapterDelegate};
+use language::{BufferSnapshot, CodeLabelBuilder, HighlightId, LineEnding, LspAdapterDelegate};
 use project::{PathMatchCandidateSet, Project};
 use serde::{Deserialize, Serialize};
 use smol::stream::StreamExt;
@@ -168,7 +168,7 @@ impl SlashCommand for FileSlashCommand {
                         .display(path_style)
                         .to_string();
 
-                    let mut label = CodeLabel::default();
+                    let mut label = CodeLabelBuilder::default();
                     let file_name = path_match.path.file_name()?;
                     let label_text = if path_match.is_dir {
                         format!("{}/ ", file_name)
@@ -178,10 +178,10 @@ impl SlashCommand for FileSlashCommand {
 
                     label.push_str(label_text.as_str(), None);
                     label.push_str(&text, comment_id);
-                    label.filter_range = 0..file_name.len();
+                    label.respan_filter_range(Some(file_name));
 
                     Some(ArgumentCompletion {
-                        label,
+                        label: label.build(),
                         new_text: text,
                         after_completion: AfterCompletion::Compose,
                         replace_previous_arguments: false,

crates/assistant_slash_commands/src/tab_command.rs 🔗

@@ -7,7 +7,7 @@ use collections::{HashMap, HashSet};
 use editor::Editor;
 use futures::future::join_all;
 use gpui::{Task, WeakEntity};
-use language::{BufferSnapshot, CodeLabel, HighlightId, LspAdapterDelegate};
+use language::{BufferSnapshot, CodeLabel, CodeLabelBuilder, HighlightId, LspAdapterDelegate};
 use std::sync::{Arc, atomic::AtomicBool};
 use ui::{ActiveTheme, App, Window, prelude::*};
 use util::{ResultExt, paths::PathStyle};
@@ -308,10 +308,10 @@ fn create_tab_completion_label(
     comment_id: Option<HighlightId>,
 ) -> CodeLabel {
     let (parent_path, file_name) = path_style.split(path);
-    let mut label = CodeLabel::default();
+    let mut label = CodeLabelBuilder::default();
     label.push_str(file_name, None);
     label.push_str(" ", None);
     label.push_str(parent_path.unwrap_or_default(), comment_id);
-    label.filter_range = 0..file_name.len();
-    label
+    label.respan_filter_range(Some(file_name));
+    label.build()
 }

crates/debugger_ui/src/session/running/console.rs 🔗

@@ -669,11 +669,7 @@ impl ConsoleQueryBarCompletionProvider {
                             &snapshot,
                         ),
                         new_text: string_match.string.clone(),
-                        label: CodeLabel {
-                            filter_range: 0..string_match.string.len(),
-                            text: string_match.string.clone(),
-                            runs: Vec::new(),
-                        },
+                        label: CodeLabel::plain(string_match.string.clone(), None),
                         icon_path: None,
                         documentation: Some(CompletionDocumentation::MultiLineMarkdown(
                             variable_value.into(),
@@ -782,11 +778,7 @@ impl ConsoleQueryBarCompletionProvider {
                             &snapshot,
                         ),
                         new_text,
-                        label: CodeLabel {
-                            filter_range: 0..completion.label.len(),
-                            text: completion.label,
-                            runs: Vec::new(),
-                        },
+                        label: CodeLabel::plain(completion.label, None),
                         icon_path: None,
                         documentation: completion.detail.map(|detail| {
                             CompletionDocumentation::MultiLineMarkdown(detail.into())

crates/editor/src/code_context_menus.rs 🔗

@@ -328,11 +328,7 @@ impl CompletionsMenu {
             .map(|choice| Completion {
                 replace_range: selection.start.text_anchor..selection.end.text_anchor,
                 new_text: choice.to_string(),
-                label: CodeLabel {
-                    text: choice.to_string(),
-                    runs: Default::default(),
-                    filter_range: Default::default(),
-                },
+                label: CodeLabel::plain(choice.to_string(), None),
                 icon_path: None,
                 documentation: None,
                 confirm: None,

crates/editor/src/editor.rs 🔗

@@ -23077,11 +23077,7 @@ fn snippet_completions(
                         }),
                         lsp_defaults: None,
                     },
-                    label: CodeLabel {
-                        text: matching_prefix.clone(),
-                        runs: Vec::new(),
-                        filter_range: 0..matching_prefix.len(),
-                    },
+                    label: CodeLabel::plain(matching_prefix.clone(), None),
                     icon_path: None,
                     documentation: Some(CompletionDocumentation::SingleLineAndMultiLinePlainText {
                         single_line: snippet.name.clone().into(),

crates/editor/src/editor_tests.rs 🔗

@@ -14878,12 +14878,7 @@ async fn test_multiline_completion(cx: &mut TestAppContext) {
                 } else {
                     item.label.clone()
                 };
-                let len = text.len();
-                Some(language::CodeLabel {
-                    text,
-                    runs: Vec::new(),
-                    filter_range: 0..len,
-                })
+                Some(language::CodeLabel::plain(text, None))
             })),
             ..FakeLspAdapter::default()
         },

crates/file_finder/src/open_path_prompt.rs 🔗

@@ -755,7 +755,7 @@ impl PickerDelegate for OpenPathDelegate {
                                     .with_default_highlights(
                                         &window.text_style(),
                                         vec![(
-                                            delta..delta + label_len,
+                                            delta..label_len,
                                             HighlightStyle::color(Color::Conflict.color(cx)),
                                         )],
                                     )
@@ -765,7 +765,7 @@ impl PickerDelegate for OpenPathDelegate {
                                     .with_default_highlights(
                                         &window.text_style(),
                                         vec![(
-                                            delta..delta + label_len,
+                                            delta..label_len,
                                             HighlightStyle::color(Color::Created.color(cx)),
                                         )],
                                     )

crates/gpui/src/elements/text.rs 🔗

@@ -180,8 +180,7 @@ impl StyledText {
             "Can't use `with_default_highlights` and `with_highlights`"
         );
         let runs = Self::compute_runs(&self.text, default_style, highlights);
-        self.runs = Some(runs);
-        self
+        self.with_runs(runs)
     }
 
     /// Set the styling attributes for the given text, as well as
@@ -194,7 +193,15 @@ impl StyledText {
             self.runs.is_none(),
             "Can't use `with_highlights` and `with_default_highlights`"
         );
-        self.delayed_highlights = Some(highlights.into_iter().collect::<Vec<_>>());
+        self.delayed_highlights = Some(
+            highlights
+                .into_iter()
+                .inspect(|(run, _)| {
+                    debug_assert!(self.text.is_char_boundary(run.start));
+                    debug_assert!(self.text.is_char_boundary(run.end));
+                })
+                .collect::<Vec<_>>(),
+        );
         self
     }
 
@@ -207,8 +214,10 @@ impl StyledText {
         let mut ix = 0;
         for (range, highlight) in highlights {
             if ix < range.start {
+                debug_assert!(text.is_char_boundary(range.start));
                 runs.push(default_style.clone().to_run(range.start - ix));
             }
+            debug_assert!(text.is_char_boundary(range.end));
             runs.push(
                 default_style
                     .clone()
@@ -225,6 +234,11 @@ impl StyledText {
 
     /// Set the text runs for this piece of text.
     pub fn with_runs(mut self, runs: Vec<TextRun>) -> Self {
+        let mut text = &**self.text;
+        for run in &runs {
+            text = text.get(run.len..).expect("invalid text run");
+        }
+        assert!(text.is_empty(), "invalid text run");
         self.runs = Some(runs);
         self
     }

crates/gpui/src/text_system/line_wrapper.rs 🔗

@@ -225,19 +225,15 @@ impl LineWrapper {
 
 fn update_runs_after_truncation(result: &str, ellipsis: &str, runs: &mut Vec<TextRun>) {
     let mut truncate_at = result.len() - ellipsis.len();
-    let mut run_end = None;
     for (run_index, run) in runs.iter_mut().enumerate() {
         if run.len <= truncate_at {
             truncate_at -= run.len;
         } else {
             run.len = truncate_at + ellipsis.len();
-            run_end = Some(run_index + 1);
+            runs.truncate(run_index + 1);
             break;
         }
     }
-    if let Some(run_end) = run_end {
-        runs.truncate(run_end);
-    }
 }
 
 /// A fragment of a line that can be wrapped.

crates/language/src/language.rs 🔗

@@ -670,6 +670,16 @@ pub struct CodeLabel {
     pub filter_range: Range<usize>,
 }
 
+#[derive(Clone, Debug, Default, PartialEq, Eq)]
+pub struct CodeLabelBuilder {
+    /// The text to display.
+    text: String,
+    /// Syntax highlighting runs.
+    runs: Vec<(Range<usize>, HighlightId)>,
+    /// The portion of the text that should be used in fuzzy filtering.
+    filter_range: Range<usize>,
+}
+
 #[derive(Clone, Deserialize, JsonSchema)]
 pub struct LanguageConfig {
     /// Human-readable name of the language.
@@ -2223,6 +2233,34 @@ impl Grammar {
     }
 }
 
+impl CodeLabelBuilder {
+    pub fn respan_filter_range(&mut self, filter_text: Option<&str>) {
+        self.filter_range = filter_text
+            .and_then(|filter| self.text.find(filter).map(|ix| ix..ix + filter.len()))
+            .unwrap_or(0..self.text.len());
+    }
+
+    pub fn push_str(&mut self, text: &str, highlight: Option<HighlightId>) {
+        let start_ix = self.text.len();
+        self.text.push_str(text);
+        if let Some(highlight) = highlight {
+            let end_ix = self.text.len();
+            self.runs.push((start_ix..end_ix, highlight));
+        }
+    }
+
+    pub fn build(mut self) -> CodeLabel {
+        if self.filter_range.end == 0 {
+            self.respan_filter_range(None);
+        }
+        CodeLabel {
+            text: self.text,
+            runs: self.runs,
+            filter_range: self.filter_range,
+        }
+    }
+}
+
 impl CodeLabel {
     pub fn fallback_for_completion(
         item: &lsp::CompletionItem,
@@ -2286,25 +2324,39 @@ impl CodeLabel {
     }
 
     pub fn plain(text: String, filter_text: Option<&str>) -> Self {
+        Self::filtered(text, filter_text, Vec::new())
+    }
+
+    pub fn filtered(
+        text: String,
+        filter_text: Option<&str>,
+        runs: Vec<(Range<usize>, HighlightId)>,
+    ) -> Self {
         let filter_range = filter_text
             .and_then(|filter| text.find(filter).map(|ix| ix..ix + filter.len()))
             .unwrap_or(0..text.len());
+        Self::new(text, filter_range, runs)
+    }
+
+    pub fn new(
+        text: String,
+        filter_range: Range<usize>,
+        runs: Vec<(Range<usize>, HighlightId)>,
+    ) -> Self {
+        assert!(
+            text.get(filter_range.clone()).is_some(),
+            "invalid filter range"
+        );
+        runs.iter().for_each(|(range, _)| {
+            assert!(text.get(range.clone()).is_some(), "invalid run range");
+        });
         Self {
-            runs: Vec::new(),
+            runs,
             filter_range,
             text,
         }
     }
 
-    pub fn push_str(&mut self, text: &str, highlight: Option<HighlightId>) {
-        let start_ix = self.text.len();
-        self.text.push_str(text);
-        let end_ix = self.text.len();
-        if let Some(highlight) = highlight {
-            self.runs.push((start_ix..end_ix, highlight));
-        }
-    }
-
     pub fn text(&self) -> &str {
         self.text.as_str()
     }

crates/language_extension/src/extension_lsp_adapter.rs 🔗

@@ -463,11 +463,7 @@ fn build_code_label(
 
     let filter_range = label.filter_range.clone();
     text.get(filter_range.clone())?;
-    Some(CodeLabel {
-        text,
-        runs,
-        filter_range,
-    })
+    Some(CodeLabel::new(text, filter_range, runs))
 }
 
 fn lsp_completion_to_extension(value: lsp::CompletionItem) -> extension::Completion {
@@ -615,11 +611,7 @@ fn test_build_code_label() {
 
     assert_eq!(
         label,
-        CodeLabel {
-            text: label_text,
-            runs: label_runs,
-            filter_range: label.filter_range.clone()
-        }
+        CodeLabel::new(label_text, label.filter_range.clone(), label_runs)
     )
 }
 

crates/languages/src/c.rs 🔗

@@ -188,11 +188,7 @@ impl super::LspAdapter for CLspAdapter {
                             .map(|start| start..start + filter_text.len())
                     })
                     .unwrap_or(detail.len() + 1..text.len());
-                return Some(CodeLabel {
-                    filter_range,
-                    text,
-                    runs,
-                });
+                return Some(CodeLabel::new(text, filter_range, runs));
             }
             Some(lsp::CompletionItemKind::CONSTANT | lsp::CompletionItemKind::VARIABLE)
                 if completion.detail.is_some() =>
@@ -208,11 +204,7 @@ impl super::LspAdapter for CLspAdapter {
                             .map(|start| start..start + filter_text.len())
                     })
                     .unwrap_or(detail.len() + 1..text.len());
-                return Some(CodeLabel {
-                    filter_range,
-                    text,
-                    runs,
-                });
+                return Some(CodeLabel::new(text, filter_range, runs));
             }
             Some(lsp::CompletionItemKind::FUNCTION | lsp::CompletionItemKind::METHOD)
                 if completion.detail.is_some() =>
@@ -236,11 +228,7 @@ impl super::LspAdapter for CLspAdapter {
                         filter_start..filter_end
                     });
 
-                return Some(CodeLabel {
-                    filter_range,
-                    text,
-                    runs,
-                });
+                return Some(CodeLabel::new(text, filter_range, runs));
             }
             Some(kind) => {
                 let highlight_name = match kind {
@@ -324,11 +312,11 @@ impl super::LspAdapter for CLspAdapter {
             _ => return None,
         };
 
-        Some(CodeLabel {
-            runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
-            text: text[display_range].to_string(),
+        Some(CodeLabel::new(
+            text[display_range.clone()].to_string(),
             filter_range,
-        })
+            language.highlight_text(&text.as_str().into(), display_range),
+        ))
     }
 
     fn prepare_initialize_params(

crates/languages/src/go.rs 🔗

@@ -231,11 +231,7 @@ impl LspAdapter for GoLspAdapter {
                             .map(|start| start..start + filter_text.len())
                     })
                     .unwrap_or(0..label.len());
-                return Some(CodeLabel {
-                    text,
-                    runs,
-                    filter_range,
-                });
+                return Some(CodeLabel::new(text, filter_range, runs));
             }
             Some((
                 lsp::CompletionItemKind::CONSTANT | lsp::CompletionItemKind::VARIABLE,
@@ -256,11 +252,7 @@ impl LspAdapter for GoLspAdapter {
                             .map(|start| start..start + filter_text.len())
                     })
                     .unwrap_or(0..label.len());
-                return Some(CodeLabel {
-                    text,
-                    runs,
-                    filter_range,
-                });
+                return Some(CodeLabel::new(text, filter_range, runs));
             }
             Some((lsp::CompletionItemKind::STRUCT, _)) => {
                 let text = format!("{label} struct {{}}");
@@ -277,11 +269,7 @@ impl LspAdapter for GoLspAdapter {
                             .map(|start| start..start + filter_text.len())
                     })
                     .unwrap_or(0..label.len());
-                return Some(CodeLabel {
-                    text,
-                    runs,
-                    filter_range,
-                });
+                return Some(CodeLabel::new(text, filter_range, runs));
             }
             Some((lsp::CompletionItemKind::INTERFACE, _)) => {
                 let text = format!("{label} interface {{}}");
@@ -298,11 +286,7 @@ impl LspAdapter for GoLspAdapter {
                             .map(|start| start..start + filter_text.len())
                     })
                     .unwrap_or(0..label.len());
-                return Some(CodeLabel {
-                    text,
-                    runs,
-                    filter_range,
-                });
+                return Some(CodeLabel::new(text, filter_range, runs));
             }
             Some((lsp::CompletionItemKind::FIELD, detail)) => {
                 let text = format!("{label} {detail}");
@@ -320,11 +304,7 @@ impl LspAdapter for GoLspAdapter {
                             .map(|start| start..start + filter_text.len())
                     })
                     .unwrap_or(0..label.len());
-                return Some(CodeLabel {
-                    text,
-                    runs,
-                    filter_range,
-                });
+                return Some(CodeLabel::new(text, filter_range, runs));
             }
             Some((lsp::CompletionItemKind::FUNCTION | lsp::CompletionItemKind::METHOD, detail)) => {
                 if let Some(signature) = detail.strip_prefix("func") {
@@ -342,11 +322,7 @@ impl LspAdapter for GoLspAdapter {
                                 .map(|start| start..start + filter_text.len())
                         })
                         .unwrap_or(0..label.len());
-                    return Some(CodeLabel {
-                        filter_range,
-                        text,
-                        runs,
-                    });
+                    return Some(CodeLabel::new(text, filter_range, runs));
                 }
             }
             _ => {}
@@ -406,11 +382,11 @@ impl LspAdapter for GoLspAdapter {
             _ => return None,
         };
 
-        Some(CodeLabel {
-            runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
-            text: text[display_range].to_string(),
+        Some(CodeLabel::new(
+            text[display_range.clone()].to_string(),
             filter_range,
-        })
+            language.highlight_text(&text.as_str().into(), display_range),
+        ))
     }
 
     fn diagnostic_message_to_markdown(&self, message: &str) -> Option<String> {
@@ -810,15 +786,15 @@ mod tests {
                     &language
                 )
                 .await,
-            Some(CodeLabel {
-                text: "Hello(a B) c.D".to_string(),
-                filter_range: 0..5,
-                runs: vec![
+            Some(CodeLabel::new(
+                "Hello(a B) c.D".to_string(),
+                0..5,
+                vec![
                     (0..5, highlight_function),
                     (8..9, highlight_type),
                     (13..14, highlight_type),
-                ],
-            })
+                ]
+            ))
         );
 
         // Nested methods
@@ -834,15 +810,15 @@ mod tests {
                     &language
                 )
                 .await,
-            Some(CodeLabel {
-                text: "one.two.Three() [3]interface{}".to_string(),
-                filter_range: 0..13,
-                runs: vec![
+            Some(CodeLabel::new(
+                "one.two.Three() [3]interface{}".to_string(),
+                0..13,
+                vec![
                     (8..13, highlight_function),
                     (17..18, highlight_number),
                     (19..28, highlight_keyword),
                 ],
-            })
+            ))
         );
 
         // Nested fields
@@ -858,11 +834,11 @@ mod tests {
                     &language
                 )
                 .await,
-            Some(CodeLabel {
-                text: "two.Three a.Bcd".to_string(),
-                filter_range: 0..9,
-                runs: vec![(4..9, highlight_field), (12..15, highlight_type)],
-            })
+            Some(CodeLabel::new(
+                "two.Three a.Bcd".to_string(),
+                0..9,
+                vec![(4..9, highlight_field), (12..15, highlight_type)],
+            ))
         );
     }
 

crates/languages/src/python.rs 🔗

@@ -407,11 +407,6 @@ impl LspAdapter for PyrightLspAdapter {
                 return None;
             }
         };
-        let filter_range = item
-            .filter_text
-            .as_deref()
-            .and_then(|filter| label.find(filter).map(|ix| ix..ix + filter.len()))
-            .unwrap_or(0..label.len());
         let mut text = label.clone();
         if let Some(completion_details) = item
             .label_details
@@ -420,14 +415,14 @@ impl LspAdapter for PyrightLspAdapter {
         {
             write!(&mut text, " {}", completion_details).ok();
         }
-        Some(language::CodeLabel {
-            runs: highlight_id
+        Some(language::CodeLabel::filtered(
+            text,
+            item.filter_text.as_deref(),
+            highlight_id
                 .map(|id| (0..label.len(), id))
                 .into_iter()
                 .collect(),
-            text,
-            filter_range,
-        })
+        ))
     }
 
     async fn label_for_symbol(
@@ -458,11 +453,11 @@ impl LspAdapter for PyrightLspAdapter {
             _ => return None,
         };
 
-        Some(language::CodeLabel {
-            runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
-            text: text[display_range].to_string(),
+        Some(language::CodeLabel::new(
+            text[display_range.clone()].to_string(),
             filter_range,
-        })
+            language.highlight_text(&text.as_str().into(), display_range),
+        ))
     }
 
     async fn workspace_configuration(
@@ -1424,16 +1419,11 @@ impl LspAdapter for PyLspAdapter {
             lsp::CompletionItemKind::CONSTANT => grammar.highlight_id_for_name("constant")?,
             _ => return None,
         };
-        let filter_range = item
-            .filter_text
-            .as_deref()
-            .and_then(|filter| label.find(filter).map(|ix| ix..ix + filter.len()))
-            .unwrap_or(0..label.len());
-        Some(language::CodeLabel {
-            text: label.clone(),
-            runs: vec![(0..label.len(), highlight_id)],
-            filter_range,
-        })
+        Some(language::CodeLabel::filtered(
+            label.clone(),
+            item.filter_text.as_deref(),
+            vec![(0..label.len(), highlight_id)],
+        ))
     }
 
     async fn label_for_symbol(
@@ -1463,12 +1453,11 @@ impl LspAdapter for PyLspAdapter {
             }
             _ => return None,
         };
-
-        Some(language::CodeLabel {
-            runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
-            text: text[display_range].to_string(),
+        Some(language::CodeLabel::new(
+            text[display_range.clone()].to_string(),
             filter_range,
-        })
+            language.highlight_text(&text.as_str().into(), display_range),
+        ))
     }
 
     async fn workspace_configuration(
@@ -1708,11 +1697,6 @@ impl LspAdapter for BasedPyrightLspAdapter {
                 return None;
             }
         };
-        let filter_range = item
-            .filter_text
-            .as_deref()
-            .and_then(|filter| label.find(filter).map(|ix| ix..ix + filter.len()))
-            .unwrap_or(0..label.len());
         let mut text = label.clone();
         if let Some(completion_details) = item
             .label_details
@@ -1721,14 +1705,14 @@ impl LspAdapter for BasedPyrightLspAdapter {
         {
             write!(&mut text, " {}", completion_details).ok();
         }
-        Some(language::CodeLabel {
-            runs: highlight_id
+        Some(language::CodeLabel::filtered(
+            text,
+            item.filter_text.as_deref(),
+            highlight_id
                 .map(|id| (0..label.len(), id))
                 .into_iter()
                 .collect(),
-            text,
-            filter_range,
-        })
+        ))
     }
 
     async fn label_for_symbol(
@@ -1758,12 +1742,11 @@ impl LspAdapter for BasedPyrightLspAdapter {
             }
             _ => return None,
         };
-
-        Some(language::CodeLabel {
-            runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
-            text: text[display_range].to_string(),
+        Some(language::CodeLabel::new(
+            text[display_range.clone()].to_string(),
             filter_range,
-        })
+            language.highlight_text(&text.as_str().into(), display_range),
+        ))
     }
 
     async fn workspace_configuration(

crates/languages/src/rust.rs 🔗

@@ -209,11 +209,7 @@ impl LspAdapter for RustLspAdapter {
                 })
                 .unwrap_or_else(filter_range);
 
-            CodeLabel {
-                text,
-                runs,
-                filter_range,
-            }
+            CodeLabel::new(text, filter_range, runs)
         };
         let mut label = match (detail_right, completion.kind) {
             (Some(signature), Some(lsp::CompletionItemKind::FIELD)) => {
@@ -364,11 +360,11 @@ impl LspAdapter for RustLspAdapter {
 
         let filter_range = prefix.len()..prefix.len() + name.len();
         let display_range = 0..filter_range.end;
-        Some(CodeLabel {
-            runs: language.highlight_text(&Rope::from_iter([prefix, name, suffix]), display_range),
-            text: format!("{prefix}{name}"),
+        Some(CodeLabel::new(
+            format!("{prefix}{name}"),
             filter_range,
-        })
+            language.highlight_text(&Rope::from_iter([prefix, name, suffix]), display_range),
+        ))
     }
 
     fn prepare_initialize_params(
@@ -1166,10 +1162,10 @@ mod tests {
                     &language
                 )
                 .await,
-            Some(CodeLabel {
-                text: "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
-                filter_range: 0..5,
-                runs: vec![
+            Some(CodeLabel::new(
+                "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
+                0..5,
+                vec![
                     (0..5, highlight_function),
                     (7..10, highlight_keyword),
                     (11..17, highlight_type),
@@ -1177,7 +1173,7 @@ mod tests {
                     (25..28, highlight_type),
                     (29..30, highlight_type),
                 ],
-            })
+            ))
         );
         assert_eq!(
             adapter
@@ -1194,10 +1190,10 @@ mod tests {
                     &language
                 )
                 .await,
-            Some(CodeLabel {
-                text: "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
-                filter_range: 0..5,
-                runs: vec![
+            Some(CodeLabel::new(
+                "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
+                0..5,
+                vec![
                     (0..5, highlight_function),
                     (7..10, highlight_keyword),
                     (11..17, highlight_type),
@@ -1205,7 +1201,7 @@ mod tests {
                     (25..28, highlight_type),
                     (29..30, highlight_type),
                 ],
-            })
+            ))
         );
         assert_eq!(
             adapter
@@ -1219,11 +1215,11 @@ mod tests {
                     &language
                 )
                 .await,
-            Some(CodeLabel {
-                text: "len: usize".to_string(),
-                filter_range: 0..3,
-                runs: vec![(0..3, highlight_field), (5..10, highlight_type),],
-            })
+            Some(CodeLabel::new(
+                "len: usize".to_string(),
+                0..3,
+                vec![(0..3, highlight_field), (5..10, highlight_type),],
+            ))
         );
 
         assert_eq!(
@@ -1242,10 +1238,10 @@ mod tests {
                     &language
                 )
                 .await,
-            Some(CodeLabel {
-                text: "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
-                filter_range: 0..5,
-                runs: vec![
+            Some(CodeLabel::new(
+                "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
+                0..5,
+                vec![
                     (0..5, highlight_function),
                     (7..10, highlight_keyword),
                     (11..17, highlight_type),
@@ -1253,7 +1249,7 @@ mod tests {
                     (25..28, highlight_type),
                     (29..30, highlight_type),
                 ],
-            })
+            ))
         );
 
         assert_eq!(
@@ -1271,10 +1267,10 @@ mod tests {
                     &language
                 )
                 .await,
-            Some(CodeLabel {
-                text: "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
-                filter_range: 0..5,
-                runs: vec![
+            Some(CodeLabel::new(
+                "hello(&mut Option<T>) -> Vec<T> (use crate::foo)".to_string(),
+                0..5,
+                vec![
                     (0..5, highlight_function),
                     (7..10, highlight_keyword),
                     (11..17, highlight_type),
@@ -1282,7 +1278,7 @@ mod tests {
                     (25..28, highlight_type),
                     (29..30, highlight_type),
                 ],
-            })
+            ))
         );
 
         assert_eq!(
@@ -1301,16 +1297,16 @@ mod tests {
                     &language
                 )
                 .await,
-            Some(CodeLabel {
-                text: "await.as_deref_mut(&mut self) -> IterMut<'_, T>".to_string(),
-                filter_range: 6..18,
-                runs: vec![
+            Some(CodeLabel::new(
+                "await.as_deref_mut(&mut self) -> IterMut<'_, T>".to_string(),
+                6..18,
+                vec![
                     (6..18, HighlightId(2)),
                     (20..23, HighlightId(1)),
                     (33..40, HighlightId(0)),
                     (45..46, HighlightId(0))
                 ],
-            })
+            ))
         );
 
         assert_eq!(
@@ -1331,10 +1327,10 @@ mod tests {
                     &language
                 )
                 .await,
-            Some(CodeLabel {
-                text: "pub fn as_deref_mut(&mut self) -> IterMut<'_, T>".to_string(),
-                filter_range: 7..19,
-                runs: vec![
+            Some(CodeLabel::new(
+                "pub fn as_deref_mut(&mut self) -> IterMut<'_, T>".to_string(),
+                7..19,
+                vec![
                     (0..3, HighlightId(1)),
                     (4..6, HighlightId(1)),
                     (7..19, HighlightId(2)),
@@ -1342,7 +1338,7 @@ mod tests {
                     (34..41, HighlightId(0)),
                     (46..47, HighlightId(0))
                 ],
-            })
+            ))
         );
 
         assert_eq!(
@@ -1358,11 +1354,11 @@ mod tests {
                     &language,
                 )
                 .await,
-            Some(CodeLabel {
-                text: "inner_value: String".to_string(),
-                filter_range: 6..11,
-                runs: vec![(0..11, HighlightId(3)), (13..19, HighlightId(0))],
-            })
+            Some(CodeLabel::new(
+                "inner_value: String".to_string(),
+                6..11,
+                vec![(0..11, HighlightId(3)), (13..19, HighlightId(0))],
+            ))
         );
     }
 
@@ -1388,22 +1384,22 @@ mod tests {
             adapter
                 .label_for_symbol("hello", lsp::SymbolKind::FUNCTION, &language)
                 .await,
-            Some(CodeLabel {
-                text: "fn hello".to_string(),
-                filter_range: 3..8,
-                runs: vec![(0..2, highlight_keyword), (3..8, highlight_function)],
-            })
+            Some(CodeLabel::new(
+                "fn hello".to_string(),
+                3..8,
+                vec![(0..2, highlight_keyword), (3..8, highlight_function)],
+            ))
         );
 
         assert_eq!(
             adapter
                 .label_for_symbol("World", lsp::SymbolKind::TYPE_PARAMETER, &language)
                 .await,
-            Some(CodeLabel {
-                text: "type World".to_string(),
-                filter_range: 5..10,
-                runs: vec![(0..4, highlight_keyword), (5..10, highlight_type)],
-            })
+            Some(CodeLabel::new(
+                "type World".to_string(),
+                5..10,
+                vec![(0..4, highlight_keyword), (5..10, highlight_type)],
+            ))
         );
     }
 

crates/languages/src/typescript.rs 🔗

@@ -777,16 +777,11 @@ impl LspAdapter for TypeScriptLspAdapter {
         } else {
             item.label.clone()
         };
-        let filter_range = item
-            .filter_text
-            .as_deref()
-            .and_then(|filter| text.find(filter).map(|ix| ix..ix + filter.len()))
-            .unwrap_or(0..len);
-        Some(language::CodeLabel {
+        Some(language::CodeLabel::filtered(
             text,
-            runs: vec![(0..len, highlight_id)],
-            filter_range,
-        })
+            item.filter_text.as_deref(),
+            vec![(0..len, highlight_id)],
+        ))
     }
 
     async fn initialization_options(

crates/languages/src/vtsls.rs 🔗

@@ -201,16 +201,11 @@ impl LspAdapter for VtslsLspAdapter {
         } else {
             item.label.clone()
         };
-        let filter_range = item
-            .filter_text
-            .as_deref()
-            .and_then(|filter| text.find(filter).map(|ix| ix..ix + filter.len()))
-            .unwrap_or(0..len);
-        Some(language::CodeLabel {
+        Some(language::CodeLabel::filtered(
             text,
-            runs: vec![(0..len, highlight_id)],
-            filter_range,
-        })
+            item.filter_text.as_deref(),
+            vec![(0..len, highlight_id)],
+        ))
     }
 
     async fn workspace_configuration(

crates/project/src/lsp_store.rs 🔗

@@ -9365,11 +9365,7 @@ impl LspStore {
                         name: symbol.name,
                         kind: symbol.kind,
                         range: symbol.range,
-                        label: CodeLabel {
-                            text: Default::default(),
-                            runs: Default::default(),
-                            filter_range: Default::default(),
-                        },
+                        label: CodeLabel::default(),
                     },
                     cx,
                 )
@@ -9559,11 +9555,7 @@ impl LspStore {
                     new_text: completion.new_text,
                     source: completion.source,
                     documentation: None,
-                    label: CodeLabel {
-                        text: Default::default(),
-                        runs: Default::default(),
-                        filter_range: Default::default(),
-                    },
+                    label: CodeLabel::default(),
                     insert_text_mode: None,
                     icon_path: None,
                     confirm: None,
@@ -13060,19 +13052,19 @@ mod tests {
 
     #[test]
     fn test_multi_len_chars_normalization() {
-        let mut label = CodeLabel {
-            text: "myElˇ (parameter) myElˇ: {\n    foo: string;\n}".to_string(),
-            runs: vec![(0..6, HighlightId(1))],
-            filter_range: 0..6,
-        };
+        let mut label = CodeLabel::new(
+            "myElˇ (parameter) myElˇ: {\n    foo: string;\n}".to_string(),
+            0..6,
+            vec![(0..6, HighlightId(1))],
+        );
         ensure_uniform_list_compatible_label(&mut label);
         assert_eq!(
             label,
-            CodeLabel {
-                text: "myElˇ (parameter) myElˇ: { foo: string; }".to_string(),
-                runs: vec![(0..6, HighlightId(1))],
-                filter_range: 0..6,
-            }
+            CodeLabel::new(
+                "myElˇ (parameter) myElˇ: { foo: string; }".to_string(),
+                0..6,
+                vec![(0..6, HighlightId(1))],
+            )
         );
     }
 }