Fix crash when filtering items in Picker (#37929)

Smit Barmase created

Closes #37617

We're already using `get` in a bunch of places, this PR updates the
remaining spots to follow the same pattern. Note that the `ix` we read
in `render_match` can sometimes be stale.

The likely reason is that we run the match-update logic asynchronously
(see
[here](https://github.com/zed-industries/zed/blob/138117e0b15664079f5526cb56168750382b49b9/crates/picker/src/picker.rs#L643)).
That means it's possible to render items after the list's [data
update](https://github.com/zed-industries/zed/blob/138117e0b15664079f5526cb56168750382b49b9/crates/picker/src/picker.rs#L652)
but before the [list
reset](https://github.com/zed-industries/zed/blob/138117e0b15664079f5526cb56168750382b49b9/crates/picker/src/picker.rs#L662),
in which case the `ix` can be greater than that of our updated data.

Release Notes:

- Fixed crash when filtering MCP tools.

Change summary

crates/agent_ui/src/agent_configuration/tool_picker.rs            |  2 
crates/agent_ui/src/context_picker/file_context_picker.rs         |  2 
crates/agent_ui/src/context_picker/rules_context_picker.rs        |  2 
crates/agent_ui/src/context_picker/symbol_context_picker.rs       |  2 
crates/agent_ui/src/context_picker/thread_context_picker.rs       |  2 
crates/collab_ui/src/collab_panel/contact_finder.rs               |  2 
crates/debugger_ui/src/attach_modal.rs                            |  2 
crates/debugger_ui/src/new_process_modal.rs                       |  2 
crates/extensions_ui/src/extension_version_selector.rs            |  4 
crates/file_finder/src/file_finder.rs                             |  5 
crates/git_ui/src/branch_picker.rs                                |  2 
crates/git_ui/src/picker_prompt.rs                                |  2 
crates/jj_ui/src/bookmark_picker.rs                               |  2 
crates/language_selector/src/language_selector.rs                 |  2 
crates/line_ending_selector/src/line_ending_selector.rs           |  4 
crates/onboarding/src/base_keymap_picker.rs                       |  2 
crates/project_symbols/src/project_symbols.rs                     |  4 
crates/settings_profile_selector/src/settings_profile_selector.rs |  4 
crates/snippets_ui/src/snippets_ui.rs                             |  2 
crates/tab_switcher/src/tab_switcher.rs                           |  5 
crates/tasks_ui/src/modal.rs                                      |  2 
crates/theme_selector/src/icon_theme_selector.rs                  |  2 
crates/theme_selector/src/theme_selector.rs                       |  2 
crates/toolchain_selector/src/toolchain_selector.rs               |  4 
crates/vim/src/state.rs                                           | 10 
25 files changed, 31 insertions(+), 43 deletions(-)

Detailed changes

crates/agent_ui/src/agent_configuration/tool_picker.rs 🔗

@@ -318,7 +318,7 @@ impl PickerDelegate for ToolPickerDelegate {
         _window: &mut Window,
         cx: &mut Context<Picker<Self>>,
     ) -> Option<Self::ListItem> {
-        let item = &self.filtered_items[ix];
+        let item = &self.filtered_items.get(ix)?;
         match item {
             PickerItem::ContextServer { server_id, .. } => Some(
                 div()

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

@@ -160,7 +160,7 @@ impl PickerDelegate for FileContextPickerDelegate {
         _window: &mut Window,
         cx: &mut Context<Picker<Self>>,
     ) -> Option<Self::ListItem> {
-        let FileMatch { mat, .. } = &self.matches[ix];
+        let FileMatch { mat, .. } = &self.matches.get(ix)?;
 
         Some(
             ListItem::new(ix)

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

@@ -146,7 +146,7 @@ impl PickerDelegate for RulesContextPickerDelegate {
         _window: &mut Window,
         cx: &mut Context<Picker<Self>>,
     ) -> Option<Self::ListItem> {
-        let thread = &self.matches[ix];
+        let thread = &self.matches.get(ix)?;
 
         Some(ListItem::new(ix).inset(true).toggle_state(selected).child(
             render_thread_context_entry(thread, self.context_store.clone(), cx),

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

@@ -169,7 +169,7 @@ impl PickerDelegate for SymbolContextPickerDelegate {
         _window: &mut Window,
         _: &mut Context<Picker<Self>>,
     ) -> Option<Self::ListItem> {
-        let mat = &self.matches[ix];
+        let mat = &self.matches.get(ix)?;
 
         Some(ListItem::new(ix).inset(true).toggle_state(selected).child(
             render_symbol_context_entry(ElementId::named_usize("symbol-ctx-picker", ix), mat),

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

@@ -220,7 +220,7 @@ impl PickerDelegate for ThreadContextPickerDelegate {
         _window: &mut Window,
         cx: &mut Context<Picker<Self>>,
     ) -> Option<Self::ListItem> {
-        let thread = &self.matches[ix];
+        let thread = &self.matches.get(ix)?;
 
         Some(ListItem::new(ix).inset(true).toggle_state(selected).child(
             render_thread_context_entry(thread, self.context_store.clone(), cx),

crates/collab_ui/src/collab_panel/contact_finder.rs 🔗

@@ -148,7 +148,7 @@ impl PickerDelegate for ContactFinderDelegate {
         _: &mut Window,
         cx: &mut Context<Picker<Self>>,
     ) -> Option<Self::ListItem> {
-        let user = &self.potential_contacts[ix];
+        let user = &self.potential_contacts.get(ix)?;
         let request_status = self.user_store.read(cx).contact_request_status(user);
 
         let icon_path = match request_status {

crates/debugger_ui/src/attach_modal.rs 🔗

@@ -289,7 +289,7 @@ impl PickerDelegate for AttachModalDelegate {
         _window: &mut Window,
         _: &mut Context<Picker<Self>>,
     ) -> Option<Self::ListItem> {
-        let hit = &self.matches[ix];
+        let hit = &self.matches.get(ix)?;
         let candidate = self.candidates.get(hit.candidate_id)?;
 
         Some(

crates/debugger_ui/src/new_process_modal.rs 🔗

@@ -1446,7 +1446,7 @@ impl PickerDelegate for DebugDelegate {
         window: &mut Window,
         cx: &mut Context<picker::Picker<Self>>,
     ) -> Option<Self::ListItem> {
-        let hit = &self.matches[ix];
+        let hit = &self.matches.get(ix)?;
 
         let highlighted_location = HighlightedMatch {
             text: hit.string.clone(),

crates/extensions_ui/src/extension_version_selector.rs 🔗

@@ -207,8 +207,8 @@ impl PickerDelegate for ExtensionVersionSelectorDelegate {
         _: &mut Window,
         cx: &mut Context<Picker<Self>>,
     ) -> Option<Self::ListItem> {
-        let version_match = &self.matches[ix];
-        let extension_version = &self.extension_versions[version_match.candidate_id];
+        let version_match = &self.matches.get(ix)?;
+        let extension_version = &self.extension_versions.get(version_match.candidate_id)?;
 
         let is_version_compatible =
             extension_host::is_version_compatible(ReleaseChannel::global(cx), extension_version);

crates/file_finder/src/file_finder.rs 🔗

@@ -1599,10 +1599,7 @@ impl PickerDelegate for FileFinderDelegate {
     ) -> Option<Self::ListItem> {
         let settings = FileFinderSettings::get_global(cx);
 
-        let path_match = self
-            .matches
-            .get(ix)
-            .expect("Invalid matches state: no element for index {ix}");
+        let path_match = self.matches.get(ix)?;
 
         let history_icon = match &path_match {
             Match::History { .. } => Icon::new(IconName::HistoryRerun)

crates/git_ui/src/branch_picker.rs 🔗

@@ -454,7 +454,7 @@ impl PickerDelegate for BranchListDelegate {
         _window: &mut Window,
         cx: &mut Context<Picker<Self>>,
     ) -> Option<Self::ListItem> {
-        let entry = &self.matches[ix];
+        let entry = &self.matches.get(ix)?;
 
         let (commit_time, author_name, subject) = entry
             .branch

crates/git_ui/src/picker_prompt.rs 🔗

@@ -216,7 +216,7 @@ impl PickerDelegate for PickerPromptDelegate {
         _window: &mut Window,
         _cx: &mut Context<Picker<Self>>,
     ) -> Option<Self::ListItem> {
-        let hit = &self.matches[ix];
+        let hit = &self.matches.get(ix)?;
         let shortened_option = util::truncate_and_trailoff(&hit.string, self.max_match_length);
 
         Some(

crates/jj_ui/src/bookmark_picker.rs 🔗

@@ -182,7 +182,7 @@ impl PickerDelegate for BookmarkPickerDelegate {
         _window: &mut Window,
         _cx: &mut Context<Picker<Self>>,
     ) -> Option<Self::ListItem> {
-        let entry = &self.matches[ix];
+        let entry = &self.matches.get(ix)?;
 
         Some(
             ListItem::new(ix)

crates/language_selector/src/language_selector.rs 🔗

@@ -283,7 +283,7 @@ impl PickerDelegate for LanguageSelectorDelegate {
         _: &mut Window,
         cx: &mut Context<Picker<Self>>,
     ) -> Option<Self::ListItem> {
-        let mat = &self.matches[ix];
+        let mat = &self.matches.get(ix)?;
         let (label, language_icon) = self.language_data_for_match(mat, cx);
         Some(
             ListItem::new(ix)

crates/line_ending_selector/src/line_ending_selector.rs 🔗

@@ -171,7 +171,7 @@ impl PickerDelegate for LineEndingSelectorDelegate {
         _: &mut Window,
         _: &mut Context<Picker<Self>>,
     ) -> Option<Self::ListItem> {
-        let line_ending = self.matches[ix];
+        let line_ending = self.matches.get(ix)?;
         let label = match line_ending {
             LineEnding::Unix => "LF",
             LineEnding::Windows => "CRLF",
@@ -183,7 +183,7 @@ impl PickerDelegate for LineEndingSelectorDelegate {
             .toggle_state(selected)
             .child(Label::new(label));
 
-        if self.line_ending == line_ending {
+        if &self.line_ending == line_ending {
             list_item = list_item.end_slot(Icon::new(IconName::Check).color(Color::Muted));
         }
 

crates/onboarding/src/base_keymap_picker.rs 🔗

@@ -213,7 +213,7 @@ impl PickerDelegate for BaseKeymapSelectorDelegate {
         _window: &mut Window,
         _cx: &mut Context<Picker<Self>>,
     ) -> Option<Self::ListItem> {
-        let keymap_match = &self.matches[ix];
+        let keymap_match = &self.matches.get(ix)?;
 
         Some(
             ListItem::new(ix)

crates/project_symbols/src/project_symbols.rs 🔗

@@ -217,8 +217,8 @@ impl PickerDelegate for ProjectSymbolsDelegate {
         _window: &mut Window,
         cx: &mut Context<Picker<Self>>,
     ) -> Option<Self::ListItem> {
-        let string_match = &self.matches[ix];
-        let symbol = &self.symbols[string_match.candidate_id];
+        let string_match = &self.matches.get(ix)?;
+        let symbol = &self.symbols.get(string_match.candidate_id)?;
         let syntax_runs = styled_runs_for_code_label(&symbol.label, cx.theme().syntax());
 
         let mut path = symbol.path.path.to_string_lossy();

crates/settings_profile_selector/src/settings_profile_selector.rs 🔗

@@ -257,8 +257,8 @@ impl PickerDelegate for SettingsProfileSelectorDelegate {
         _: &mut Window,
         _: &mut Context<Picker<Self>>,
     ) -> Option<Self::ListItem> {
-        let mat = &self.matches[ix];
-        let profile_name = &self.profile_names[mat.candidate_id];
+        let mat = &self.matches.get(ix)?;
+        let profile_name = &self.profile_names.get(mat.candidate_id)?;
 
         Some(
             ListItem::new(ix)

crates/snippets_ui/src/snippets_ui.rs 🔗

@@ -310,7 +310,7 @@ impl PickerDelegate for ScopeSelectorDelegate {
         _window: &mut Window,
         cx: &mut Context<Picker<Self>>,
     ) -> Option<Self::ListItem> {
-        let mat = &self.matches[ix];
+        let mat = &self.matches.get(ix)?;
         let name_label = mat.string.clone();
 
         let scope_name = ScopeName(Cow::Owned(

crates/tab_switcher/src/tab_switcher.rs 🔗

@@ -649,10 +649,7 @@ impl PickerDelegate for TabSwitcherDelegate {
         window: &mut Window,
         cx: &mut Context<Picker<Self>>,
     ) -> Option<Self::ListItem> {
-        let tab_match = self
-            .matches
-            .get(ix)
-            .expect("Invalid matches state: no element for index {ix}");
+        let tab_match = self.matches.get(ix)?;
 
         let params = TabContentParams {
             detail: Some(tab_match.detail),

crates/tasks_ui/src/modal.rs 🔗

@@ -443,7 +443,7 @@ impl PickerDelegate for TasksModalDelegate {
         cx: &mut Context<picker::Picker<Self>>,
     ) -> Option<Self::ListItem> {
         let candidates = self.candidates.as_ref()?;
-        let hit = &self.matches[ix];
+        let hit = &self.matches.get(ix)?;
         let (source_kind, resolved_task) = &candidates.get(hit.candidate_id)?;
         let template = resolved_task.original_task();
         let display_label = resolved_task.display_label();

crates/theme_selector/src/icon_theme_selector.rs 🔗

@@ -287,7 +287,7 @@ impl PickerDelegate for IconThemeSelectorDelegate {
         _window: &mut Window,
         _cx: &mut Context<Picker<Self>>,
     ) -> Option<Self::ListItem> {
-        let theme_match = &self.matches[ix];
+        let theme_match = &self.matches.get(ix)?;
 
         Some(
             ListItem::new(ix)

crates/theme_selector/src/theme_selector.rs 🔗

@@ -345,7 +345,7 @@ impl PickerDelegate for ThemeSelectorDelegate {
         _window: &mut Window,
         _cx: &mut Context<Picker<Self>>,
     ) -> Option<Self::ListItem> {
-        let theme_match = &self.matches[ix];
+        let theme_match = &self.matches.get(ix)?;
 
         Some(
             ListItem::new(ix)

crates/toolchain_selector/src/toolchain_selector.rs 🔗

@@ -1011,8 +1011,8 @@ impl PickerDelegate for ToolchainSelectorDelegate {
         _: &mut Window,
         cx: &mut Context<Picker<Self>>,
     ) -> Option<Self::ListItem> {
-        let mat = &self.matches[ix];
-        let (toolchain, scope) = &self.candidates[mat.candidate_id];
+        let mat = &self.matches.get(ix)?;
+        let (toolchain, scope) = &self.candidates.get(mat.candidate_id)?;
 
         let label = toolchain.name.clone();
         let path = Self::relativize_path(toolchain.path.clone(), &self.worktree_abs_path_root);

crates/vim/src/state.rs 🔗

@@ -1192,10 +1192,7 @@ impl PickerDelegate for RegistersViewDelegate {
         _: &mut Window,
         cx: &mut Context<Picker<Self>>,
     ) -> Option<Self::ListItem> {
-        let register_match = self
-            .matches
-            .get(ix)
-            .expect("Invalid matches state: no element for index {ix}");
+        let register_match = self.matches.get(ix)?;
 
         let mut output = String::new();
         let mut runs = Vec::new();
@@ -1584,10 +1581,7 @@ impl PickerDelegate for MarksViewDelegate {
         _: &mut Window,
         cx: &mut Context<Picker<Self>>,
     ) -> Option<Self::ListItem> {
-        let mark_match = self
-            .matches
-            .get(ix)
-            .expect("Invalid matches state: no element for index {ix}");
+        let mark_match = self.matches.get(ix)?;
 
         let mut left_output = String::new();
         let mut left_runs = Vec::new();