mirror of
https://github.com/zed-industries/zed.git
synced 2024-09-19 10:29:35 +03:00
Add recent project picker
This commit is contained in:
parent
b1e37378dc
commit
2bc685281c
19
Cargo.lock
generated
19
Cargo.lock
generated
@ -4805,6 +4805,24 @@ dependencies = [
|
|||||||
"rand_core 0.3.1",
|
"rand_core 0.3.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "recent_projects"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"db",
|
||||||
|
"editor",
|
||||||
|
"fuzzy",
|
||||||
|
"gpui",
|
||||||
|
"language",
|
||||||
|
"ordered-float",
|
||||||
|
"picker",
|
||||||
|
"postage",
|
||||||
|
"settings",
|
||||||
|
"smol",
|
||||||
|
"text",
|
||||||
|
"workspace",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.2.16"
|
version = "0.2.16"
|
||||||
@ -8152,6 +8170,7 @@ dependencies = [
|
|||||||
"project_panel",
|
"project_panel",
|
||||||
"project_symbols",
|
"project_symbols",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
|
"recent_projects",
|
||||||
"regex",
|
"regex",
|
||||||
"rpc",
|
"rpc",
|
||||||
"rsa",
|
"rsa",
|
||||||
|
@ -40,6 +40,7 @@ members = [
|
|||||||
"crates/project",
|
"crates/project",
|
||||||
"crates/project_panel",
|
"crates/project_panel",
|
||||||
"crates/project_symbols",
|
"crates/project_symbols",
|
||||||
|
"crates/recent_projects",
|
||||||
"crates/rope",
|
"crates/rope",
|
||||||
"crates/rpc",
|
"crates/rpc",
|
||||||
"crates/search",
|
"crates/search",
|
||||||
|
@ -62,11 +62,12 @@ impl View for FileFinder {
|
|||||||
|
|
||||||
impl FileFinder {
|
impl FileFinder {
|
||||||
fn labels_for_match(&self, path_match: &PathMatch) -> (String, Vec<usize>, String, Vec<usize>) {
|
fn labels_for_match(&self, path_match: &PathMatch) -> (String, Vec<usize>, String, Vec<usize>) {
|
||||||
let path_string = path_match.path.to_string_lossy();
|
let path = &path_match.path;
|
||||||
|
let path_string = path.to_string_lossy();
|
||||||
let full_path = [path_match.path_prefix.as_ref(), path_string.as_ref()].join("");
|
let full_path = [path_match.path_prefix.as_ref(), path_string.as_ref()].join("");
|
||||||
let path_positions = path_match.positions.clone();
|
let path_positions = path_match.positions.clone();
|
||||||
|
|
||||||
let file_name = path_match.path.file_name().map_or_else(
|
let file_name = path.file_name().map_or_else(
|
||||||
|| path_match.path_prefix.to_string(),
|
|| path_match.path_prefix.to_string(),
|
||||||
|file_name| file_name.to_string_lossy().to_string(),
|
|file_name| file_name.to_string_lossy().to_string(),
|
||||||
);
|
);
|
||||||
@ -161,7 +162,7 @@ impl FileFinder {
|
|||||||
self.cancel_flag = Arc::new(AtomicBool::new(false));
|
self.cancel_flag = Arc::new(AtomicBool::new(false));
|
||||||
let cancel_flag = self.cancel_flag.clone();
|
let cancel_flag = self.cancel_flag.clone();
|
||||||
cx.spawn(|this, mut cx| async move {
|
cx.spawn(|this, mut cx| async move {
|
||||||
let matches = fuzzy::match_paths(
|
let matches = fuzzy::match_path_sets(
|
||||||
candidate_sets.as_slice(),
|
candidate_sets.as_slice(),
|
||||||
&query,
|
&query,
|
||||||
false,
|
false,
|
||||||
|
@ -1,794 +1,8 @@
|
|||||||
mod char_bag;
|
mod char_bag;
|
||||||
|
mod matcher;
|
||||||
use gpui::executor;
|
mod paths;
|
||||||
use std::{
|
mod strings;
|
||||||
borrow::Cow,
|
|
||||||
cmp::{self, Ordering},
|
|
||||||
path::Path,
|
|
||||||
sync::atomic::{self, AtomicBool},
|
|
||||||
sync::Arc,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub use char_bag::CharBag;
|
pub use char_bag::CharBag;
|
||||||
|
pub use paths::{match_path_sets, PathMatch, PathMatchCandidate, PathMatchCandidateSet};
|
||||||
const BASE_DISTANCE_PENALTY: f64 = 0.6;
|
pub use strings::{match_strings, StringMatch, StringMatchCandidate};
|
||||||
const ADDITIONAL_DISTANCE_PENALTY: f64 = 0.05;
|
|
||||||
const MIN_DISTANCE_PENALTY: f64 = 0.2;
|
|
||||||
|
|
||||||
pub struct Matcher<'a> {
|
|
||||||
query: &'a [char],
|
|
||||||
lowercase_query: &'a [char],
|
|
||||||
query_char_bag: CharBag,
|
|
||||||
smart_case: bool,
|
|
||||||
max_results: usize,
|
|
||||||
min_score: f64,
|
|
||||||
match_positions: Vec<usize>,
|
|
||||||
last_positions: Vec<usize>,
|
|
||||||
score_matrix: Vec<Option<f64>>,
|
|
||||||
best_position_matrix: Vec<usize>,
|
|
||||||
}
|
|
||||||
|
|
||||||
trait Match: Ord {
|
|
||||||
fn score(&self) -> f64;
|
|
||||||
fn set_positions(&mut self, positions: Vec<usize>);
|
|
||||||
}
|
|
||||||
|
|
||||||
trait MatchCandidate {
|
|
||||||
fn has_chars(&self, bag: CharBag) -> bool;
|
|
||||||
fn to_string(&self) -> Cow<'_, str>;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct PathMatchCandidate<'a> {
|
|
||||||
pub path: &'a Arc<Path>,
|
|
||||||
pub char_bag: CharBag,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct PathMatch {
|
|
||||||
pub score: f64,
|
|
||||||
pub positions: Vec<usize>,
|
|
||||||
pub worktree_id: usize,
|
|
||||||
pub path: Arc<Path>,
|
|
||||||
pub path_prefix: Arc<str>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct StringMatchCandidate {
|
|
||||||
pub id: usize,
|
|
||||||
pub string: String,
|
|
||||||
pub char_bag: CharBag,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait PathMatchCandidateSet<'a>: Send + Sync {
|
|
||||||
type Candidates: Iterator<Item = PathMatchCandidate<'a>>;
|
|
||||||
fn id(&self) -> usize;
|
|
||||||
fn len(&self) -> usize;
|
|
||||||
fn is_empty(&self) -> bool {
|
|
||||||
self.len() == 0
|
|
||||||
}
|
|
||||||
fn prefix(&self) -> Arc<str>;
|
|
||||||
fn candidates(&'a self, start: usize) -> Self::Candidates;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Match for PathMatch {
|
|
||||||
fn score(&self) -> f64 {
|
|
||||||
self.score
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_positions(&mut self, positions: Vec<usize>) {
|
|
||||||
self.positions = positions;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Match for StringMatch {
|
|
||||||
fn score(&self) -> f64 {
|
|
||||||
self.score
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_positions(&mut self, positions: Vec<usize>) {
|
|
||||||
self.positions = positions;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> MatchCandidate for PathMatchCandidate<'a> {
|
|
||||||
fn has_chars(&self, bag: CharBag) -> bool {
|
|
||||||
self.char_bag.is_superset(bag)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn to_string(&self) -> Cow<'a, str> {
|
|
||||||
self.path.to_string_lossy()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl StringMatchCandidate {
|
|
||||||
pub fn new(id: usize, string: String) -> Self {
|
|
||||||
Self {
|
|
||||||
id,
|
|
||||||
char_bag: CharBag::from(string.as_str()),
|
|
||||||
string,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> MatchCandidate for &'a StringMatchCandidate {
|
|
||||||
fn has_chars(&self, bag: CharBag) -> bool {
|
|
||||||
self.char_bag.is_superset(bag)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn to_string(&self) -> Cow<'a, str> {
|
|
||||||
self.string.as_str().into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct StringMatch {
|
|
||||||
pub candidate_id: usize,
|
|
||||||
pub score: f64,
|
|
||||||
pub positions: Vec<usize>,
|
|
||||||
pub string: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PartialEq for StringMatch {
|
|
||||||
fn eq(&self, other: &Self) -> bool {
|
|
||||||
self.cmp(other).is_eq()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Eq for StringMatch {}
|
|
||||||
|
|
||||||
impl PartialOrd for StringMatch {
|
|
||||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
|
||||||
Some(self.cmp(other))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Ord for StringMatch {
|
|
||||||
fn cmp(&self, other: &Self) -> Ordering {
|
|
||||||
self.score
|
|
||||||
.partial_cmp(&other.score)
|
|
||||||
.unwrap_or(Ordering::Equal)
|
|
||||||
.then_with(|| self.candidate_id.cmp(&other.candidate_id))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PartialEq for PathMatch {
|
|
||||||
fn eq(&self, other: &Self) -> bool {
|
|
||||||
self.cmp(other).is_eq()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Eq for PathMatch {}
|
|
||||||
|
|
||||||
impl PartialOrd for PathMatch {
|
|
||||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
|
||||||
Some(self.cmp(other))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Ord for PathMatch {
|
|
||||||
fn cmp(&self, other: &Self) -> Ordering {
|
|
||||||
self.score
|
|
||||||
.partial_cmp(&other.score)
|
|
||||||
.unwrap_or(Ordering::Equal)
|
|
||||||
.then_with(|| self.worktree_id.cmp(&other.worktree_id))
|
|
||||||
.then_with(|| Arc::as_ptr(&self.path).cmp(&Arc::as_ptr(&other.path)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn match_strings(
|
|
||||||
candidates: &[StringMatchCandidate],
|
|
||||||
query: &str,
|
|
||||||
smart_case: bool,
|
|
||||||
max_results: usize,
|
|
||||||
cancel_flag: &AtomicBool,
|
|
||||||
background: Arc<executor::Background>,
|
|
||||||
) -> Vec<StringMatch> {
|
|
||||||
if candidates.is_empty() || max_results == 0 {
|
|
||||||
return Default::default();
|
|
||||||
}
|
|
||||||
|
|
||||||
if query.is_empty() {
|
|
||||||
return candidates
|
|
||||||
.iter()
|
|
||||||
.map(|candidate| StringMatch {
|
|
||||||
candidate_id: candidate.id,
|
|
||||||
score: 0.,
|
|
||||||
positions: Default::default(),
|
|
||||||
string: candidate.string.clone(),
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
}
|
|
||||||
|
|
||||||
let lowercase_query = query.to_lowercase().chars().collect::<Vec<_>>();
|
|
||||||
let query = query.chars().collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let lowercase_query = &lowercase_query;
|
|
||||||
let query = &query;
|
|
||||||
let query_char_bag = CharBag::from(&lowercase_query[..]);
|
|
||||||
|
|
||||||
let num_cpus = background.num_cpus().min(candidates.len());
|
|
||||||
let segment_size = (candidates.len() + num_cpus - 1) / num_cpus;
|
|
||||||
let mut segment_results = (0..num_cpus)
|
|
||||||
.map(|_| Vec::with_capacity(max_results.min(candidates.len())))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
background
|
|
||||||
.scoped(|scope| {
|
|
||||||
for (segment_idx, results) in segment_results.iter_mut().enumerate() {
|
|
||||||
let cancel_flag = &cancel_flag;
|
|
||||||
scope.spawn(async move {
|
|
||||||
let segment_start = cmp::min(segment_idx * segment_size, candidates.len());
|
|
||||||
let segment_end = cmp::min(segment_start + segment_size, candidates.len());
|
|
||||||
let mut matcher = Matcher::new(
|
|
||||||
query,
|
|
||||||
lowercase_query,
|
|
||||||
query_char_bag,
|
|
||||||
smart_case,
|
|
||||||
max_results,
|
|
||||||
);
|
|
||||||
matcher.match_strings(
|
|
||||||
&candidates[segment_start..segment_end],
|
|
||||||
results,
|
|
||||||
cancel_flag,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let mut results = Vec::new();
|
|
||||||
for segment_result in segment_results {
|
|
||||||
if results.is_empty() {
|
|
||||||
results = segment_result;
|
|
||||||
} else {
|
|
||||||
util::extend_sorted(&mut results, segment_result, max_results, |a, b| b.cmp(a));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
results
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn match_paths<'a, Set: PathMatchCandidateSet<'a>>(
|
|
||||||
candidate_sets: &'a [Set],
|
|
||||||
query: &str,
|
|
||||||
smart_case: bool,
|
|
||||||
max_results: usize,
|
|
||||||
cancel_flag: &AtomicBool,
|
|
||||||
background: Arc<executor::Background>,
|
|
||||||
) -> Vec<PathMatch> {
|
|
||||||
let path_count: usize = candidate_sets.iter().map(|s| s.len()).sum();
|
|
||||||
if path_count == 0 {
|
|
||||||
return Vec::new();
|
|
||||||
}
|
|
||||||
|
|
||||||
let lowercase_query = query.to_lowercase().chars().collect::<Vec<_>>();
|
|
||||||
let query = query.chars().collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let lowercase_query = &lowercase_query;
|
|
||||||
let query = &query;
|
|
||||||
let query_char_bag = CharBag::from(&lowercase_query[..]);
|
|
||||||
|
|
||||||
let num_cpus = background.num_cpus().min(path_count);
|
|
||||||
let segment_size = (path_count + num_cpus - 1) / num_cpus;
|
|
||||||
let mut segment_results = (0..num_cpus)
|
|
||||||
.map(|_| Vec::with_capacity(max_results))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
background
|
|
||||||
.scoped(|scope| {
|
|
||||||
for (segment_idx, results) in segment_results.iter_mut().enumerate() {
|
|
||||||
scope.spawn(async move {
|
|
||||||
let segment_start = segment_idx * segment_size;
|
|
||||||
let segment_end = segment_start + segment_size;
|
|
||||||
let mut matcher = Matcher::new(
|
|
||||||
query,
|
|
||||||
lowercase_query,
|
|
||||||
query_char_bag,
|
|
||||||
smart_case,
|
|
||||||
max_results,
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut tree_start = 0;
|
|
||||||
for candidate_set in candidate_sets {
|
|
||||||
let tree_end = tree_start + candidate_set.len();
|
|
||||||
|
|
||||||
if tree_start < segment_end && segment_start < tree_end {
|
|
||||||
let start = cmp::max(tree_start, segment_start) - tree_start;
|
|
||||||
let end = cmp::min(tree_end, segment_end) - tree_start;
|
|
||||||
let candidates = candidate_set.candidates(start).take(end - start);
|
|
||||||
|
|
||||||
matcher.match_paths(
|
|
||||||
candidate_set.id(),
|
|
||||||
candidate_set.prefix(),
|
|
||||||
candidates,
|
|
||||||
results,
|
|
||||||
cancel_flag,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if tree_end >= segment_end {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
tree_start = tree_end;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let mut results = Vec::new();
|
|
||||||
for segment_result in segment_results {
|
|
||||||
if results.is_empty() {
|
|
||||||
results = segment_result;
|
|
||||||
} else {
|
|
||||||
util::extend_sorted(&mut results, segment_result, max_results, |a, b| b.cmp(a));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
results
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Matcher<'a> {
|
|
||||||
pub fn new(
|
|
||||||
query: &'a [char],
|
|
||||||
lowercase_query: &'a [char],
|
|
||||||
query_char_bag: CharBag,
|
|
||||||
smart_case: bool,
|
|
||||||
max_results: usize,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
query,
|
|
||||||
lowercase_query,
|
|
||||||
query_char_bag,
|
|
||||||
min_score: 0.0,
|
|
||||||
last_positions: vec![0; query.len()],
|
|
||||||
match_positions: vec![0; query.len()],
|
|
||||||
score_matrix: Vec::new(),
|
|
||||||
best_position_matrix: Vec::new(),
|
|
||||||
smart_case,
|
|
||||||
max_results,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn match_strings(
|
|
||||||
&mut self,
|
|
||||||
candidates: &[StringMatchCandidate],
|
|
||||||
results: &mut Vec<StringMatch>,
|
|
||||||
cancel_flag: &AtomicBool,
|
|
||||||
) {
|
|
||||||
self.match_internal(
|
|
||||||
&[],
|
|
||||||
&[],
|
|
||||||
candidates.iter(),
|
|
||||||
results,
|
|
||||||
cancel_flag,
|
|
||||||
|candidate, score| StringMatch {
|
|
||||||
candidate_id: candidate.id,
|
|
||||||
score,
|
|
||||||
positions: Vec::new(),
|
|
||||||
string: candidate.string.to_string(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn match_paths<'c: 'a>(
|
|
||||||
&mut self,
|
|
||||||
tree_id: usize,
|
|
||||||
path_prefix: Arc<str>,
|
|
||||||
path_entries: impl Iterator<Item = PathMatchCandidate<'c>>,
|
|
||||||
results: &mut Vec<PathMatch>,
|
|
||||||
cancel_flag: &AtomicBool,
|
|
||||||
) {
|
|
||||||
let prefix = path_prefix.chars().collect::<Vec<_>>();
|
|
||||||
let lowercase_prefix = prefix
|
|
||||||
.iter()
|
|
||||||
.map(|c| c.to_ascii_lowercase())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
self.match_internal(
|
|
||||||
&prefix,
|
|
||||||
&lowercase_prefix,
|
|
||||||
path_entries,
|
|
||||||
results,
|
|
||||||
cancel_flag,
|
|
||||||
|candidate, score| PathMatch {
|
|
||||||
score,
|
|
||||||
worktree_id: tree_id,
|
|
||||||
positions: Vec::new(),
|
|
||||||
path: candidate.path.clone(),
|
|
||||||
path_prefix: path_prefix.clone(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn match_internal<C: MatchCandidate, R, F>(
|
|
||||||
&mut self,
|
|
||||||
prefix: &[char],
|
|
||||||
lowercase_prefix: &[char],
|
|
||||||
candidates: impl Iterator<Item = C>,
|
|
||||||
results: &mut Vec<R>,
|
|
||||||
cancel_flag: &AtomicBool,
|
|
||||||
build_match: F,
|
|
||||||
) where
|
|
||||||
R: Match,
|
|
||||||
F: Fn(&C, f64) -> R,
|
|
||||||
{
|
|
||||||
let mut candidate_chars = Vec::new();
|
|
||||||
let mut lowercase_candidate_chars = Vec::new();
|
|
||||||
|
|
||||||
for candidate in candidates {
|
|
||||||
if !candidate.has_chars(self.query_char_bag) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if cancel_flag.load(atomic::Ordering::Relaxed) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
candidate_chars.clear();
|
|
||||||
lowercase_candidate_chars.clear();
|
|
||||||
for c in candidate.to_string().chars() {
|
|
||||||
candidate_chars.push(c);
|
|
||||||
lowercase_candidate_chars.push(c.to_ascii_lowercase());
|
|
||||||
}
|
|
||||||
|
|
||||||
if !self.find_last_positions(lowercase_prefix, &lowercase_candidate_chars) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let matrix_len = self.query.len() * (prefix.len() + candidate_chars.len());
|
|
||||||
self.score_matrix.clear();
|
|
||||||
self.score_matrix.resize(matrix_len, None);
|
|
||||||
self.best_position_matrix.clear();
|
|
||||||
self.best_position_matrix.resize(matrix_len, 0);
|
|
||||||
|
|
||||||
let score = self.score_match(
|
|
||||||
&candidate_chars,
|
|
||||||
&lowercase_candidate_chars,
|
|
||||||
prefix,
|
|
||||||
lowercase_prefix,
|
|
||||||
);
|
|
||||||
|
|
||||||
if score > 0.0 {
|
|
||||||
let mut mat = build_match(&candidate, score);
|
|
||||||
if let Err(i) = results.binary_search_by(|m| mat.cmp(m)) {
|
|
||||||
if results.len() < self.max_results {
|
|
||||||
mat.set_positions(self.match_positions.clone());
|
|
||||||
results.insert(i, mat);
|
|
||||||
} else if i < results.len() {
|
|
||||||
results.pop();
|
|
||||||
mat.set_positions(self.match_positions.clone());
|
|
||||||
results.insert(i, mat);
|
|
||||||
}
|
|
||||||
if results.len() == self.max_results {
|
|
||||||
self.min_score = results.last().unwrap().score();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn find_last_positions(
|
|
||||||
&mut self,
|
|
||||||
lowercase_prefix: &[char],
|
|
||||||
lowercase_candidate: &[char],
|
|
||||||
) -> bool {
|
|
||||||
let mut lowercase_prefix = lowercase_prefix.iter();
|
|
||||||
let mut lowercase_candidate = lowercase_candidate.iter();
|
|
||||||
for (i, char) in self.lowercase_query.iter().enumerate().rev() {
|
|
||||||
if let Some(j) = lowercase_candidate.rposition(|c| c == char) {
|
|
||||||
self.last_positions[i] = j + lowercase_prefix.len();
|
|
||||||
} else if let Some(j) = lowercase_prefix.rposition(|c| c == char) {
|
|
||||||
self.last_positions[i] = j;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
fn score_match(
|
|
||||||
&mut self,
|
|
||||||
path: &[char],
|
|
||||||
path_cased: &[char],
|
|
||||||
prefix: &[char],
|
|
||||||
lowercase_prefix: &[char],
|
|
||||||
) -> f64 {
|
|
||||||
let score = self.recursive_score_match(
|
|
||||||
path,
|
|
||||||
path_cased,
|
|
||||||
prefix,
|
|
||||||
lowercase_prefix,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
self.query.len() as f64,
|
|
||||||
) * self.query.len() as f64;
|
|
||||||
|
|
||||||
if score <= 0.0 {
|
|
||||||
return 0.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
let path_len = prefix.len() + path.len();
|
|
||||||
let mut cur_start = 0;
|
|
||||||
let mut byte_ix = 0;
|
|
||||||
let mut char_ix = 0;
|
|
||||||
for i in 0..self.query.len() {
|
|
||||||
let match_char_ix = self.best_position_matrix[i * path_len + cur_start];
|
|
||||||
while char_ix < match_char_ix {
|
|
||||||
let ch = prefix
|
|
||||||
.get(char_ix)
|
|
||||||
.or_else(|| path.get(char_ix - prefix.len()))
|
|
||||||
.unwrap();
|
|
||||||
byte_ix += ch.len_utf8();
|
|
||||||
char_ix += 1;
|
|
||||||
}
|
|
||||||
cur_start = match_char_ix + 1;
|
|
||||||
self.match_positions[i] = byte_ix;
|
|
||||||
}
|
|
||||||
|
|
||||||
score
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn recursive_score_match(
|
|
||||||
&mut self,
|
|
||||||
path: &[char],
|
|
||||||
path_cased: &[char],
|
|
||||||
prefix: &[char],
|
|
||||||
lowercase_prefix: &[char],
|
|
||||||
query_idx: usize,
|
|
||||||
path_idx: usize,
|
|
||||||
cur_score: f64,
|
|
||||||
) -> f64 {
|
|
||||||
if query_idx == self.query.len() {
|
|
||||||
return 1.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
let path_len = prefix.len() + path.len();
|
|
||||||
|
|
||||||
if let Some(memoized) = self.score_matrix[query_idx * path_len + path_idx] {
|
|
||||||
return memoized;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut score = 0.0;
|
|
||||||
let mut best_position = 0;
|
|
||||||
|
|
||||||
let query_char = self.lowercase_query[query_idx];
|
|
||||||
let limit = self.last_positions[query_idx];
|
|
||||||
|
|
||||||
let mut last_slash = 0;
|
|
||||||
for j in path_idx..=limit {
|
|
||||||
let path_char = if j < prefix.len() {
|
|
||||||
lowercase_prefix[j]
|
|
||||||
} else {
|
|
||||||
path_cased[j - prefix.len()]
|
|
||||||
};
|
|
||||||
let is_path_sep = path_char == '/' || path_char == '\\';
|
|
||||||
|
|
||||||
if query_idx == 0 && is_path_sep {
|
|
||||||
last_slash = j;
|
|
||||||
}
|
|
||||||
|
|
||||||
if query_char == path_char || (is_path_sep && query_char == '_' || query_char == '\\') {
|
|
||||||
let curr = if j < prefix.len() {
|
|
||||||
prefix[j]
|
|
||||||
} else {
|
|
||||||
path[j - prefix.len()]
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut char_score = 1.0;
|
|
||||||
if j > path_idx {
|
|
||||||
let last = if j - 1 < prefix.len() {
|
|
||||||
prefix[j - 1]
|
|
||||||
} else {
|
|
||||||
path[j - 1 - prefix.len()]
|
|
||||||
};
|
|
||||||
|
|
||||||
if last == '/' {
|
|
||||||
char_score = 0.9;
|
|
||||||
} else if (last == '-' || last == '_' || last == ' ' || last.is_numeric())
|
|
||||||
|| (last.is_lowercase() && curr.is_uppercase())
|
|
||||||
{
|
|
||||||
char_score = 0.8;
|
|
||||||
} else if last == '.' {
|
|
||||||
char_score = 0.7;
|
|
||||||
} else if query_idx == 0 {
|
|
||||||
char_score = BASE_DISTANCE_PENALTY;
|
|
||||||
} else {
|
|
||||||
char_score = MIN_DISTANCE_PENALTY.max(
|
|
||||||
BASE_DISTANCE_PENALTY
|
|
||||||
- (j - path_idx - 1) as f64 * ADDITIONAL_DISTANCE_PENALTY,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply a severe penalty if the case doesn't match.
|
|
||||||
// This will make the exact matches have higher score than the case-insensitive and the
|
|
||||||
// path insensitive matches.
|
|
||||||
if (self.smart_case || curr == '/') && self.query[query_idx] != curr {
|
|
||||||
char_score *= 0.001;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut multiplier = char_score;
|
|
||||||
|
|
||||||
// Scale the score based on how deep within the path we found the match.
|
|
||||||
if query_idx == 0 {
|
|
||||||
multiplier /= ((prefix.len() + path.len()) - last_slash) as f64;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut next_score = 1.0;
|
|
||||||
if self.min_score > 0.0 {
|
|
||||||
next_score = cur_score * multiplier;
|
|
||||||
// Scores only decrease. If we can't pass the previous best, bail
|
|
||||||
if next_score < self.min_score {
|
|
||||||
// Ensure that score is non-zero so we use it in the memo table.
|
|
||||||
if score == 0.0 {
|
|
||||||
score = 1e-18;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let new_score = self.recursive_score_match(
|
|
||||||
path,
|
|
||||||
path_cased,
|
|
||||||
prefix,
|
|
||||||
lowercase_prefix,
|
|
||||||
query_idx + 1,
|
|
||||||
j + 1,
|
|
||||||
next_score,
|
|
||||||
) * multiplier;
|
|
||||||
|
|
||||||
if new_score > score {
|
|
||||||
score = new_score;
|
|
||||||
best_position = j;
|
|
||||||
// Optimization: can't score better than 1.
|
|
||||||
if new_score == 1.0 {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if best_position != 0 {
|
|
||||||
self.best_position_matrix[query_idx * path_len + path_idx] = best_position;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.score_matrix[query_idx * path_len + path_idx] = Some(score);
|
|
||||||
score
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_get_last_positions() {
|
|
||||||
let mut query: &[char] = &['d', 'c'];
|
|
||||||
let mut matcher = Matcher::new(query, query, query.into(), false, 10);
|
|
||||||
let result = matcher.find_last_positions(&['a', 'b', 'c'], &['b', 'd', 'e', 'f']);
|
|
||||||
assert!(!result);
|
|
||||||
|
|
||||||
query = &['c', 'd'];
|
|
||||||
let mut matcher = Matcher::new(query, query, query.into(), false, 10);
|
|
||||||
let result = matcher.find_last_positions(&['a', 'b', 'c'], &['b', 'd', 'e', 'f']);
|
|
||||||
assert!(result);
|
|
||||||
assert_eq!(matcher.last_positions, vec![2, 4]);
|
|
||||||
|
|
||||||
query = &['z', '/', 'z', 'f'];
|
|
||||||
let mut matcher = Matcher::new(query, query, query.into(), false, 10);
|
|
||||||
let result = matcher.find_last_positions(&['z', 'e', 'd', '/'], &['z', 'e', 'd', '/', 'f']);
|
|
||||||
assert!(result);
|
|
||||||
assert_eq!(matcher.last_positions, vec![0, 3, 4, 8]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_match_path_entries() {
|
|
||||||
let paths = vec![
|
|
||||||
"",
|
|
||||||
"a",
|
|
||||||
"ab",
|
|
||||||
"abC",
|
|
||||||
"abcd",
|
|
||||||
"alphabravocharlie",
|
|
||||||
"AlphaBravoCharlie",
|
|
||||||
"thisisatestdir",
|
|
||||||
"/////ThisIsATestDir",
|
|
||||||
"/this/is/a/test/dir",
|
|
||||||
"/test/tiatd",
|
|
||||||
];
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
match_query("abc", false, &paths),
|
|
||||||
vec![
|
|
||||||
("abC", vec![0, 1, 2]),
|
|
||||||
("abcd", vec![0, 1, 2]),
|
|
||||||
("AlphaBravoCharlie", vec![0, 5, 10]),
|
|
||||||
("alphabravocharlie", vec![4, 5, 10]),
|
|
||||||
]
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
match_query("t/i/a/t/d", false, &paths),
|
|
||||||
vec![("/this/is/a/test/dir", vec![1, 5, 6, 8, 9, 10, 11, 15, 16]),]
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
match_query("tiatd", false, &paths),
|
|
||||||
vec![
|
|
||||||
("/test/tiatd", vec![6, 7, 8, 9, 10]),
|
|
||||||
("/this/is/a/test/dir", vec![1, 6, 9, 11, 16]),
|
|
||||||
("/////ThisIsATestDir", vec![5, 9, 11, 12, 16]),
|
|
||||||
("thisisatestdir", vec![0, 2, 6, 7, 11]),
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_match_multibyte_path_entries() {
|
|
||||||
let paths = vec!["aαbβ/cγdδ", "αβγδ/bcde", "c1️⃣2️⃣3️⃣/d4️⃣5️⃣6️⃣/e7️⃣8️⃣9️⃣/f", "/d/🆒/h"];
|
|
||||||
assert_eq!("1️⃣".len(), 7);
|
|
||||||
assert_eq!(
|
|
||||||
match_query("bcd", false, &paths),
|
|
||||||
vec![
|
|
||||||
("αβγδ/bcde", vec![9, 10, 11]),
|
|
||||||
("aαbβ/cγdδ", vec![3, 7, 10]),
|
|
||||||
]
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
match_query("cde", false, &paths),
|
|
||||||
vec![
|
|
||||||
("αβγδ/bcde", vec![10, 11, 12]),
|
|
||||||
("c1️⃣2️⃣3️⃣/d4️⃣5️⃣6️⃣/e7️⃣8️⃣9️⃣/f", vec![0, 23, 46]),
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn match_query<'a>(
|
|
||||||
query: &str,
|
|
||||||
smart_case: bool,
|
|
||||||
paths: &[&'a str],
|
|
||||||
) -> Vec<(&'a str, Vec<usize>)> {
|
|
||||||
let lowercase_query = query.to_lowercase().chars().collect::<Vec<_>>();
|
|
||||||
let query = query.chars().collect::<Vec<_>>();
|
|
||||||
let query_chars = CharBag::from(&lowercase_query[..]);
|
|
||||||
|
|
||||||
let path_arcs = paths
|
|
||||||
.iter()
|
|
||||||
.map(|path| Arc::from(PathBuf::from(path)))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
let mut path_entries = Vec::new();
|
|
||||||
for (i, path) in paths.iter().enumerate() {
|
|
||||||
let lowercase_path = path.to_lowercase().chars().collect::<Vec<_>>();
|
|
||||||
let char_bag = CharBag::from(lowercase_path.as_slice());
|
|
||||||
path_entries.push(PathMatchCandidate {
|
|
||||||
char_bag,
|
|
||||||
path: path_arcs.get(i).unwrap(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut matcher = Matcher::new(&query, &lowercase_query, query_chars, smart_case, 100);
|
|
||||||
|
|
||||||
let cancel_flag = AtomicBool::new(false);
|
|
||||||
let mut results = Vec::new();
|
|
||||||
matcher.match_paths(
|
|
||||||
0,
|
|
||||||
"".into(),
|
|
||||||
path_entries.into_iter(),
|
|
||||||
&mut results,
|
|
||||||
&cancel_flag,
|
|
||||||
);
|
|
||||||
|
|
||||||
results
|
|
||||||
.into_iter()
|
|
||||||
.map(|result| {
|
|
||||||
(
|
|
||||||
paths
|
|
||||||
.iter()
|
|
||||||
.copied()
|
|
||||||
.find(|p| result.path.as_ref() == Path::new(p))
|
|
||||||
.unwrap(),
|
|
||||||
result.positions,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
463
crates/fuzzy/src/matcher.rs
Normal file
463
crates/fuzzy/src/matcher.rs
Normal file
@ -0,0 +1,463 @@
|
|||||||
|
use std::{
|
||||||
|
borrow::Cow,
|
||||||
|
sync::atomic::{self, AtomicBool},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::CharBag;
|
||||||
|
|
||||||
|
const BASE_DISTANCE_PENALTY: f64 = 0.6;
|
||||||
|
const ADDITIONAL_DISTANCE_PENALTY: f64 = 0.05;
|
||||||
|
const MIN_DISTANCE_PENALTY: f64 = 0.2;
|
||||||
|
|
||||||
|
pub struct Matcher<'a> {
|
||||||
|
query: &'a [char],
|
||||||
|
lowercase_query: &'a [char],
|
||||||
|
query_char_bag: CharBag,
|
||||||
|
smart_case: bool,
|
||||||
|
max_results: usize,
|
||||||
|
min_score: f64,
|
||||||
|
match_positions: Vec<usize>,
|
||||||
|
last_positions: Vec<usize>,
|
||||||
|
score_matrix: Vec<Option<f64>>,
|
||||||
|
best_position_matrix: Vec<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Match: Ord {
|
||||||
|
fn score(&self) -> f64;
|
||||||
|
fn set_positions(&mut self, positions: Vec<usize>);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait MatchCandidate {
|
||||||
|
fn has_chars(&self, bag: CharBag) -> bool;
|
||||||
|
fn to_string(&self) -> Cow<'_, str>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Matcher<'a> {
|
||||||
|
pub fn new(
|
||||||
|
query: &'a [char],
|
||||||
|
lowercase_query: &'a [char],
|
||||||
|
query_char_bag: CharBag,
|
||||||
|
smart_case: bool,
|
||||||
|
max_results: usize,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
query,
|
||||||
|
lowercase_query,
|
||||||
|
query_char_bag,
|
||||||
|
min_score: 0.0,
|
||||||
|
last_positions: vec![0; query.len()],
|
||||||
|
match_positions: vec![0; query.len()],
|
||||||
|
score_matrix: Vec::new(),
|
||||||
|
best_position_matrix: Vec::new(),
|
||||||
|
smart_case,
|
||||||
|
max_results,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn match_candidates<C: MatchCandidate, R, F>(
|
||||||
|
&mut self,
|
||||||
|
prefix: &[char],
|
||||||
|
lowercase_prefix: &[char],
|
||||||
|
candidates: impl Iterator<Item = C>,
|
||||||
|
results: &mut Vec<R>,
|
||||||
|
cancel_flag: &AtomicBool,
|
||||||
|
build_match: F,
|
||||||
|
) where
|
||||||
|
R: Match,
|
||||||
|
F: Fn(&C, f64) -> R,
|
||||||
|
{
|
||||||
|
let mut candidate_chars = Vec::new();
|
||||||
|
let mut lowercase_candidate_chars = Vec::new();
|
||||||
|
|
||||||
|
for candidate in candidates {
|
||||||
|
if !candidate.has_chars(self.query_char_bag) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if cancel_flag.load(atomic::Ordering::Relaxed) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
candidate_chars.clear();
|
||||||
|
lowercase_candidate_chars.clear();
|
||||||
|
for c in candidate.to_string().chars() {
|
||||||
|
candidate_chars.push(c);
|
||||||
|
lowercase_candidate_chars.push(c.to_ascii_lowercase());
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.find_last_positions(lowercase_prefix, &lowercase_candidate_chars) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let matrix_len = self.query.len() * (prefix.len() + candidate_chars.len());
|
||||||
|
self.score_matrix.clear();
|
||||||
|
self.score_matrix.resize(matrix_len, None);
|
||||||
|
self.best_position_matrix.clear();
|
||||||
|
self.best_position_matrix.resize(matrix_len, 0);
|
||||||
|
|
||||||
|
let score = self.score_match(
|
||||||
|
&candidate_chars,
|
||||||
|
&lowercase_candidate_chars,
|
||||||
|
prefix,
|
||||||
|
lowercase_prefix,
|
||||||
|
);
|
||||||
|
|
||||||
|
if score > 0.0 {
|
||||||
|
let mut mat = build_match(&candidate, score);
|
||||||
|
if let Err(i) = results.binary_search_by(|m| mat.cmp(m)) {
|
||||||
|
if results.len() < self.max_results {
|
||||||
|
mat.set_positions(self.match_positions.clone());
|
||||||
|
results.insert(i, mat);
|
||||||
|
} else if i < results.len() {
|
||||||
|
results.pop();
|
||||||
|
mat.set_positions(self.match_positions.clone());
|
||||||
|
results.insert(i, mat);
|
||||||
|
}
|
||||||
|
if results.len() == self.max_results {
|
||||||
|
self.min_score = results.last().unwrap().score();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_last_positions(
|
||||||
|
&mut self,
|
||||||
|
lowercase_prefix: &[char],
|
||||||
|
lowercase_candidate: &[char],
|
||||||
|
) -> bool {
|
||||||
|
let mut lowercase_prefix = lowercase_prefix.iter();
|
||||||
|
let mut lowercase_candidate = lowercase_candidate.iter();
|
||||||
|
for (i, char) in self.lowercase_query.iter().enumerate().rev() {
|
||||||
|
if let Some(j) = lowercase_candidate.rposition(|c| c == char) {
|
||||||
|
self.last_positions[i] = j + lowercase_prefix.len();
|
||||||
|
} else if let Some(j) = lowercase_prefix.rposition(|c| c == char) {
|
||||||
|
self.last_positions[i] = j;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn score_match(
|
||||||
|
&mut self,
|
||||||
|
path: &[char],
|
||||||
|
path_cased: &[char],
|
||||||
|
prefix: &[char],
|
||||||
|
lowercase_prefix: &[char],
|
||||||
|
) -> f64 {
|
||||||
|
let score = self.recursive_score_match(
|
||||||
|
path,
|
||||||
|
path_cased,
|
||||||
|
prefix,
|
||||||
|
lowercase_prefix,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
self.query.len() as f64,
|
||||||
|
) * self.query.len() as f64;
|
||||||
|
|
||||||
|
if score <= 0.0 {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let path_len = prefix.len() + path.len();
|
||||||
|
let mut cur_start = 0;
|
||||||
|
let mut byte_ix = 0;
|
||||||
|
let mut char_ix = 0;
|
||||||
|
for i in 0..self.query.len() {
|
||||||
|
let match_char_ix = self.best_position_matrix[i * path_len + cur_start];
|
||||||
|
while char_ix < match_char_ix {
|
||||||
|
let ch = prefix
|
||||||
|
.get(char_ix)
|
||||||
|
.or_else(|| path.get(char_ix - prefix.len()))
|
||||||
|
.unwrap();
|
||||||
|
byte_ix += ch.len_utf8();
|
||||||
|
char_ix += 1;
|
||||||
|
}
|
||||||
|
cur_start = match_char_ix + 1;
|
||||||
|
self.match_positions[i] = byte_ix;
|
||||||
|
}
|
||||||
|
|
||||||
|
score
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn recursive_score_match(
|
||||||
|
&mut self,
|
||||||
|
path: &[char],
|
||||||
|
path_cased: &[char],
|
||||||
|
prefix: &[char],
|
||||||
|
lowercase_prefix: &[char],
|
||||||
|
query_idx: usize,
|
||||||
|
path_idx: usize,
|
||||||
|
cur_score: f64,
|
||||||
|
) -> f64 {
|
||||||
|
if query_idx == self.query.len() {
|
||||||
|
return 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let path_len = prefix.len() + path.len();
|
||||||
|
|
||||||
|
if let Some(memoized) = self.score_matrix[query_idx * path_len + path_idx] {
|
||||||
|
return memoized;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut score = 0.0;
|
||||||
|
let mut best_position = 0;
|
||||||
|
|
||||||
|
let query_char = self.lowercase_query[query_idx];
|
||||||
|
let limit = self.last_positions[query_idx];
|
||||||
|
|
||||||
|
let mut last_slash = 0;
|
||||||
|
for j in path_idx..=limit {
|
||||||
|
let path_char = if j < prefix.len() {
|
||||||
|
lowercase_prefix[j]
|
||||||
|
} else {
|
||||||
|
path_cased[j - prefix.len()]
|
||||||
|
};
|
||||||
|
let is_path_sep = path_char == '/' || path_char == '\\';
|
||||||
|
|
||||||
|
if query_idx == 0 && is_path_sep {
|
||||||
|
last_slash = j;
|
||||||
|
}
|
||||||
|
|
||||||
|
if query_char == path_char || (is_path_sep && query_char == '_' || query_char == '\\') {
|
||||||
|
let curr = if j < prefix.len() {
|
||||||
|
prefix[j]
|
||||||
|
} else {
|
||||||
|
path[j - prefix.len()]
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut char_score = 1.0;
|
||||||
|
if j > path_idx {
|
||||||
|
let last = if j - 1 < prefix.len() {
|
||||||
|
prefix[j - 1]
|
||||||
|
} else {
|
||||||
|
path[j - 1 - prefix.len()]
|
||||||
|
};
|
||||||
|
|
||||||
|
if last == '/' {
|
||||||
|
char_score = 0.9;
|
||||||
|
} else if (last == '-' || last == '_' || last == ' ' || last.is_numeric())
|
||||||
|
|| (last.is_lowercase() && curr.is_uppercase())
|
||||||
|
{
|
||||||
|
char_score = 0.8;
|
||||||
|
} else if last == '.' {
|
||||||
|
char_score = 0.7;
|
||||||
|
} else if query_idx == 0 {
|
||||||
|
char_score = BASE_DISTANCE_PENALTY;
|
||||||
|
} else {
|
||||||
|
char_score = MIN_DISTANCE_PENALTY.max(
|
||||||
|
BASE_DISTANCE_PENALTY
|
||||||
|
- (j - path_idx - 1) as f64 * ADDITIONAL_DISTANCE_PENALTY,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply a severe penalty if the case doesn't match.
|
||||||
|
// This will make the exact matches have higher score than the case-insensitive and the
|
||||||
|
// path insensitive matches.
|
||||||
|
if (self.smart_case || curr == '/') && self.query[query_idx] != curr {
|
||||||
|
char_score *= 0.001;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut multiplier = char_score;
|
||||||
|
|
||||||
|
// Scale the score based on how deep within the path we found the match.
|
||||||
|
if query_idx == 0 {
|
||||||
|
multiplier /= ((prefix.len() + path.len()) - last_slash) as f64;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut next_score = 1.0;
|
||||||
|
if self.min_score > 0.0 {
|
||||||
|
next_score = cur_score * multiplier;
|
||||||
|
// Scores only decrease. If we can't pass the previous best, bail
|
||||||
|
if next_score < self.min_score {
|
||||||
|
// Ensure that score is non-zero so we use it in the memo table.
|
||||||
|
if score == 0.0 {
|
||||||
|
score = 1e-18;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let new_score = self.recursive_score_match(
|
||||||
|
path,
|
||||||
|
path_cased,
|
||||||
|
prefix,
|
||||||
|
lowercase_prefix,
|
||||||
|
query_idx + 1,
|
||||||
|
j + 1,
|
||||||
|
next_score,
|
||||||
|
) * multiplier;
|
||||||
|
|
||||||
|
if new_score > score {
|
||||||
|
score = new_score;
|
||||||
|
best_position = j;
|
||||||
|
// Optimization: can't score better than 1.
|
||||||
|
if new_score == 1.0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if best_position != 0 {
|
||||||
|
self.best_position_matrix[query_idx * path_len + path_idx] = best_position;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.score_matrix[query_idx * path_len + path_idx] = Some(score);
|
||||||
|
score
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::{PathMatch, PathMatchCandidate};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use std::{
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_last_positions() {
|
||||||
|
let mut query: &[char] = &['d', 'c'];
|
||||||
|
let mut matcher = Matcher::new(query, query, query.into(), false, 10);
|
||||||
|
let result = matcher.find_last_positions(&['a', 'b', 'c'], &['b', 'd', 'e', 'f']);
|
||||||
|
assert!(!result);
|
||||||
|
|
||||||
|
query = &['c', 'd'];
|
||||||
|
let mut matcher = Matcher::new(query, query, query.into(), false, 10);
|
||||||
|
let result = matcher.find_last_positions(&['a', 'b', 'c'], &['b', 'd', 'e', 'f']);
|
||||||
|
assert!(result);
|
||||||
|
assert_eq!(matcher.last_positions, vec![2, 4]);
|
||||||
|
|
||||||
|
query = &['z', '/', 'z', 'f'];
|
||||||
|
let mut matcher = Matcher::new(query, query, query.into(), false, 10);
|
||||||
|
let result = matcher.find_last_positions(&['z', 'e', 'd', '/'], &['z', 'e', 'd', '/', 'f']);
|
||||||
|
assert!(result);
|
||||||
|
assert_eq!(matcher.last_positions, vec![0, 3, 4, 8]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_match_path_entries() {
|
||||||
|
let paths = vec![
|
||||||
|
"",
|
||||||
|
"a",
|
||||||
|
"ab",
|
||||||
|
"abC",
|
||||||
|
"abcd",
|
||||||
|
"alphabravocharlie",
|
||||||
|
"AlphaBravoCharlie",
|
||||||
|
"thisisatestdir",
|
||||||
|
"/////ThisIsATestDir",
|
||||||
|
"/this/is/a/test/dir",
|
||||||
|
"/test/tiatd",
|
||||||
|
];
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
match_single_path_query("abc", false, &paths),
|
||||||
|
vec![
|
||||||
|
("abC", vec![0, 1, 2]),
|
||||||
|
("abcd", vec![0, 1, 2]),
|
||||||
|
("AlphaBravoCharlie", vec![0, 5, 10]),
|
||||||
|
("alphabravocharlie", vec![4, 5, 10]),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
match_single_path_query("t/i/a/t/d", false, &paths),
|
||||||
|
vec![("/this/is/a/test/dir", vec![1, 5, 6, 8, 9, 10, 11, 15, 16]),]
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
match_single_path_query("tiatd", false, &paths),
|
||||||
|
vec![
|
||||||
|
("/test/tiatd", vec![6, 7, 8, 9, 10]),
|
||||||
|
("/this/is/a/test/dir", vec![1, 6, 9, 11, 16]),
|
||||||
|
("/////ThisIsATestDir", vec![5, 9, 11, 12, 16]),
|
||||||
|
("thisisatestdir", vec![0, 2, 6, 7, 11]),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_match_multibyte_path_entries() {
|
||||||
|
let paths = vec!["aαbβ/cγdδ", "αβγδ/bcde", "c1️⃣2️⃣3️⃣/d4️⃣5️⃣6️⃣/e7️⃣8️⃣9️⃣/f", "/d/🆒/h"];
|
||||||
|
assert_eq!("1️⃣".len(), 7);
|
||||||
|
assert_eq!(
|
||||||
|
match_single_path_query("bcd", false, &paths),
|
||||||
|
vec![
|
||||||
|
("αβγδ/bcde", vec![9, 10, 11]),
|
||||||
|
("aαbβ/cγdδ", vec![3, 7, 10]),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
match_single_path_query("cde", false, &paths),
|
||||||
|
vec![
|
||||||
|
("αβγδ/bcde", vec![10, 11, 12]),
|
||||||
|
("c1️⃣2️⃣3️⃣/d4️⃣5️⃣6️⃣/e7️⃣8️⃣9️⃣/f", vec![0, 23, 46]),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn match_single_path_query<'a>(
|
||||||
|
query: &str,
|
||||||
|
smart_case: bool,
|
||||||
|
paths: &[&'a str],
|
||||||
|
) -> Vec<(&'a str, Vec<usize>)> {
|
||||||
|
let lowercase_query = query.to_lowercase().chars().collect::<Vec<_>>();
|
||||||
|
let query = query.chars().collect::<Vec<_>>();
|
||||||
|
let query_chars = CharBag::from(&lowercase_query[..]);
|
||||||
|
|
||||||
|
let path_arcs: Vec<Arc<Path>> = paths
|
||||||
|
.iter()
|
||||||
|
.map(|path| Arc::from(PathBuf::from(path)))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let mut path_entries = Vec::new();
|
||||||
|
for (i, path) in paths.iter().enumerate() {
|
||||||
|
let lowercase_path = path.to_lowercase().chars().collect::<Vec<_>>();
|
||||||
|
let char_bag = CharBag::from(lowercase_path.as_slice());
|
||||||
|
path_entries.push(PathMatchCandidate {
|
||||||
|
char_bag,
|
||||||
|
path: &path_arcs[i],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut matcher = Matcher::new(&query, &lowercase_query, query_chars, smart_case, 100);
|
||||||
|
|
||||||
|
let cancel_flag = AtomicBool::new(false);
|
||||||
|
let mut results = Vec::new();
|
||||||
|
|
||||||
|
matcher.match_candidates(
|
||||||
|
&[],
|
||||||
|
&[],
|
||||||
|
path_entries.into_iter(),
|
||||||
|
&mut results,
|
||||||
|
&cancel_flag,
|
||||||
|
|candidate, score| PathMatch {
|
||||||
|
score,
|
||||||
|
worktree_id: 0,
|
||||||
|
positions: Vec::new(),
|
||||||
|
path: candidate.path.clone(),
|
||||||
|
path_prefix: "".into(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
results
|
||||||
|
.into_iter()
|
||||||
|
.map(|result| {
|
||||||
|
(
|
||||||
|
paths
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.find(|p| result.path.as_ref() == Path::new(p))
|
||||||
|
.unwrap(),
|
||||||
|
result.positions,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
174
crates/fuzzy/src/paths.rs
Normal file
174
crates/fuzzy/src/paths.rs
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
use std::{
|
||||||
|
borrow::Cow,
|
||||||
|
cmp::{self, Ordering},
|
||||||
|
path::Path,
|
||||||
|
sync::{atomic::AtomicBool, Arc},
|
||||||
|
};
|
||||||
|
|
||||||
|
use gpui::executor;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
matcher::{Match, MatchCandidate, Matcher},
|
||||||
|
CharBag,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct PathMatchCandidate<'a> {
|
||||||
|
pub path: &'a Arc<Path>,
|
||||||
|
pub char_bag: CharBag,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct PathMatch {
|
||||||
|
pub score: f64,
|
||||||
|
pub positions: Vec<usize>,
|
||||||
|
pub worktree_id: usize,
|
||||||
|
pub path: Arc<Path>,
|
||||||
|
pub path_prefix: Arc<str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait PathMatchCandidateSet<'a>: Send + Sync {
|
||||||
|
type Candidates: Iterator<Item = PathMatchCandidate<'a>>;
|
||||||
|
fn id(&self) -> usize;
|
||||||
|
fn len(&self) -> usize;
|
||||||
|
fn is_empty(&self) -> bool {
|
||||||
|
self.len() == 0
|
||||||
|
}
|
||||||
|
fn prefix(&self) -> Arc<str>;
|
||||||
|
fn candidates(&'a self, start: usize) -> Self::Candidates;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Match for PathMatch {
|
||||||
|
fn score(&self) -> f64 {
|
||||||
|
self.score
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_positions(&mut self, positions: Vec<usize>) {
|
||||||
|
self.positions = positions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> MatchCandidate for PathMatchCandidate<'a> {
|
||||||
|
fn has_chars(&self, bag: CharBag) -> bool {
|
||||||
|
self.char_bag.is_superset(bag)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_string(&self) -> Cow<'a, str> {
|
||||||
|
self.path.to_string_lossy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for PathMatch {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.cmp(other).is_eq()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for PathMatch {}
|
||||||
|
|
||||||
|
impl PartialOrd for PathMatch {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||||
|
Some(self.cmp(other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ord for PathMatch {
|
||||||
|
fn cmp(&self, other: &Self) -> Ordering {
|
||||||
|
self.score
|
||||||
|
.partial_cmp(&other.score)
|
||||||
|
.unwrap_or(Ordering::Equal)
|
||||||
|
.then_with(|| self.worktree_id.cmp(&other.worktree_id))
|
||||||
|
.then_with(|| self.path.cmp(&other.path))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn match_path_sets<'a, Set: PathMatchCandidateSet<'a>>(
|
||||||
|
candidate_sets: &'a [Set],
|
||||||
|
query: &str,
|
||||||
|
smart_case: bool,
|
||||||
|
max_results: usize,
|
||||||
|
cancel_flag: &AtomicBool,
|
||||||
|
background: Arc<executor::Background>,
|
||||||
|
) -> Vec<PathMatch> {
|
||||||
|
let path_count: usize = candidate_sets.iter().map(|s| s.len()).sum();
|
||||||
|
if path_count == 0 {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
let lowercase_query = query.to_lowercase().chars().collect::<Vec<_>>();
|
||||||
|
let query = query.chars().collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let lowercase_query = &lowercase_query;
|
||||||
|
let query = &query;
|
||||||
|
let query_char_bag = CharBag::from(&lowercase_query[..]);
|
||||||
|
|
||||||
|
let num_cpus = background.num_cpus().min(path_count);
|
||||||
|
let segment_size = (path_count + num_cpus - 1) / num_cpus;
|
||||||
|
let mut segment_results = (0..num_cpus)
|
||||||
|
.map(|_| Vec::with_capacity(max_results))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
background
|
||||||
|
.scoped(|scope| {
|
||||||
|
for (segment_idx, results) in segment_results.iter_mut().enumerate() {
|
||||||
|
scope.spawn(async move {
|
||||||
|
let segment_start = segment_idx * segment_size;
|
||||||
|
let segment_end = segment_start + segment_size;
|
||||||
|
let mut matcher = Matcher::new(
|
||||||
|
query,
|
||||||
|
lowercase_query,
|
||||||
|
query_char_bag,
|
||||||
|
smart_case,
|
||||||
|
max_results,
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut tree_start = 0;
|
||||||
|
for candidate_set in candidate_sets {
|
||||||
|
let tree_end = tree_start + candidate_set.len();
|
||||||
|
|
||||||
|
if tree_start < segment_end && segment_start < tree_end {
|
||||||
|
let start = cmp::max(tree_start, segment_start) - tree_start;
|
||||||
|
let end = cmp::min(tree_end, segment_end) - tree_start;
|
||||||
|
let candidates = candidate_set.candidates(start).take(end - start);
|
||||||
|
|
||||||
|
let worktree_id = candidate_set.id();
|
||||||
|
let prefix = candidate_set.prefix().chars().collect::<Vec<_>>();
|
||||||
|
let lowercase_prefix = prefix
|
||||||
|
.iter()
|
||||||
|
.map(|c| c.to_ascii_lowercase())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
matcher.match_candidates(
|
||||||
|
&prefix,
|
||||||
|
&lowercase_prefix,
|
||||||
|
candidates,
|
||||||
|
results,
|
||||||
|
cancel_flag,
|
||||||
|
|candidate, score| PathMatch {
|
||||||
|
score,
|
||||||
|
worktree_id,
|
||||||
|
positions: Vec::new(),
|
||||||
|
path: candidate.path.clone(),
|
||||||
|
path_prefix: candidate_set.prefix(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if tree_end >= segment_end {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
tree_start = tree_end;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let mut results = Vec::new();
|
||||||
|
for segment_result in segment_results {
|
||||||
|
if results.is_empty() {
|
||||||
|
results = segment_result;
|
||||||
|
} else {
|
||||||
|
util::extend_sorted(&mut results, segment_result, max_results, |a, b| b.cmp(a));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
results
|
||||||
|
}
|
161
crates/fuzzy/src/strings.rs
Normal file
161
crates/fuzzy/src/strings.rs
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
use std::{
|
||||||
|
borrow::Cow,
|
||||||
|
cmp::{self, Ordering},
|
||||||
|
sync::{atomic::AtomicBool, Arc},
|
||||||
|
};
|
||||||
|
|
||||||
|
use gpui::executor;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
matcher::{Match, MatchCandidate, Matcher},
|
||||||
|
CharBag,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct StringMatchCandidate {
|
||||||
|
pub id: usize,
|
||||||
|
pub string: String,
|
||||||
|
pub char_bag: CharBag,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Match for StringMatch {
|
||||||
|
fn score(&self) -> f64 {
|
||||||
|
self.score
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_positions(&mut self, positions: Vec<usize>) {
|
||||||
|
self.positions = positions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StringMatchCandidate {
|
||||||
|
pub fn new(id: usize, string: String) -> Self {
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
char_bag: CharBag::from(string.as_str()),
|
||||||
|
string,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> MatchCandidate for &'a StringMatchCandidate {
|
||||||
|
fn has_chars(&self, bag: CharBag) -> bool {
|
||||||
|
self.char_bag.is_superset(bag)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_string(&self) -> Cow<'a, str> {
|
||||||
|
self.string.as_str().into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct StringMatch {
|
||||||
|
pub candidate_id: usize,
|
||||||
|
pub score: f64,
|
||||||
|
pub positions: Vec<usize>,
|
||||||
|
pub string: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for StringMatch {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.cmp(other).is_eq()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for StringMatch {}
|
||||||
|
|
||||||
|
impl PartialOrd for StringMatch {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||||
|
Some(self.cmp(other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ord for StringMatch {
|
||||||
|
fn cmp(&self, other: &Self) -> Ordering {
|
||||||
|
self.score
|
||||||
|
.partial_cmp(&other.score)
|
||||||
|
.unwrap_or(Ordering::Equal)
|
||||||
|
.then_with(|| self.candidate_id.cmp(&other.candidate_id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn match_strings(
|
||||||
|
candidates: &[StringMatchCandidate],
|
||||||
|
query: &str,
|
||||||
|
smart_case: bool,
|
||||||
|
max_results: usize,
|
||||||
|
cancel_flag: &AtomicBool,
|
||||||
|
background: Arc<executor::Background>,
|
||||||
|
) -> Vec<StringMatch> {
|
||||||
|
if candidates.is_empty() || max_results == 0 {
|
||||||
|
return Default::default();
|
||||||
|
}
|
||||||
|
|
||||||
|
if query.is_empty() {
|
||||||
|
return candidates
|
||||||
|
.iter()
|
||||||
|
.map(|candidate| StringMatch {
|
||||||
|
candidate_id: candidate.id,
|
||||||
|
score: 0.,
|
||||||
|
positions: Default::default(),
|
||||||
|
string: candidate.string.clone(),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
let lowercase_query = query.to_lowercase().chars().collect::<Vec<_>>();
|
||||||
|
let query = query.chars().collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let lowercase_query = &lowercase_query;
|
||||||
|
let query = &query;
|
||||||
|
let query_char_bag = CharBag::from(&lowercase_query[..]);
|
||||||
|
|
||||||
|
let num_cpus = background.num_cpus().min(candidates.len());
|
||||||
|
let segment_size = (candidates.len() + num_cpus - 1) / num_cpus;
|
||||||
|
let mut segment_results = (0..num_cpus)
|
||||||
|
.map(|_| Vec::with_capacity(max_results.min(candidates.len())))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
background
|
||||||
|
.scoped(|scope| {
|
||||||
|
for (segment_idx, results) in segment_results.iter_mut().enumerate() {
|
||||||
|
let cancel_flag = &cancel_flag;
|
||||||
|
scope.spawn(async move {
|
||||||
|
let segment_start = cmp::min(segment_idx * segment_size, candidates.len());
|
||||||
|
let segment_end = cmp::min(segment_start + segment_size, candidates.len());
|
||||||
|
let mut matcher = Matcher::new(
|
||||||
|
query,
|
||||||
|
lowercase_query,
|
||||||
|
query_char_bag,
|
||||||
|
smart_case,
|
||||||
|
max_results,
|
||||||
|
);
|
||||||
|
|
||||||
|
matcher.match_candidates(
|
||||||
|
&[],
|
||||||
|
&[],
|
||||||
|
candidates[segment_start..segment_end].iter(),
|
||||||
|
results,
|
||||||
|
cancel_flag,
|
||||||
|
|candidate, score| StringMatch {
|
||||||
|
candidate_id: candidate.id,
|
||||||
|
score,
|
||||||
|
positions: Vec::new(),
|
||||||
|
string: candidate.string.to_string(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let mut results = Vec::new();
|
||||||
|
for segment_result in segment_results {
|
||||||
|
if results.is_empty() {
|
||||||
|
results = segment_result;
|
||||||
|
} else {
|
||||||
|
util::extend_sorted(&mut results, segment_result, max_results, |a, b| b.cmp(a));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
results
|
||||||
|
}
|
@ -84,13 +84,13 @@ impl OutlineView {
|
|||||||
.active_item(cx)
|
.active_item(cx)
|
||||||
.and_then(|item| item.downcast::<Editor>())
|
.and_then(|item| item.downcast::<Editor>())
|
||||||
{
|
{
|
||||||
let buffer = editor
|
let outline = editor
|
||||||
.read(cx)
|
.read(cx)
|
||||||
.buffer()
|
.buffer()
|
||||||
.read(cx)
|
.read(cx)
|
||||||
.snapshot(cx)
|
.snapshot(cx)
|
||||||
.outline(Some(cx.global::<Settings>().theme.editor.syntax.as_ref()));
|
.outline(Some(cx.global::<Settings>().theme.editor.syntax.as_ref()));
|
||||||
if let Some(outline) = buffer {
|
if let Some(outline) = outline {
|
||||||
workspace.toggle_modal(cx, |_, cx| {
|
workspace.toggle_modal(cx, |_, cx| {
|
||||||
let view = cx.add_view(|cx| OutlineView::new(outline, editor, cx));
|
let view = cx.add_view(|cx| OutlineView::new(outline, editor, cx));
|
||||||
cx.subscribe(&view, Self::on_event).detach();
|
cx.subscribe(&view, Self::on_event).detach();
|
||||||
|
22
crates/recent_projects/Cargo.toml
Normal file
22
crates/recent_projects/Cargo.toml
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
[package]
|
||||||
|
name = "recent_projects"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/recent_projects.rs"
|
||||||
|
doctest = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
db = { path = "../db" }
|
||||||
|
editor = { path = "../editor" }
|
||||||
|
fuzzy = { path = "../fuzzy" }
|
||||||
|
gpui = { path = "../gpui" }
|
||||||
|
language = { path = "../language" }
|
||||||
|
picker = { path = "../picker" }
|
||||||
|
settings = { path = "../settings" }
|
||||||
|
text = { path = "../text" }
|
||||||
|
workspace = { path = "../workspace" }
|
||||||
|
ordered-float = "2.1.1"
|
||||||
|
postage = { version = "0.4", features = ["futures-traits"] }
|
||||||
|
smol = "1.2"
|
129
crates/recent_projects/src/highlighted_workspace_location.rs
Normal file
129
crates/recent_projects/src/highlighted_workspace_location.rs
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use fuzzy::StringMatch;
|
||||||
|
use gpui::{
|
||||||
|
elements::{Label, LabelStyle},
|
||||||
|
Element, ElementBox,
|
||||||
|
};
|
||||||
|
use workspace::WorkspaceLocation;
|
||||||
|
|
||||||
|
pub struct HighlightedText {
|
||||||
|
pub text: String,
|
||||||
|
pub highlight_positions: Vec<usize>,
|
||||||
|
char_count: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HighlightedText {
|
||||||
|
fn join(components: impl Iterator<Item = Self>, separator: &str) -> Self {
|
||||||
|
let mut char_count = 0;
|
||||||
|
let separator_char_count = separator.chars().count();
|
||||||
|
let mut text = String::new();
|
||||||
|
let mut highlight_positions = Vec::new();
|
||||||
|
for component in components {
|
||||||
|
if char_count != 0 {
|
||||||
|
text.push_str(separator);
|
||||||
|
char_count += separator_char_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
highlight_positions.extend(
|
||||||
|
component
|
||||||
|
.highlight_positions
|
||||||
|
.iter()
|
||||||
|
.map(|position| position + char_count),
|
||||||
|
);
|
||||||
|
text.push_str(&component.text);
|
||||||
|
char_count += component.text.chars().count();
|
||||||
|
}
|
||||||
|
|
||||||
|
Self {
|
||||||
|
text,
|
||||||
|
highlight_positions,
|
||||||
|
char_count,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render(self, style: impl Into<LabelStyle>) -> ElementBox {
|
||||||
|
Label::new(self.text, style)
|
||||||
|
.with_highlights(self.highlight_positions)
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct HighlightedWorkspaceLocation {
|
||||||
|
pub names: HighlightedText,
|
||||||
|
pub paths: Vec<HighlightedText>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HighlightedWorkspaceLocation {
|
||||||
|
pub fn new(string_match: &StringMatch, location: &WorkspaceLocation) -> Self {
|
||||||
|
let mut path_start_offset = 0;
|
||||||
|
let (names, paths): (Vec<_>, Vec<_>) = location
|
||||||
|
.paths()
|
||||||
|
.iter()
|
||||||
|
.map(|path| {
|
||||||
|
let highlighted_text = Self::highlights_for_path(
|
||||||
|
path.as_ref(),
|
||||||
|
&string_match.positions,
|
||||||
|
path_start_offset,
|
||||||
|
);
|
||||||
|
|
||||||
|
path_start_offset += highlighted_text.1.char_count;
|
||||||
|
|
||||||
|
highlighted_text
|
||||||
|
})
|
||||||
|
.unzip();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
names: HighlightedText::join(names.into_iter().filter_map(|name| name), ", "),
|
||||||
|
paths,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute the highlighted text for the name and path
|
||||||
|
fn highlights_for_path(
|
||||||
|
path: &Path,
|
||||||
|
match_positions: &Vec<usize>,
|
||||||
|
path_start_offset: usize,
|
||||||
|
) -> (Option<HighlightedText>, HighlightedText) {
|
||||||
|
let path_string = path.to_string_lossy();
|
||||||
|
let path_char_count = path_string.chars().count();
|
||||||
|
// Get the subset of match highlight positions that line up with the given path.
|
||||||
|
// Also adjusts them to start at the path start
|
||||||
|
let path_positions = match_positions
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.skip_while(|position| *position < path_start_offset)
|
||||||
|
.take_while(|position| *position < path_start_offset + path_char_count)
|
||||||
|
.map(|position| position - path_start_offset)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
// Again subset the highlight positions to just those that line up with the file_name
|
||||||
|
// again adjusted to the start of the file_name
|
||||||
|
let file_name_text_and_positions = path.file_name().map(|file_name| {
|
||||||
|
let text = file_name.to_string_lossy();
|
||||||
|
let char_count = text.chars().count();
|
||||||
|
let file_name_start = path_char_count - char_count;
|
||||||
|
let highlight_positions = path_positions
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.skip_while(|position| *position < file_name_start)
|
||||||
|
.take_while(|position| *position < file_name_start + char_count)
|
||||||
|
.map(|position| position - file_name_start)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
HighlightedText {
|
||||||
|
text: text.to_string(),
|
||||||
|
highlight_positions,
|
||||||
|
char_count,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
(
|
||||||
|
file_name_text_and_positions,
|
||||||
|
HighlightedText {
|
||||||
|
text: path_string.to_string(),
|
||||||
|
highlight_positions: path_positions,
|
||||||
|
char_count: path_char_count,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
187
crates/recent_projects/src/recent_projects.rs
Normal file
187
crates/recent_projects/src/recent_projects.rs
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
mod highlighted_workspace_location;
|
||||||
|
|
||||||
|
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||||
|
use gpui::{
|
||||||
|
actions,
|
||||||
|
elements::{ChildView, Flex, ParentElement},
|
||||||
|
AnyViewHandle, Element, ElementBox, Entity, MutableAppContext, RenderContext, Task, View,
|
||||||
|
ViewContext, ViewHandle,
|
||||||
|
};
|
||||||
|
use highlighted_workspace_location::HighlightedWorkspaceLocation;
|
||||||
|
use ordered_float::OrderedFloat;
|
||||||
|
use picker::{Picker, PickerDelegate};
|
||||||
|
use settings::Settings;
|
||||||
|
use workspace::{OpenPaths, Workspace, WorkspaceLocation};
|
||||||
|
|
||||||
|
const RECENT_LIMIT: usize = 100;
|
||||||
|
|
||||||
|
actions!(recent_projects, [Toggle]);
|
||||||
|
|
||||||
|
pub fn init(cx: &mut MutableAppContext) {
|
||||||
|
cx.add_action(RecentProjectsView::toggle);
|
||||||
|
Picker::<RecentProjectsView>::init(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RecentProjectsView {
|
||||||
|
picker: ViewHandle<Picker<Self>>,
|
||||||
|
workspace_locations: Vec<WorkspaceLocation>,
|
||||||
|
selected_match_index: usize,
|
||||||
|
matches: Vec<StringMatch>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RecentProjectsView {
|
||||||
|
fn new(cx: &mut ViewContext<Self>) -> Self {
|
||||||
|
let handle = cx.weak_handle();
|
||||||
|
let workspace_locations: Vec<WorkspaceLocation> = workspace::WORKSPACE_DB
|
||||||
|
.recent_workspaces(RECENT_LIMIT)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.into_iter()
|
||||||
|
.map(|(_, location)| location)
|
||||||
|
.collect();
|
||||||
|
Self {
|
||||||
|
picker: cx.add_view(|cx| {
|
||||||
|
Picker::new("Recent Projects...", handle, cx).with_max_size(800., 1200.)
|
||||||
|
}),
|
||||||
|
workspace_locations,
|
||||||
|
selected_match_index: 0,
|
||||||
|
matches: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
|
||||||
|
workspace.toggle_modal(cx, |_, cx| {
|
||||||
|
let view = cx.add_view(|cx| Self::new(cx));
|
||||||
|
cx.subscribe(&view, Self::on_event).detach();
|
||||||
|
view
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_event(
|
||||||
|
workspace: &mut Workspace,
|
||||||
|
_: ViewHandle<Self>,
|
||||||
|
event: &Event,
|
||||||
|
cx: &mut ViewContext<Workspace>,
|
||||||
|
) {
|
||||||
|
match event {
|
||||||
|
Event::Dismissed => workspace.dismiss_modal(cx),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Event {
|
||||||
|
Dismissed,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Entity for RecentProjectsView {
|
||||||
|
type Event = Event;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl View for RecentProjectsView {
|
||||||
|
fn ui_name() -> &'static str {
|
||||||
|
"RecentProjectsView"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||||
|
ChildView::new(self.picker.clone(), cx).boxed()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||||
|
if cx.is_self_focused() {
|
||||||
|
cx.focus(&self.picker);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PickerDelegate for RecentProjectsView {
|
||||||
|
fn match_count(&self) -> usize {
|
||||||
|
self.matches.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selected_index(&self) -> usize {
|
||||||
|
self.selected_match_index
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<Self>) {
|
||||||
|
self.selected_match_index = ix;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) -> gpui::Task<()> {
|
||||||
|
let query = query.trim_start();
|
||||||
|
let smart_case = query.chars().any(|c| c.is_uppercase());
|
||||||
|
let candidates = self
|
||||||
|
.workspace_locations
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(id, location)| {
|
||||||
|
let combined_string = location
|
||||||
|
.paths()
|
||||||
|
.iter()
|
||||||
|
.map(|path| path.to_string_lossy().to_owned())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("");
|
||||||
|
StringMatchCandidate::new(id, combined_string)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
self.matches = smol::block_on(fuzzy::match_strings(
|
||||||
|
candidates.as_slice(),
|
||||||
|
query,
|
||||||
|
smart_case,
|
||||||
|
100,
|
||||||
|
&Default::default(),
|
||||||
|
cx.background().clone(),
|
||||||
|
));
|
||||||
|
self.matches.sort_unstable_by_key(|m| m.candidate_id);
|
||||||
|
|
||||||
|
self.selected_match_index = self
|
||||||
|
.matches
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.max_by_key(|(_, m)| OrderedFloat(m.score))
|
||||||
|
.map(|(ix, _)| ix)
|
||||||
|
.unwrap_or(0);
|
||||||
|
Task::ready(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn confirm(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
|
let selected_match = &self.matches[self.selected_index()];
|
||||||
|
let workspace_location = &self.workspace_locations[selected_match.candidate_id];
|
||||||
|
cx.dispatch_global_action(OpenPaths {
|
||||||
|
paths: workspace_location.paths().as_ref().clone(),
|
||||||
|
});
|
||||||
|
cx.emit(Event::Dismissed);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
|
cx.emit(Event::Dismissed);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_match(
|
||||||
|
&self,
|
||||||
|
ix: usize,
|
||||||
|
mouse_state: &mut gpui::MouseState,
|
||||||
|
selected: bool,
|
||||||
|
cx: &gpui::AppContext,
|
||||||
|
) -> ElementBox {
|
||||||
|
let settings = cx.global::<Settings>();
|
||||||
|
let string_match = &self.matches[ix];
|
||||||
|
let style = settings.theme.picker.item.style_for(mouse_state, selected);
|
||||||
|
|
||||||
|
let highlighted_location = HighlightedWorkspaceLocation::new(
|
||||||
|
&string_match,
|
||||||
|
&self.workspace_locations[string_match.candidate_id],
|
||||||
|
);
|
||||||
|
|
||||||
|
Flex::column()
|
||||||
|
.with_child(highlighted_location.names.render(style.label.clone()))
|
||||||
|
.with_children(
|
||||||
|
highlighted_location
|
||||||
|
.paths
|
||||||
|
.into_iter()
|
||||||
|
.map(|highlighted_path| highlighted_path.render(style.label.clone())),
|
||||||
|
)
|
||||||
|
.flex(1., false)
|
||||||
|
.contained()
|
||||||
|
.with_style(style.container)
|
||||||
|
.named("match")
|
||||||
|
}
|
||||||
|
}
|
@ -50,7 +50,7 @@ pub use pane_group::*;
|
|||||||
use persistence::{model::SerializedItem, DB};
|
use persistence::{model::SerializedItem, DB};
|
||||||
pub use persistence::{
|
pub use persistence::{
|
||||||
model::{ItemId, WorkspaceLocation},
|
model::{ItemId, WorkspaceLocation},
|
||||||
WorkspaceDb,
|
WorkspaceDb, DB as WORKSPACE_DB,
|
||||||
};
|
};
|
||||||
use postage::prelude::Stream;
|
use postage::prelude::Stream;
|
||||||
use project::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
|
use project::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
|
||||||
|
@ -44,6 +44,7 @@ plugin_runtime = { path = "../plugin_runtime" }
|
|||||||
project = { path = "../project" }
|
project = { path = "../project" }
|
||||||
project_panel = { path = "../project_panel" }
|
project_panel = { path = "../project_panel" }
|
||||||
project_symbols = { path = "../project_symbols" }
|
project_symbols = { path = "../project_symbols" }
|
||||||
|
recent_projects = { path = "../recent_projects" }
|
||||||
rpc = { path = "../rpc" }
|
rpc = { path = "../rpc" }
|
||||||
settings = { path = "../settings" }
|
settings = { path = "../settings" }
|
||||||
sum_tree = { path = "../sum_tree" }
|
sum_tree = { path = "../sum_tree" }
|
||||||
|
@ -121,6 +121,7 @@ fn main() {
|
|||||||
vim::init(cx);
|
vim::init(cx);
|
||||||
terminal::init(cx);
|
terminal::init(cx);
|
||||||
theme_testbench::init(cx);
|
theme_testbench::init(cx);
|
||||||
|
recent_projects::init(cx);
|
||||||
|
|
||||||
cx.spawn(|cx| watch_themes(fs.clone(), themes.clone(), cx))
|
cx.spawn(|cx| watch_themes(fs.clone(), themes.clone(), cx))
|
||||||
.detach();
|
.detach();
|
||||||
|
Loading…
Reference in New Issue
Block a user