From 2f9aa725b4b43784bcb6ed65c17a99e57e385860 Mon Sep 17 00:00:00 2001 From: Axel Kappel <69117984+Kl4rry@users.noreply.github.com> Date: Thu, 30 May 2024 22:39:48 +0200 Subject: [PATCH] add proximity to current file in fuzzy finder scoring --- Cargo.lock | 28 ++++++++ Cargo.toml | 4 +- crates/ferrite-core/Cargo.toml | 1 + crates/ferrite-core/src/engine.rs | 13 ++++ crates/ferrite-core/src/palette/cmd.rs | 1 + crates/ferrite-core/src/palette/cmd_parser.rs | 2 + crates/ferrite-core/src/search_buffer.rs | 17 +++-- .../src/search_buffer/fuzzy_match.rs | 64 ++++++++++++++++--- 8 files changed, 110 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ce485d0..fd6be90 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -467,6 +467,7 @@ dependencies = [ "anstyle", "clap_lex", "strsim", + "terminal_size", ] [[package]] @@ -948,6 +949,7 @@ dependencies = [ "notify", "once_cell", "opener", + "proximity-sort", "rayon", "ropey", "serde", @@ -2006,6 +2008,12 @@ dependencies = [ "libredox 0.0.2", ] +[[package]] +name = "os_str_bytes" +version = "6.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" + [[package]] name = "overload" version = "0.1.1" @@ -2174,6 +2182,16 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43d84d1d7a6ac92673717f9f6d1518374ef257669c24ebc5ac25d5033828be58" +[[package]] +name = "proximity-sort" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a0f5e887b6bc3b8ce2a53445e94dc390786acbbd9a5714743513ef2dc8211d4" +dependencies = [ + "clap", + "os_str_bytes", +] + [[package]] name = "quick-xml" version = "0.31.0" @@ -2813,6 +2831,16 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "terminal_size" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" +dependencies = [ + "rustix", + "windows-sys 0.48.0", +] + [[package]] name = "thiserror" version = "1.0.61" diff --git a/Cargo.toml b/Cargo.toml index 726f960..1eb7d3d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,9 +8,6 @@ version = "0.1.0" anyhow = "1.0.68" arboard = "3.2.0" bitflags = "2.5.0" - - - blake3 = "1.5.1" cb = { package = "crossbeam-channel", version = "0.5.8" } cc = "1.0.79" @@ -41,6 +38,7 @@ notify = "6.0.0" num-traits = "0.2.15" once_cell = "1.17.1" opener = "0.7.0" +proximity-sort = "1.3.0" rayon = "1.7.0" ropey = "1.5.1" serde = "1.0.152" diff --git a/crates/ferrite-core/Cargo.toml b/crates/ferrite-core/Cargo.toml index 235d5b4..aec4e6d 100644 --- a/crates/ferrite-core/Cargo.toml +++ b/crates/ferrite-core/Cargo.toml @@ -29,6 +29,7 @@ memchr = { workspace = true } notify = { workspace = true } once_cell = { workspace = true } opener = { workspace = true } +proximity-sort = { workspace = true } rayon = { workspace = true } ropey = { workspace = true } serde = { workspace = true, features = ["derive"] } diff --git a/crates/ferrite-core/src/engine.rs b/crates/ferrite-core/src/engine.rs index e5e382f..4e02b98 100644 --- a/crates/ferrite-core/src/engine.rs +++ b/crates/ferrite-core/src/engine.rs @@ -126,6 +126,7 @@ impl Engine { file_finder = Some(SearchBuffer::new( FileFindProvider(daemon.subscribe()), proxy.dup(), + None, )); file_daemon = Some(daemon); } @@ -397,6 +398,12 @@ impl Engine { self.palette.reset(); match cmd_parser::parse_cmd(&content) { Ok(cmd) => match cmd { + Command::Path => match self.try_get_current_buffer_path() { + Some(path) => self.palette.set_msg(path.to_string_lossy()), + None => self + .palette + .set_error("No path has been set for the current buffer"), + }, Command::About => { self.palette.set_msg(format!( "ferrite\nVersion: {}\nCommit: {}", @@ -874,6 +881,7 @@ impl Engine { self.buffer_finder = Some(SearchBuffer::new( BufferFindProvider(buffers.into()), self.proxy.dup(), + self.try_get_current_buffer_path(), )); } @@ -883,6 +891,7 @@ impl Engine { self.file_finder = Some(SearchBuffer::new( FileFindProvider(self.file_daemon.subscribe()), self.proxy.dup(), + self.try_get_current_buffer_path(), )); } @@ -1066,6 +1075,10 @@ impl Engine { } } } + + fn try_get_current_buffer_path(&self) -> Option { + self.get_current_buffer()?.file().map(|p| p.to_owned()) + } } impl Drop for Engine { diff --git a/crates/ferrite-core/src/palette/cmd.rs b/crates/ferrite-core/src/palette/cmd.rs index 73cd545..be66137 100644 --- a/crates/ferrite-core/src/palette/cmd.rs +++ b/crates/ferrite-core/src/palette/cmd.rs @@ -16,6 +16,7 @@ pub enum Command { Case(Case), Split(Direction), About, + Path, Pwd, New, Reload, diff --git a/crates/ferrite-core/src/palette/cmd_parser.rs b/crates/ferrite-core/src/palette/cmd_parser.rs index 5e613c1..3d10b0f 100644 --- a/crates/ferrite-core/src/palette/cmd_parser.rs +++ b/crates/ferrite-core/src/palette/cmd_parser.rs @@ -40,6 +40,7 @@ pub fn parse_cmd(input: &str) -> Result { ("encoding", [encoding, ..]) => Command::Encoding(encoding.take().map(|encoding| encoding.unwrap_string())), ("indent", [indent, ..]) => Command::Indent(indent.take().map(|indent| indent.unwrap_string())), ("about", [..]) => Command::About, + ("path", [..]) => Command::Path, ("git-reload", [..]) => Command::GitReload, ("new", [..]) => Command::New, ("pwd", [..]) => Command::Pwd, @@ -117,6 +118,7 @@ static COMMANDS: Lazy> = Lazy::new(|| { CommandTemplate::new("pwd", None, true), CommandTemplate::new("indent", Some(("indent", CommandTemplateArg::String)), true), CommandTemplate::new("about", None, true), + CommandTemplate::new("path", None, true), CommandTemplate::new("git-reload", None, true), CommandTemplate::new("reload", None, true).add_alias("r"), CommandTemplate::new("logger", None, true).add_alias("log"), diff --git a/crates/ferrite-core/src/search_buffer.rs b/crates/ferrite-core/src/search_buffer.rs index 8ac27ba..c1b0283 100644 --- a/crates/ferrite-core/src/search_buffer.rs +++ b/crates/ferrite-core/src/search_buffer.rs @@ -1,4 +1,4 @@ -use std::{borrow::Cow, sync::Arc, thread}; +use std::{borrow::Cow, path::PathBuf, sync::Arc, thread}; use cb::select; use ferrite_utility::{graphemes::RopeGraphemeExt, line_ending::LineEnding}; @@ -34,6 +34,7 @@ where pub fn new + Send + Sync + 'static>( option_provder: T, proxy: Box, + path: Option, ) -> Self { let mut search_field = Buffer::new(); search_field.set_view_lines(1); @@ -77,7 +78,7 @@ where continue; } - let output = fuzzy_match::fuzzy_match(&query, (*options).clone()); + let output = fuzzy_match::fuzzy_match(&query, (*options).clone(), path.as_deref()); let result = SearchResult { matches: output, total: options.len(), @@ -120,17 +121,19 @@ where self.choice.take() } - pub fn get_matches(&mut self) -> &[FuzzyMatch] { - if let Ok(result) = self.rx.try_recv() { + fn poll_rx(&mut self) { + while let Ok(result) = self.rx.try_recv() { self.result = result; } + } + + pub fn get_matches(&mut self) -> &[FuzzyMatch] { + self.poll_rx(); &self.result.matches } pub fn get_total(&mut self) -> usize { - if let Ok(result) = self.rx.try_recv() { - self.result = result; - } + self.poll_rx(); self.result.total } diff --git a/crates/ferrite-core/src/search_buffer/fuzzy_match.rs b/crates/ferrite-core/src/search_buffer/fuzzy_match.rs index 53dd94a..c529bae 100644 --- a/crates/ferrite-core/src/search_buffer/fuzzy_match.rs +++ b/crates/ferrite-core/src/search_buffer/fuzzy_match.rs @@ -1,4 +1,4 @@ -use std::cmp; +use std::{cmp, path::Path}; use rayon::prelude::*; use sublime_fuzzy::{ContinuousMatch, FuzzySearch, Scoring}; @@ -8,6 +8,7 @@ use super::Matchable; #[derive(Debug, Clone)] pub struct FuzzyMatch { pub score: i64, + pub proximity: i64, pub item: T, pub matches: Vec, } @@ -44,17 +45,25 @@ impl PartialOrd for FuzzyMatch { impl Ord for FuzzyMatch { fn cmp(&self, other: &Self) -> cmp::Ordering { match self.score.cmp(&other.score) { - cmp::Ordering::Equal => lexical_sort::natural_lexical_cmp( - &self.item.as_match_str(), - &self.item.as_match_str(), - ), + cmp::Ordering::Equal => match self.proximity.cmp(&other.proximity) { + cmp::Ordering::Equal => lexical_sort::natural_lexical_cmp( + &self.item.as_match_str(), + &self.item.as_match_str(), + ), + cmp::Ordering::Greater => cmp::Ordering::Less, + cmp::Ordering::Less => cmp::Ordering::Greater, + }, cmp::Ordering::Greater => cmp::Ordering::Less, cmp::Ordering::Less => cmp::Ordering::Greater, } } } -pub fn fuzzy_match(term: &str, items: Vec) -> Vec> { +pub fn fuzzy_match( + term: &str, + items: Vec, + path: Option<&Path>, +) -> Vec> { let scoring = Scoring::emphasize_distance(); let mut matches: Vec<_> = items .into_par_iter() @@ -62,6 +71,7 @@ pub fn fuzzy_match(term: &str, items: Vec) -> Vec if term.is_empty() { return Some(FuzzyMatch { score: 0, + proximity: 0, item, matches: Vec::new(), }); @@ -70,10 +80,44 @@ pub fn fuzzy_match(term: &str, items: Vec) -> Vec FuzzySearch::new(term, &item.as_match_str()) .score_with(&scoring) .best_match() - .map(|m| FuzzyMatch { - score: m.score() as i64, - item, - matches: m.continuous_matches().map(|m| m.into()).collect(), + .map(|m| { + let proximity = match path { + Some(path) => { + let mut missed = false; + let mut path = path.iter(); + Path::new(&*item.as_match_str()) + .components() + .skip_while(|c| matches!(c, std::path::Component::CurDir)) + .map(|c| { + // if we've already missed, each additional dir is one further away + if missed { + return -1; + } + + // we want to score positively if c matches the next segment from target path + if let Some(p) = path.next() { + if p == c.as_os_str() { + // matching path segment! + return 1; + } else { + // non-matching path segment + missed = true; + } + } + + -1 + }) + .sum() + } + None => 0, + }; + + FuzzyMatch { + score: m.score() as i64, + proximity, + item, + matches: m.continuous_matches().map(|m| m.into()).collect(), + } }) }) .collect();