Terminal Markdown previewer — GUI-like experience.
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

chore: extract fuzzy matching

RivoLink 9f7b025b 004a600f

+138 -131
+11 -131
src/app/file_picker.rs
··· 11 11 const MAX_FUZZY_PICKER_DIRS_VISITED: usize = 5_000; 12 12 const MAX_FUZZY_PICKER_FILES_INDEXED: usize = 10_000; 13 13 const MAX_FUZZY_PICKER_INDEX_DURATION: Duration = Duration::from_secs(5); 14 - const IGNORED_FUZZY_PICKER_DIRS: &[&str] = &[ 15 - ".git", 16 - "node_modules", 17 - "target", 18 - ".venv", 19 - "venv", 20 - "vendor", 21 - "var", 22 - "dist", 23 - "build", 24 - ".next", 25 - ".cache", 26 - ]; 27 - 28 14 #[derive(Clone, Debug, PartialEq, Eq)] 29 15 pub(crate) struct FilePickerEntry { 30 16 label: String, ··· 60 46 &self.label 61 47 } 62 48 63 - fn label_lower(&self) -> &str { 49 + pub(super) fn label_lower(&self) -> &str { 64 50 &self.label_lower 65 51 } 66 52 67 - fn file_name_lower(&self) -> &str { 53 + pub(super) fn file_name_lower(&self) -> &str { 68 54 &self.file_name_lower 69 55 } 70 56 71 - fn file_name_offset(&self) -> usize { 57 + pub(super) fn file_name_offset(&self) -> usize { 72 58 self.file_name_offset 73 59 } 74 60 75 - fn path_depth(&self) -> usize { 61 + pub(super) fn path_depth(&self) -> usize { 76 62 self.path_depth 77 63 } 78 64 ··· 184 170 Ok(entries) 185 171 } 186 172 187 - fn is_ignored_fuzzy_picker_dir_name(name: &str) -> bool { 188 - IGNORED_FUZZY_PICKER_DIRS.contains(&name) 189 - } 190 - 191 - fn fuzzy_directory_sort_key(root: &std::path::Path, path: &std::path::Path) -> (bool, String) { 192 - let label = path 193 - .strip_prefix(root) 194 - .unwrap_or(path) 195 - .display() 196 - .to_string(); 197 - ( 198 - !label 199 - .split(std::path::MAIN_SEPARATOR) 200 - .next() 201 - .unwrap_or(&label) 202 - .starts_with('.'), 203 - label.to_lowercase(), 204 - ) 205 - } 206 - 207 173 fn build_fuzzy_file_picker_entries( 208 174 dir: &std::path::Path, 209 175 ) -> std::io::Result<PickerIndexResult> { ··· 255 221 256 222 if file_type.is_dir() { 257 223 let name = entry.file_name(); 258 - if Self::is_ignored_fuzzy_picker_dir_name(name.to_string_lossy().as_ref()) { 224 + if super::fuzzy::is_ignored_fuzzy_picker_dir_name( 225 + name.to_string_lossy().as_ref(), 226 + ) { 259 227 continue; 260 228 } 261 229 dirs.push(path); ··· 278 246 } 279 247 280 248 files.sort_by(|left, right| { 281 - Self::fuzzy_entry_sort_key(left).cmp(&Self::fuzzy_entry_sort_key(right)) 249 + super::fuzzy::fuzzy_entry_sort_key(left) 250 + .cmp(&super::fuzzy::fuzzy_entry_sort_key(right)) 282 251 }); 283 - dirs.sort_by_key(|path| Self::fuzzy_directory_sort_key(dir, path)); 252 + dirs.sort_by_key(|path| super::fuzzy::fuzzy_directory_sort_key(dir, path)); 284 253 285 254 entries.extend(files); 286 255 if truncated.is_some() { ··· 293 262 Ok(PickerIndexResult { entries, truncated }) 294 263 } 295 264 296 - fn fuzzy_entry_sort_key(entry: &FilePickerEntry) -> (bool, &str) { 297 - let first_component = entry 298 - .label 299 - .split(std::path::MAIN_SEPARATOR) 300 - .next() 301 - .unwrap_or(entry.label()); 302 - (!first_component.starts_with('.'), entry.label_lower()) 303 - } 304 - 305 - fn fuzzy_component_match(candidate: &str, query: &str) -> Option<(usize, Vec<usize>)> { 306 - if let Some(start) = candidate.find(query) { 307 - let start_chars = candidate[..start].chars().count(); 308 - let query_len = query.chars().count(); 309 - let len_diff = candidate.chars().count().saturating_sub(query_len); 310 - let prefix_bonus = usize::from(start_chars == 0).saturating_mul(80); 311 - let boundary_bonus = 312 - usize::from(Self::is_match_boundary(candidate, start_chars)).saturating_mul(40); 313 - let score = start_chars 314 - .saturating_mul(10) 315 - .saturating_add(len_diff) 316 - .saturating_sub(prefix_bonus) 317 - .saturating_sub(boundary_bonus); 318 - let positions = (start_chars..start_chars + query_len).collect::<Vec<_>>(); 319 - return Some((score, positions)); 320 - } 321 - 322 - let mut search_from = 0usize; 323 - let mut positions = Vec::with_capacity(query.len()); 324 - 325 - for needle in query.chars() { 326 - let found = candidate[search_from..] 327 - .char_indices() 328 - .find(|(_, ch)| *ch == needle) 329 - .map(|(idx, _)| search_from + idx)?; 330 - let char_pos = candidate[..found].chars().count(); 331 - positions.push(char_pos); 332 - search_from = found + needle.len_utf8(); 333 - } 334 - 335 - let first = *positions.first()?; 336 - let last = *positions.last()?; 337 - let span = last.saturating_sub(first); 338 - let gaps = positions 339 - .windows(2) 340 - .map(|window| window[1].saturating_sub(window[0]).saturating_sub(1)) 341 - .sum::<usize>(); 342 - let len_diff = candidate 343 - .chars() 344 - .count() 345 - .saturating_sub(query.chars().count()); 346 - let prefix_bonus = usize::from(first == 0).saturating_mul(80); 347 - let boundary_bonus = 348 - usize::from(Self::is_match_boundary(candidate, first)).saturating_mul(40); 349 - let score = 1_000usize 350 - .saturating_add(gaps.saturating_mul(120)) 351 - .saturating_add(first.saturating_mul(10)) 352 - .saturating_add(span) 353 - .saturating_add(len_diff) 354 - .saturating_sub(prefix_bonus) 355 - .saturating_sub(boundary_bonus); 356 - Some((score, positions)) 357 - } 358 - 359 - fn is_match_boundary(candidate: &str, char_pos: usize) -> bool { 360 - if char_pos == 0 { 361 - return true; 362 - } 363 - 364 - candidate 365 - .chars() 366 - .nth(char_pos.saturating_sub(1)) 367 - .is_some_and(|ch| matches!(ch, '-' | '_' | '.' | ' ')) 368 - } 369 - 370 - fn fuzzy_match(entry: &FilePickerEntry, query: &str) -> Option<(usize, Vec<usize>)> { 371 - if query.is_empty() { 372 - return Some((0, Vec::new())); 373 - } 374 - 375 - let (score, positions) = Self::fuzzy_component_match(entry.file_name_lower(), query)?; 376 - Some(( 377 - score, 378 - positions 379 - .into_iter() 380 - .map(|position| entry.file_name_offset() + position) 381 - .collect(), 382 - )) 383 - } 384 - 385 265 pub(super) fn refresh_file_picker_matches(&mut self) { 386 266 if self.is_browser_file_picker() { 387 267 self.file_picker.filtered = (0..self.file_picker.entries.len()).collect(); ··· 410 290 .iter() 411 291 .enumerate() 412 292 .filter_map(|(idx, entry)| { 413 - Self::fuzzy_match(entry, &query).map(|(score, positions)| { 293 + super::fuzzy::fuzzy_match(entry, &query).map(|(score, positions)| { 414 294 ( 415 295 idx, 416 296 score,
+126
src/app/fuzzy.rs
··· 1 + use super::file_picker::FilePickerEntry; 2 + 3 + const IGNORED_FUZZY_PICKER_DIRS: &[&str] = &[ 4 + ".git", 5 + "node_modules", 6 + "target", 7 + ".venv", 8 + "venv", 9 + "vendor", 10 + "var", 11 + "dist", 12 + "build", 13 + ".next", 14 + ".cache", 15 + ]; 16 + 17 + pub(super) fn is_ignored_fuzzy_picker_dir_name(name: &str) -> bool { 18 + IGNORED_FUZZY_PICKER_DIRS.contains(&name) 19 + } 20 + 21 + pub(super) fn fuzzy_directory_sort_key( 22 + root: &std::path::Path, 23 + path: &std::path::Path, 24 + ) -> (bool, String) { 25 + let label = path 26 + .strip_prefix(root) 27 + .unwrap_or(path) 28 + .display() 29 + .to_string(); 30 + ( 31 + !label 32 + .split(std::path::MAIN_SEPARATOR) 33 + .next() 34 + .unwrap_or(&label) 35 + .starts_with('.'), 36 + label.to_lowercase(), 37 + ) 38 + } 39 + 40 + pub(super) fn fuzzy_entry_sort_key(entry: &FilePickerEntry) -> (bool, &str) { 41 + let first_component = entry 42 + .label() 43 + .split(std::path::MAIN_SEPARATOR) 44 + .next() 45 + .unwrap_or(entry.label()); 46 + (!first_component.starts_with('.'), entry.label_lower()) 47 + } 48 + 49 + pub(super) fn fuzzy_component_match(candidate: &str, query: &str) -> Option<(usize, Vec<usize>)> { 50 + if let Some(start) = candidate.find(query) { 51 + let start_chars = candidate[..start].chars().count(); 52 + let query_len = query.chars().count(); 53 + let len_diff = candidate.chars().count().saturating_sub(query_len); 54 + let prefix_bonus = usize::from(start_chars == 0).saturating_mul(80); 55 + let boundary_bonus = 56 + usize::from(is_match_boundary(candidate, start_chars)).saturating_mul(40); 57 + let score = start_chars 58 + .saturating_mul(10) 59 + .saturating_add(len_diff) 60 + .saturating_sub(prefix_bonus) 61 + .saturating_sub(boundary_bonus); 62 + let positions = (start_chars..start_chars + query_len).collect::<Vec<_>>(); 63 + return Some((score, positions)); 64 + } 65 + 66 + let mut search_from = 0usize; 67 + let mut positions = Vec::with_capacity(query.len()); 68 + 69 + for needle in query.chars() { 70 + let found = candidate[search_from..] 71 + .char_indices() 72 + .find(|(_, ch)| *ch == needle) 73 + .map(|(idx, _)| search_from + idx)?; 74 + let char_pos = candidate[..found].chars().count(); 75 + positions.push(char_pos); 76 + search_from = found + needle.len_utf8(); 77 + } 78 + 79 + let first = *positions.first()?; 80 + let last = *positions.last()?; 81 + let span = last.saturating_sub(first); 82 + let gaps = positions 83 + .windows(2) 84 + .map(|window| window[1].saturating_sub(window[0]).saturating_sub(1)) 85 + .sum::<usize>(); 86 + let len_diff = candidate 87 + .chars() 88 + .count() 89 + .saturating_sub(query.chars().count()); 90 + let prefix_bonus = usize::from(first == 0).saturating_mul(80); 91 + let boundary_bonus = usize::from(is_match_boundary(candidate, first)).saturating_mul(40); 92 + let score = 1_000usize 93 + .saturating_add(gaps.saturating_mul(120)) 94 + .saturating_add(first.saturating_mul(10)) 95 + .saturating_add(span) 96 + .saturating_add(len_diff) 97 + .saturating_sub(prefix_bonus) 98 + .saturating_sub(boundary_bonus); 99 + Some((score, positions)) 100 + } 101 + 102 + pub(super) fn is_match_boundary(candidate: &str, char_pos: usize) -> bool { 103 + if char_pos == 0 { 104 + return true; 105 + } 106 + 107 + candidate 108 + .chars() 109 + .nth(char_pos.saturating_sub(1)) 110 + .is_some_and(|ch| matches!(ch, '-' | '_' | '.' | ' ')) 111 + } 112 + 113 + pub(super) fn fuzzy_match(entry: &FilePickerEntry, query: &str) -> Option<(usize, Vec<usize>)> { 114 + if query.is_empty() { 115 + return Some((0, Vec::new())); 116 + } 117 + 118 + let (score, positions) = fuzzy_component_match(entry.file_name_lower(), query)?; 119 + Some(( 120 + score, 121 + positions 122 + .into_iter() 123 + .map(|position| entry.file_name_offset() + position) 124 + .collect(), 125 + )) 126 + }
+1
src/app/mod.rs
··· 18 18 pub(crate) use search::SearchState; 19 19 20 20 pub(crate) mod file_picker; 21 + mod fuzzy; 21 22 pub(crate) use file_picker::{FilePickerMode, FilePickerState, PickerIndexTruncation}; 22 23 use file_picker::{PendingPicker, PickerLoadState}; 23 24