{"record":{"id":"c935a3d2dec9ae48","repo":"gitbutlerapp/gitbutler","slug":"fuzzypickeritem-columns-must-return-a-searchable","errorCode":null,"errorMessage":"FuzzyPickerItem::columns must return a searchable column","messagePattern":"FuzzyPickerItem::columns must return a searchable column","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/but/src/command/legacy/status/tui/fuzzy_picker.rs","lineNumber":271,"sourceCode":"\n        if query.is_empty() {\n            self.items_to_show.extend(\n                self.items\n                    .iter()\n                    .enumerate()\n                    .map(|(item_idx, _)| ItemToShow::Plain { item_idx }),\n            );\n        } else {\n            let mut fuzzy_matches = self\n                .items\n                .iter()\n                .enumerate()\n                .filter_map(|(item_idx, item)| {\n                    let col = item\n                        .columns(SearchableToken(()))\n                        .into_iter()\n                        .find(|col| col.searchable.is_some())\n                        .expect(\"FuzzyPickerItem::columns must return a searchable column\");\n                    let (score, indices) = self.matcher.fuzzy_indices(&col.text, query)?;\n                    Some((item_idx, col, score, indices))\n                })\n                .collect::<Vec<_>>();\n            fuzzy_matches.sort_unstable_by(|(_, _, score_a, _), (_, _, score_b, _)| {\n                score_a.cmp(score_b).reverse()\n            });\n            self.items_to_show.extend(fuzzy_matches.into_iter().map(\n                |(item_idx, _, _, indices)| ItemToShow::FuzzyMatch {\n                    item_idx,\n                    char_indices: indices,\n                },\n            ));\n        }\n    }\n\n    pub fn handle_message(\n        mut self,","sourceCodeStart":253,"sourceCodeEnd":289,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/2497b8007aa4a1922dae9a805b32ffe5b5037785/crates/but/src/command/legacy/status/tui/fuzzy_picker.rs#L253-L289","documentation":"Same FuzzyPickerItem contract, enforced at query time: once the user types a query, the picker iterates every item and calls .find(|col| col.searchable.is_some()).expect(...). Unlike the layout-time check at fuzzy_picker.rs:114 (which inspects only items[0]), this one covers all items, so it fires on the first keystroke when any item other than the first lacks a searchable column.","triggerScenarios":"Typing into the fuzzy picker when the list is heterogeneous and some item/variant's columns impl yields no searchable column (header- or separator-like items mixed with real items), or an impl whose searchable column is conditional on item state.","commonSituations":"Mixed item lists passed to one picker; items in a state (unnamed branch/stack, empty field) where the impl returns all searchable: None; refactoring columns() and dropping the token; item added after the picker was laid out fine (first item still conforms).","solutions":["Make every columns impl unconditionally mark one column searchable; use empty text rather than None when there is nothing to search","Filter out non-searchable items before passing the list to the picker","Soften the query path: treat a missing searchable column as unmatchable (return None from the filter_map) instead of expecting","Add a test that types a query over every item kind used in pickers"],"exampleFix":"// before — first keystroke on a non-conforming item panics\nlet col = item.columns(SearchableToken(())).into_iter()\n    .find(|col| col.searchable.is_some())\n    .expect(\"FuzzyPickerItem::columns must return a searchable column\");\n\n// after — non-conforming items are simply unmatchable\nlet Some(col) = item.columns(SearchableToken(())).into_iter()\n    .find(|col| col.searchable.is_some()) else { return None };","handlingStrategy":"validation","validationCode":"// validate ALL items, not just the first, before the picker starts filtering\nlet all_searchable = items.iter().all(|i|\n    i.columns(SearchableToken(())).into_iter().any(|c| c.searchable.is_some())\n);\ndebug_assert!(all_searchable, \"item without searchable column in picker\");","typeGuard":"fn is_fuzzy_searchable(item: &impl FuzzyPickerItem) -> bool {\n    item.columns(SearchableToken(()))\n        .into_iter()\n        .any(|col| col.searchable.is_some())\n}","tryCatchPattern":null,"preventionTips":["The layout-time check only inspects items[0] — test with heterogeneous lists where later items differ from the first","Make columns() impls unconditional: return searchable: Some even for empty/placeholder text","Type a query in a test over every item kind; the panic fires on first keystroke, not on open","Consider skipping non-conforming items during filtering instead of expecting"],"tags":["rust","tui","fuzzy-picker","trait-contract","filtering"],"backgroundTag":"trait-contract-violation","analyzedSha":"2497b8007aa4a1922dae9a805b32ffe5b5037785","analyzedAt":"2026-08-17T00:30:25.648Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}