Move fuzzy::match_paths to Project::match_paths

This prepares the way to extract the rest of fuzzy to its own crate.

Co-Authored-By: Max Brunsfeld <maxbrunsfeld@gmail.com>
This commit is contained in:
Nathan Sobo
2021-10-01 16:44:08 -06:00
co-authored by Max Brunsfeld
parent cf270b4dff
commit 706925b781
5 changed files with 293 additions and 280 deletions
+38 -35
View File
@@ -1,9 +1,10 @@
use crate::{
editor::{self, Editor},
project::Project,
settings::Settings,
util,
workspace::Workspace,
worktree::{match_paths, PathMatch},
worktree::PathMatch,
};
use gpui::{
action,
@@ -13,8 +14,8 @@ use gpui::{
menu::{SelectNext, SelectPrev},
Binding,
},
AppContext, Axis, Entity, MutableAppContext, RenderContext, Task, View, ViewContext,
ViewHandle, WeakViewHandle,
AppContext, Axis, Entity, ModelHandle, MutableAppContext, RenderContext, Task, View,
ViewContext, ViewHandle, WeakViewHandle,
};
use postage::watch;
use std::{
@@ -29,7 +30,7 @@ use std::{
pub struct FileFinder {
handle: WeakViewHandle<Self>,
settings: watch::Receiver<Settings>,
workspace: WeakViewHandle<Workspace>,
project: ModelHandle<Project>,
query_editor: ViewHandle<Editor>,
search_count: usize,
latest_search_id: usize,
@@ -241,8 +242,8 @@ impl FileFinder {
fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
workspace.toggle_modal(cx, |cx, workspace| {
let handle = cx.handle();
let finder = cx.add_view(|cx| Self::new(workspace.settings.clone(), handle, cx));
let project = workspace.project().clone();
let finder = cx.add_view(|cx| Self::new(workspace.settings.clone(), project, cx));
cx.subscribe(&finder, Self::on_event).detach();
finder
});
@@ -269,10 +270,10 @@ impl FileFinder {
pub fn new(
settings: watch::Receiver<Settings>,
workspace: ViewHandle<Workspace>,
project: ModelHandle<Project>,
cx: &mut ViewContext<Self>,
) -> Self {
cx.observe(&workspace, Self::workspace_updated).detach();
cx.observe(&project, Self::project_updated).detach();
let query_editor = cx.add_view(|cx| {
Editor::single_line(
@@ -290,7 +291,7 @@ impl FileFinder {
Self {
handle: cx.handle().downgrade(),
settings,
workspace: workspace.downgrade(),
project,
query_editor,
search_count: 0,
latest_search_id: 0,
@@ -303,7 +304,7 @@ impl FileFinder {
}
}
fn workspace_updated(&mut self, _: ViewHandle<Workspace>, cx: &mut ViewContext<Self>) {
fn project_updated(&mut self, _: ModelHandle<Project>, cx: &mut ViewContext<Self>) {
let query = self.query_editor.update(cx, |buffer, cx| buffer.text(cx));
if let Some(task) = self.spawn_search(query, cx) {
task.detach();
@@ -381,30 +382,17 @@ impl FileFinder {
#[must_use]
fn spawn_search(&mut self, query: String, cx: &mut ViewContext<Self>) -> Option<Task<()>> {
let snapshots = self
.workspace
.upgrade(&cx)?
.read(cx)
.worktrees(cx)
.iter()
.map(|tree| tree.read(cx).snapshot())
.collect::<Vec<_>>();
let search_id = util::post_inc(&mut self.search_count);
let background = cx.as_ref().background().clone();
self.cancel_flag.store(true, atomic::Ordering::Relaxed);
self.cancel_flag = Arc::new(AtomicBool::new(false));
let cancel_flag = self.cancel_flag.clone();
let project = self.project.clone();
Some(cx.spawn(|this, mut cx| async move {
let matches = match_paths(
&snapshots,
&query,
false,
false,
100,
cancel_flag.as_ref(),
background,
)
.await;
let matches = project
.read_with(&cx, |project, cx| {
project.match_paths(&query, false, false, 100, cancel_flag.as_ref(), cx)
})
.await;
let did_cancel = cancel_flag.load(atomic::Ordering::Relaxed);
this.update(&mut cx, |this, cx| {
this.update_matches((search_id, did_cancel, query, matches), cx)
@@ -536,8 +524,13 @@ mod tests {
.unwrap();
cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
.await;
let (_, finder) =
cx.add_window(|cx| FileFinder::new(app_state.settings.clone(), workspace.clone(), cx));
let (_, finder) = cx.add_window(|cx| {
FileFinder::new(
app_state.settings.clone(),
workspace.read(cx).project().clone(),
cx,
)
});
let query = "hi".to_string();
finder
@@ -596,8 +589,13 @@ mod tests {
.unwrap();
cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
.await;
let (_, finder) =
cx.add_window(|cx| FileFinder::new(app_state.settings.clone(), workspace.clone(), cx));
let (_, finder) = cx.add_window(|cx| {
FileFinder::new(
app_state.settings.clone(),
workspace.read(cx).project().clone(),
cx,
)
});
// Even though there is only one worktree, that worktree's filename
// is included in the matching, because the worktree is a single file.
@@ -654,8 +652,13 @@ mod tests {
cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
.await;
let (_, finder) =
cx.add_window(|cx| FileFinder::new(app_state.settings.clone(), workspace.clone(), cx));
let (_, finder) = cx.add_window(|cx| {
FileFinder::new(
app_state.settings.clone(),
workspace.read(cx).project().clone(),
cx,
)
});
// Run a search that matches two files with the same relative path.
finder
+4 -112
View File
@@ -19,7 +19,7 @@ const BASE_DISTANCE_PENALTY: f64 = 0.6;
const ADDITIONAL_DISTANCE_PENALTY: f64 = 0.05;
const MIN_DISTANCE_PENALTY: f64 = 0.2;
struct Matcher<'a> {
pub struct Matcher<'a> {
query: &'a [char],
lowercase_query: &'a [char],
query_char_bag: CharBag,
@@ -213,116 +213,8 @@ pub async fn match_strings(
results
}
pub async fn match_paths(
snapshots: &[Snapshot],
query: &str,
include_ignored: bool,
smart_case: bool,
max_results: usize,
cancel_flag: &AtomicBool,
background: Arc<executor::Background>,
) -> Vec<PathMatch> {
let path_count: usize = if include_ignored {
snapshots.iter().map(Snapshot::file_count).sum()
} else {
snapshots.iter().map(Snapshot::visible_file_count).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 snapshot in snapshots {
let tree_end = if include_ignored {
tree_start + snapshot.file_count()
} else {
tree_start + snapshot.visible_file_count()
};
if tree_start < segment_end && segment_start < tree_end {
let path_prefix: Arc<str> =
if snapshot.root_entry().map_or(false, |e| e.is_file()) {
snapshot.root_name().into()
} else if snapshots.len() > 1 {
format!("{}/", snapshot.root_name()).into()
} else {
"".into()
};
let start = max(tree_start, segment_start) - tree_start;
let end = min(tree_end, segment_end) - tree_start;
let paths = snapshot
.files(include_ignored, start)
.take(end - start)
.map(|entry| {
if let EntryKind::File(char_bag) = entry.kind {
PathMatchCandidate {
path: &entry.path,
char_bag,
}
} else {
unreachable!()
}
});
matcher.match_paths(
snapshot.id(),
path_prefix,
paths,
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> {
fn new(
pub fn new(
query: &'a [char],
lowercase_query: &'a [char],
query_char_bag: CharBag,
@@ -343,7 +235,7 @@ impl<'a> Matcher<'a> {
}
}
fn match_strings(
pub fn match_strings(
&mut self,
candidates: &[StringMatchCandidate],
results: &mut Vec<StringMatch>,
@@ -363,7 +255,7 @@ impl<'a> Matcher<'a> {
)
}
fn match_paths(
pub fn match_paths(
&mut self,
tree_id: usize,
path_prefix: Arc<str>,
+243 -6
View File
@@ -1,14 +1,20 @@
use crate::{
fs::Fs,
fuzzy::{CharBag, Matcher, PathMatchCandidate},
language::LanguageRegistry,
rpc::Client,
util::TryFutureExt as _,
worktree::{self, Worktree},
util::{self, TryFutureExt as _},
worktree::{self, EntryKind, PathMatch, Snapshot, Worktree},
AppState,
};
use anyhow::Result;
use gpui::{Entity, ModelContext, ModelHandle, Task};
use std::{path::Path, sync::Arc};
use futures::Future;
use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task};
use std::{
cmp,
path::Path,
sync::{atomic::AtomicBool, Arc},
};
pub struct Project {
worktrees: Vec<ModelHandle<Worktree>>,
@@ -47,13 +53,13 @@ impl Project {
pub fn add_local_worktree(
&mut self,
path: &Path,
abs_path: &Path,
cx: &mut ModelContext<Self>,
) -> Task<Result<ModelHandle<Worktree>>> {
let fs = self.fs.clone();
let rpc = self.rpc.clone();
let languages = self.languages.clone();
let path = Arc::from(path);
let path = Arc::from(abs_path);
cx.spawn(|this, mut cx| async move {
let worktree = Worktree::open_local(rpc, path, fs, languages, &mut cx).await?;
this.update(&mut cx, |this, cx| {
@@ -182,8 +188,239 @@ impl Project {
keep
});
}
pub fn match_paths<'a>(
&self,
query: &str,
include_ignored: bool,
smart_case: bool,
max_results: usize,
cancel_flag: &'a AtomicBool,
cx: &AppContext,
) -> impl 'a + Future<Output = Vec<PathMatch>> {
let snapshots = self
.worktrees
.iter()
.map(|worktree| worktree.read(cx).snapshot())
.collect::<Vec<_>>();
let path_count: usize = if include_ignored {
snapshots.iter().map(Snapshot::file_count).sum()
} else {
snapshots.iter().map(Snapshot::visible_file_count).sum()
};
let lowercase_query = query.to_lowercase().chars().collect::<Vec<_>>();
let query = query.chars().collect::<Vec<_>>();
let query_char_bag = CharBag::from(&lowercase_query[..]);
let background = cx.background().clone();
async move {
if path_count == 0 {
return Vec::new();
}
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<_>>();
let lowercase_query = &lowercase_query;
let query = &query;
let snapshots = snapshots.as_slice();
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 snapshot in snapshots {
let tree_end = if include_ignored {
tree_start + snapshot.file_count()
} else {
tree_start + snapshot.visible_file_count()
};
if tree_start < segment_end && segment_start < tree_end {
let path_prefix: Arc<str> =
if snapshot.root_entry().map_or(false, |e| e.is_file()) {
snapshot.root_name().into()
} else if snapshots.len() > 1 {
format!("{}/", snapshot.root_name()).into()
} else {
"".into()
};
let start = cmp::max(tree_start, segment_start) - tree_start;
let end = cmp::min(tree_end, segment_end) - tree_start;
let paths = snapshot
.files(include_ignored, start)
.take(end - start)
.map(|entry| {
if let EntryKind::File(char_bag) = entry.kind {
PathMatchCandidate {
path: &entry.path,
char_bag,
}
} else {
unreachable!()
}
});
matcher.match_paths(
snapshot.id(),
path_prefix,
paths,
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 Entity for Project {
type Event = Event;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
fs::RealFs,
test::{temp_tree, test_app_state},
};
use serde_json::json;
use std::{os::unix, path::PathBuf};
#[gpui::test]
async fn test_populate_and_search(mut cx: gpui::TestAppContext) {
let mut app_state = cx.update(test_app_state);
Arc::get_mut(&mut app_state).unwrap().fs = Arc::new(RealFs);
let dir = temp_tree(json!({
"root": {
"apple": "",
"banana": {
"carrot": {
"date": "",
"endive": "",
}
},
"fennel": {
"grape": "",
}
}
}));
let root_link_path = dir.path().join("root_link");
unix::fs::symlink(&dir.path().join("root"), &root_link_path).unwrap();
unix::fs::symlink(
&dir.path().join("root/fennel"),
&dir.path().join("root/finnochio"),
)
.unwrap();
let project = cx.add_model(|_| Project::new(app_state.as_ref()));
let tree = project
.update(&mut cx, |project, cx| {
project.add_local_worktree(&root_link_path, cx)
})
.await
.unwrap();
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
.await;
cx.read(|cx| {
let tree = tree.read(cx);
assert_eq!(tree.file_count(), 5);
assert_eq!(
tree.inode_for_path("fennel/grape"),
tree.inode_for_path("finnochio/grape")
);
});
let cancel_flag = Default::default();
let results = project
.read_with(&cx, |project, cx| {
project.match_paths("bna", false, false, 10, &cancel_flag, cx)
})
.await;
assert_eq!(
results
.into_iter()
.map(|result| result.path)
.collect::<Vec<Arc<Path>>>(),
vec![
PathBuf::from("banana/carrot/date").into(),
PathBuf::from("banana/carrot/endive").into(),
]
);
}
#[gpui::test]
async fn test_search_worktree_without_files(mut cx: gpui::TestAppContext) {
let mut app_state = cx.update(test_app_state);
Arc::get_mut(&mut app_state).unwrap().fs = Arc::new(RealFs);
let dir = temp_tree(json!({
"root": {
"dir1": {},
"dir2": {
"dir3": {}
}
}
}));
let project = cx.add_model(|_| Project::new(app_state.as_ref()));
let tree = project
.update(&mut cx, |project, cx| {
project.add_local_worktree(&dir.path(), cx)
})
.await
.unwrap();
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
.await;
let cancel_flag = Default::default();
let results = project
.read_with(&cx, |project, cx| {
project.match_paths("dir", false, false, 10, &cancel_flag, cx)
})
.await;
assert!(results.is_empty());
}
}
+4
View File
@@ -454,6 +454,10 @@ impl Workspace {
}
}
pub fn project(&self) -> &ModelHandle<Project> {
&self.project
}
pub fn worktrees<'a>(&self, cx: &'a AppContext) -> &'a [ModelHandle<Worktree>] {
&self.project.read(cx).worktrees()
}
+4 -127
View File
@@ -14,12 +14,10 @@ use crate::{
use ::ignore::gitignore::{Gitignore, GitignoreBuilder};
use anyhow::{anyhow, Result};
use futures::{Stream, StreamExt};
pub use fuzzy::{match_paths, PathMatch};
use sum_tree::{self, Edit, SeekTarget, SumTree};
pub use fuzzy::PathMatch;
use gpui::{
executor,
AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task,
UpgradeModelHandle, WeakModelHandle,
executor, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext,
Task, UpgradeModelHandle, WeakModelHandle,
};
use lazy_static::lazy_static;
use parking_lot::Mutex;
@@ -44,6 +42,7 @@ use std::{
},
time::{Duration, SystemTime},
};
use sum_tree::{self, Edit, SeekTarget, SumTree};
use zrpc::{PeerId, TypedEnvelope};
lazy_static! {
@@ -2841,128 +2840,6 @@ mod tests {
})
}
#[gpui::test]
async fn test_populate_and_search(cx: gpui::TestAppContext) {
let dir = temp_tree(json!({
"root": {
"apple": "",
"banana": {
"carrot": {
"date": "",
"endive": "",
}
},
"fennel": {
"grape": "",
}
}
}));
let root_link_path = dir.path().join("root_link");
unix::fs::symlink(&dir.path().join("root"), &root_link_path).unwrap();
unix::fs::symlink(
&dir.path().join("root/fennel"),
&dir.path().join("root/finnochio"),
)
.unwrap();
let tree = Worktree::open_local(
rpc::Client::new(),
root_link_path,
Arc::new(RealFs),
Default::default(),
&mut cx.to_async(),
)
.await
.unwrap();
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
.await;
let snapshots = [cx.read(|cx| {
let tree = tree.read(cx);
assert_eq!(tree.file_count(), 5);
assert_eq!(
tree.inode_for_path("fennel/grape"),
tree.inode_for_path("finnochio/grape")
);
tree.snapshot()
})];
let cancel_flag = Default::default();
let results = cx
.read(|cx| {
match_paths(
&snapshots,
"bna",
false,
false,
10,
&cancel_flag,
cx.background().clone(),
)
})
.await;
assert_eq!(
results
.into_iter()
.map(|result| result.path)
.collect::<Vec<Arc<Path>>>(),
vec![
PathBuf::from("banana/carrot/date").into(),
PathBuf::from("banana/carrot/endive").into(),
]
);
}
#[gpui::test]
async fn test_search_worktree_without_files(cx: gpui::TestAppContext) {
let dir = temp_tree(json!({
"root": {
"dir1": {},
"dir2": {
"dir3": {}
}
}
}));
let tree = Worktree::open_local(
rpc::Client::new(),
dir.path(),
Arc::new(RealFs),
Default::default(),
&mut cx.to_async(),
)
.await
.unwrap();
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
.await;
let snapshots = [cx.read(|cx| {
let tree = tree.read(cx);
assert_eq!(tree.file_count(), 0);
tree.snapshot()
})];
let cancel_flag = Default::default();
let results = cx
.read(|cx| {
match_paths(
&snapshots,
"dir",
false,
false,
10,
&cancel_flag,
cx.background().clone(),
)
})
.await;
assert_eq!(
results
.into_iter()
.map(|result| result.path)
.collect::<Vec<Arc<Path>>>(),
vec![]
);
}
#[gpui::test]
async fn test_save_file(mut cx: gpui::TestAppContext) {
let dir = temp_tree(json!({