From 88b88a80672014b51b5ed1c79d8bf79985a5715f Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 28 Apr 2021 17:46:27 -0700 Subject: [PATCH 1/4] Start work on opening files --- zed/src/editor/buffer/mod.rs | 7 ++- zed/src/editor/buffer_view.rs | 7 +-- zed/src/workspace/mod.rs | 10 ++-- zed/src/workspace/workspace.rs | 25 +++++---- zed/src/workspace/workspace_view.rs | 82 +++++++++++++++++++++++++++-- zed/src/worktree.rs | 24 ++++++++- 6 files changed, 132 insertions(+), 23 deletions(-) diff --git a/zed/src/editor/buffer/mod.rs b/zed/src/editor/buffer/mod.rs index 597990fbb34155e0ca5ea0ff5ad113dd7607281f..55ffaec02809f4234beca977fcd17f8069ae1c3f 100644 --- a/zed/src/editor/buffer/mod.rs +++ b/zed/src/editor/buffer/mod.rs @@ -18,11 +18,12 @@ use crate::{ worktree::FileHandle, }; use anyhow::{anyhow, Result}; -use gpui::{Entity, ModelContext}; +use gpui::{AppContext, Entity, ModelContext}; use lazy_static::lazy_static; use rand::prelude::*; use std::{ cmp, + ffi::OsString, hash::BuildHasher, iter::{self, Iterator}, ops::{AddAssign, Range}, @@ -447,6 +448,10 @@ impl Buffer { } } + pub fn file_name(&self, ctx: &AppContext) -> Option { + self.file.as_ref().and_then(|file| file.file_name(ctx)) + } + pub fn path(&self) -> Option> { self.file.as_ref().map(|file| file.path()) } diff --git a/zed/src/editor/buffer_view.rs b/zed/src/editor/buffer_view.rs index 77cf3bf7799520a13588b6f1aef1e7e4e5d8aaee..bd740831932fd2d8fcede0726aecc6aff64187d9 100644 --- a/zed/src/editor/buffer_view.rs +++ b/zed/src/editor/buffer_view.rs @@ -1362,11 +1362,8 @@ impl workspace::ItemView for BufferView { } fn title(&self, app: &AppContext) -> std::string::String { - if let Some(path) = self.buffer.read(app).path() { - path.file_name() - .expect("buffer's path is always to a file") - .to_string_lossy() - .into() + if let Some(name) = self.buffer.read(app).file_name(app) { + name.to_string_lossy().into() } else { "untitled".into() } diff --git a/zed/src/workspace/mod.rs b/zed/src/workspace/mod.rs index 005b4b24355f8b0956e38e5f74af1542233dad24..ae415c0e71edbd873823fdfef1be5cd168010604 100644 --- a/zed/src/workspace/mod.rs +++ b/zed/src/workspace/mod.rs @@ -52,7 +52,7 @@ fn open_paths(params: &OpenParams, app: &mut MutableAppContext) { if let Some(handle) = app.root_view::(window_id) { if handle.update(app, |view, ctx| { if view.contains_paths(¶ms.paths, ctx.as_ref()) { - view.open_paths(¶ms.paths, ctx.as_mut()); + view.open_paths(¶ms.paths, ctx); log::info!("open paths on existing workspace"); true } else { @@ -67,8 +67,12 @@ fn open_paths(params: &OpenParams, app: &mut MutableAppContext) { log::info!("open new workspace"); // Add a new workspace if necessary - let workspace = app.add_model(|ctx| Workspace::new(params.paths.clone(), ctx)); - app.add_window(|ctx| WorkspaceView::new(workspace, params.settings.clone(), ctx)); + let workspace = app.add_model(|ctx| Workspace::new(vec![], ctx)); + app.add_window(|ctx| { + let view = WorkspaceView::new(workspace, params.settings.clone(), ctx); + view.open_paths(¶ms.paths, ctx); + view + }); } fn quit(_: &(), app: &mut MutableAppContext) { diff --git a/zed/src/workspace/workspace.rs b/zed/src/workspace/workspace.rs index 925781e34070324c21dd2dfe0e6aa5012f81f06f..0f0272957260f4c4dbb952ab44eb3ab3475ea0a4 100644 --- a/zed/src/workspace/workspace.rs +++ b/zed/src/workspace/workspace.rs @@ -117,23 +117,31 @@ impl Workspace { .any(|worktree| worktree.read(app).contains_abs_path(path)) } - pub fn open_paths(&mut self, paths: &[PathBuf], ctx: &mut ModelContext) { - for path in paths.iter().cloned() { - self.open_path(path, ctx); - } + pub fn open_paths( + &mut self, + paths: &[PathBuf], + ctx: &mut ModelContext, + ) -> Vec<(usize, Arc)> { + paths + .iter() + .cloned() + .map(move |path| self.open_path(path, ctx)) + .collect() } - pub fn open_path<'a>(&'a mut self, path: PathBuf, ctx: &mut ModelContext) { + fn open_path(&mut self, path: PathBuf, ctx: &mut ModelContext) -> (usize, Arc) { for tree in self.worktrees.iter() { - if tree.read(ctx).contains_abs_path(&path) { - return; + if let Ok(relative_path) = path.strip_prefix(tree.read(ctx).abs_path()) { + return (tree.id(), relative_path.into()); } } - let worktree = ctx.add_model(|ctx| Worktree::new(path, ctx)); + let worktree = ctx.add_model(|ctx| Worktree::new(path.clone(), ctx)); + let worktree_id = worktree.id(); ctx.observe(&worktree, Self::on_worktree_updated); self.worktrees.insert(worktree); ctx.notify(); + (worktree_id, Path::new("").into()) } pub fn open_entry( @@ -174,7 +182,6 @@ impl Workspace { let replica_id = self.replica_id; let file = worktree.file(path.clone(), ctx.as_ref())?; let history = file.load_history(ctx.as_ref()); - // let buffer = async move { Ok(Buffer::from_history(replica_id, file, history.await?)) }; let (mut tx, rx) = watch::channel(None); self.items.insert(item_key, OpenedItem::Loading(rx)); diff --git a/zed/src/workspace/workspace_view.rs b/zed/src/workspace/workspace_view.rs index f8ca8822b2cccb8fe5c81c300fbcf86f0f8e00ee..e698a3a278119511e60503c3b8c7a9ec0de8bda2 100644 --- a/zed/src/workspace/workspace_view.rs +++ b/zed/src/workspace/workspace_view.rs @@ -161,9 +161,23 @@ impl WorkspaceView { self.workspace.read(app).contains_paths(paths, app) } - pub fn open_paths(&self, paths: &[PathBuf], app: &mut MutableAppContext) { - self.workspace - .update(app, |workspace, ctx| workspace.open_paths(paths, ctx)); + pub fn open_paths(&self, paths: &[PathBuf], ctx: &mut ViewContext) { + let entries = self + .workspace + .update(ctx, |workspace, ctx| workspace.open_paths(paths, ctx)); + for (i, entry) in entries.into_iter().enumerate() { + let path = paths[i].clone(); + ctx.spawn( + ctx.background_executor() + .spawn(async move { path.is_file() }), + |me, is_file, ctx| { + if is_file { + me.open_entry(entry, ctx) + } + }, + ) + .detach(); + } } pub fn toggle_modal(&mut self, ctx: &mut ViewContext, add_view: F) @@ -382,6 +396,7 @@ mod tests { use crate::{settings, test::temp_tree, workspace::WorkspaceHandle as _}; use gpui::App; use serde_json::json; + use std::collections::HashSet; #[test] fn test_open_entry() { @@ -444,6 +459,67 @@ mod tests { }); } + #[test] + fn test_open_paths() { + App::test_async((), |mut app| async move { + let dir1 = temp_tree(json!({ + "a.txt": "", + })); + let dir2 = temp_tree(json!({ + "b.txt": "", + })); + + let workspace = app.add_model(|ctx| Workspace::new(vec![dir1.path().into()], ctx)); + let settings = settings::channel(&app.font_cache()).unwrap().1; + let (_, workspace_view) = + app.add_window(|ctx| WorkspaceView::new(workspace.clone(), settings, ctx)); + app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx)) + .await; + + // Open a file within an existing worktree. + app.update(|ctx| { + workspace_view.update(ctx, |view, ctx| { + view.open_paths(&[dir1.path().join("a.txt")], ctx); + }); + }); + workspace_view + .condition(&app, |view, ctx| { + view.active_pane() + .read(ctx) + .active_item() + .map_or(false, |item| item.title(&ctx) == "a.txt") + }) + .await; + + // Open a file outside of any existing worktree. + app.update(|ctx| { + workspace_view.update(ctx, |view, ctx| { + view.open_paths(&[dir2.path().join("b.txt")], ctx); + }); + let worktree_roots = workspace + .read(ctx) + .worktrees() + .iter() + .map(|w| w.read(ctx).abs_path()) + .collect::>(); + assert_eq!( + worktree_roots, + vec![dir1.path(), &dir2.path().join("b.txt")] + .into_iter() + .collect(), + ); + }); + workspace_view + .condition(&app, |view, ctx| { + view.active_pane() + .read(ctx) + .active_item() + .map_or(false, |item| item.title(&ctx) == "b.txt") + }) + .await; + }); + } + #[test] fn test_pane_actions() { App::test_async((), |mut app| async move { diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 2ac744e8b81f8a606029acb360e4e4523d55ec95..be1f1c2307df3572decf04da6f0271608965df10 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -20,7 +20,7 @@ use smol::{channel::Sender, Timer}; use std::{ cmp, collections::{HashMap, HashSet}, - ffi::{CStr, OsStr}, + ffi::{CStr, OsStr, OsString}, fmt, fs, future::Future, io::{self, Read, Write}, @@ -163,6 +163,10 @@ impl Worktree { self.snapshot.clone() } + pub fn abs_path(&self) -> &Path { + self.snapshot.abs_path.as_ref() + } + pub fn contains_abs_path(&self, path: &Path) -> bool { path.starts_with(&self.snapshot.abs_path) } @@ -172,7 +176,11 @@ impl Worktree { path: &Path, ctx: &AppContext, ) -> impl Future> { - let abs_path = self.snapshot.abs_path.join(path); + let abs_path = if path.file_name().is_some() { + self.snapshot.abs_path.join(path) + } else { + self.snapshot.abs_path.to_path_buf() + }; ctx.background_executor().spawn(async move { let mut file = std::fs::File::open(&abs_path)?; let mut base_text = String::new(); @@ -381,10 +389,22 @@ impl fmt::Debug for Snapshot { } impl FileHandle { + /// Returns this file's path relative to the root of its worktree. pub fn path(&self) -> Arc { self.state.lock().path.clone() } + /// Returns the last component of this handle's absolute path. If this handle refers to the root + /// of its worktree, then this method will return the name of the worktree itself. + pub fn file_name<'a>(&'a self, ctx: &'a AppContext) -> Option { + self.state + .lock() + .path + .file_name() + .or_else(|| self.worktree.read(ctx).abs_path().file_name()) + .map(Into::into) + } + pub fn is_deleted(&self) -> bool { self.state.lock().is_deleted } From b126938af75fcc1f7a04e861531d037e0371373e Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 29 Apr 2021 12:44:51 -0700 Subject: [PATCH 2/4] In file finder, handle single-file worktrees & multiple matches w/ same rel path --- zed/src/file_finder.rs | 287 ++++++++++++++++++++++++-------------- zed/src/worktree.rs | 49 ++++--- zed/src/worktree/fuzzy.rs | 8 +- 3 files changed, 212 insertions(+), 132 deletions(-) diff --git a/zed/src/file_finder.rs b/zed/src/file_finder.rs index 3add2d58af4ad03fccf2cf04ba1f96db961c5051..40bcfef5b9b43122deff564ebbbe7f8f4444cb19 100644 --- a/zed/src/file_finder.rs +++ b/zed/src/file_finder.rs @@ -33,8 +33,7 @@ pub struct FileFinder { latest_search_did_cancel: bool, latest_search_query: String, matches: Vec, - include_root_name: bool, - selected: Option>, + selected: Option<(usize, Arc)>, cancel_flag: Arc, list_state: UniformListState, } @@ -147,101 +146,110 @@ impl FileFinder { index: usize, app: &AppContext, ) -> Option { - let tree_id = path_match.tree_id; - - self.worktree(tree_id, app).map(|tree| { - let prefix = if self.include_root_name { - tree.root_name() - } else { - "" - }; - let path = path_match.path.clone(); - let path_string = path_match.path.to_string_lossy(); - let file_name = path_match - .path - .file_name() - .unwrap_or_default() - .to_string_lossy(); - - let path_positions = path_match.positions.clone(); - let file_name_start = - prefix.len() + path_string.chars().count() - file_name.chars().count(); - let mut file_name_positions = Vec::new(); - file_name_positions.extend(path_positions.iter().filter_map(|pos| { - if pos >= &file_name_start { - Some(pos - file_name_start) - } else { - None - } - })); - - let settings = smol::block_on(self.settings.read()); - let highlight_color = ColorU::from_u32(0x304ee2ff); - let bold = *Properties::new().weight(Weight::BOLD); - - let mut full_path = prefix.to_string(); - full_path.push_str(&path_string); - - let mut container = Container::new( - Flex::row() - .with_child( - Container::new( - LineBox::new( - settings.ui_font_family, - settings.ui_font_size, - Svg::new("icons/file-16.svg").boxed(), + self.labels_for_match(path_match, app).map( + |(file_name, file_name_positions, full_path, full_path_positions)| { + let settings = smol::block_on(self.settings.read()); + let highlight_color = ColorU::from_u32(0x304ee2ff); + let bold = *Properties::new().weight(Weight::BOLD); + let mut container = Container::new( + Flex::row() + .with_child( + Container::new( + LineBox::new( + settings.ui_font_family, + settings.ui_font_size, + Svg::new("icons/file-16.svg").boxed(), + ) + .boxed(), ) + .with_padding_right(6.0) .boxed(), ) - .with_padding_right(6.0) - .boxed(), - ) - .with_child( - Expanded::new( - 1.0, - Flex::column() - .with_child( - Label::new( - file_name.to_string(), - settings.ui_font_family, - settings.ui_font_size, + .with_child( + Expanded::new( + 1.0, + Flex::column() + .with_child( + Label::new( + file_name.to_string(), + settings.ui_font_family, + settings.ui_font_size, + ) + .with_highlights(highlight_color, bold, file_name_positions) + .boxed(), ) - .with_highlights(highlight_color, bold, file_name_positions) - .boxed(), - ) - .with_child( - Label::new( - full_path, - settings.ui_font_family, - settings.ui_font_size, + .with_child( + Label::new( + full_path, + settings.ui_font_family, + settings.ui_font_size, + ) + .with_highlights(highlight_color, bold, full_path_positions) + .boxed(), ) - .with_highlights(highlight_color, bold, path_positions) .boxed(), - ) - .boxed(), + ) + .boxed(), ) .boxed(), - ) - .boxed(), - ) - .with_uniform_padding(6.0); + ) + .with_uniform_padding(6.0); - let selected_index = self.selected_index(); - if index == selected_index || index < self.matches.len() - 1 { - container = - container.with_border(Border::bottom(1.0, ColorU::from_u32(0xdbdbdcff))); - } + let selected_index = self.selected_index(); + if index == selected_index || index < self.matches.len() - 1 { + container = + container.with_border(Border::bottom(1.0, ColorU::from_u32(0xdbdbdcff))); + } - if index == selected_index { - container = container.with_background_color(ColorU::from_u32(0xdbdbdcff)); - } + if index == selected_index { + container = container.with_background_color(ColorU::from_u32(0xdbdbdcff)); + } + + let entry = (path_match.tree_id, path_match.path.clone()); + EventHandler::new(container.boxed()) + .on_mouse_down(move |ctx| { + ctx.dispatch_action("file_finder:select", entry.clone()); + true + }) + .named("match") + }, + ) + } + + fn labels_for_match( + &self, + path_match: &PathMatch, + app: &AppContext, + ) -> Option<(String, Vec, String, Vec)> { + self.worktree(path_match.tree_id, app).map(|tree| { + let prefix = if path_match.include_root_name { + tree.root_name() + } else { + "" + }; - EventHandler::new(container.boxed()) - .on_mouse_down(move |ctx| { - ctx.dispatch_action("file_finder:select", (tree_id, path.clone())); - true + let path_string = path_match.path.to_string_lossy(); + let full_path = [prefix, path_string.as_ref()].join(""); + let path_positions = path_match.positions.clone(); + + let file_name = path_match.path.file_name().map_or_else( + || prefix.to_string(), + |file_name| file_name.to_string_lossy().to_string(), + ); + let file_name_start = + prefix.chars().count() + path_string.chars().count() - file_name.chars().count(); + let file_name_positions = path_positions + .iter() + .filter_map(|pos| { + if pos >= &file_name_start { + Some(pos - file_name_start) + } else { + None + } }) - .named("match") + .collect(); + + (file_name, file_name_positions, full_path, path_positions) }) } @@ -298,7 +306,6 @@ impl FileFinder { latest_search_did_cancel: false, latest_search_query: String::new(), matches: Vec::new(), - include_root_name: false, selected: None, cancel_flag: Arc::new(AtomicBool::new(false)), list_state: UniformListState::new(), @@ -335,7 +342,9 @@ impl FileFinder { fn selected_index(&self) -> usize { if let Some(selected) = self.selected.as_ref() { for (ix, path_match) in self.matches.iter().enumerate() { - if path_match.path.as_ref() == selected.as_ref() { + if (path_match.tree_id, path_match.path.as_ref()) + == (selected.0, selected.1.as_ref()) + { return ix; } } @@ -347,7 +356,8 @@ impl FileFinder { let mut selected_index = self.selected_index(); if selected_index > 0 { selected_index -= 1; - self.selected = Some(self.matches[selected_index].path.clone()); + let mat = &self.matches[selected_index]; + self.selected = Some((mat.tree_id, mat.path.clone())); } self.list_state.scroll_to(selected_index); ctx.notify(); @@ -357,7 +367,8 @@ impl FileFinder { let mut selected_index = self.selected_index(); if selected_index + 1 < self.matches.len() { selected_index += 1; - self.selected = Some(self.matches[selected_index].path.clone()); + let mat = &self.matches[selected_index]; + self.selected = Some((mat.tree_id, mat.path.clone())); } self.list_state.scroll_to(selected_index); ctx.notify(); @@ -403,7 +414,7 @@ impl FileFinder { pool, ); let did_cancel = cancel_flag.load(atomic::Ordering::Relaxed); - (search_id, include_root_name, did_cancel, query, matches) + (search_id, did_cancel, query, matches) }); ctx.spawn(task, Self::update_matches).detach(); @@ -411,13 +422,7 @@ impl FileFinder { fn update_matches( &mut self, - (search_id, include_root_name, did_cancel, query, matches): ( - usize, - bool, - bool, - String, - Vec, - ), + (search_id, did_cancel, query, matches): (usize, bool, String, Vec), ctx: &mut ViewContext, ) { if search_id >= self.latest_search_id { @@ -429,7 +434,6 @@ impl FileFinder { } self.latest_search_query = query; self.latest_search_did_cancel = did_cancel; - self.include_root_name = include_root_name; self.list_state.scroll_to(self.selected_index()); ctx.notify(); } @@ -454,20 +458,16 @@ mod tests { }; use gpui::App; use serde_json::json; - use smol::fs; + use std::fs; use tempdir::TempDir; #[test] fn test_matching_paths() { App::test_async((), |mut app| async move { let tmp_dir = TempDir::new("example").unwrap(); - fs::create_dir(tmp_dir.path().join("a")).await.unwrap(); - fs::write(tmp_dir.path().join("a/banana"), "banana") - .await - .unwrap(); - fs::write(tmp_dir.path().join("a/bandana"), "bandana") - .await - .unwrap(); + fs::create_dir(tmp_dir.path().join("a")).unwrap(); + fs::write(tmp_dir.path().join("a/banana"), "banana").unwrap(); + fs::write(tmp_dir.path().join("a/bandana"), "bandana").unwrap(); app.update(|ctx| { super::init(ctx); editor::init(ctx); @@ -560,7 +560,6 @@ mod tests { finder.update_matches( ( finder.latest_search_id, - true, true, // did-cancel query.clone(), vec![matches[1].clone(), matches[3].clone()], @@ -573,7 +572,6 @@ mod tests { finder.update_matches( ( finder.latest_search_id, - true, true, // did-cancel query.clone(), vec![matches[0].clone(), matches[2].clone(), matches[3].clone()], @@ -585,4 +583,77 @@ mod tests { }); }); } + + #[test] + fn test_single_file_worktrees() { + App::test_async((), |mut app| async move { + let temp_dir = TempDir::new("test-single-file-worktrees").unwrap(); + let dir_path = temp_dir.path().join("the-parent-dir"); + let file_path = dir_path.join("the-file"); + fs::create_dir(&dir_path).unwrap(); + fs::write(&file_path, "").unwrap(); + + let settings = settings::channel(&app.font_cache()).unwrap().1; + let workspace = app.add_model(|ctx| Workspace::new(vec![file_path], ctx)); + app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx)) + .await; + let (_, finder) = + app.add_window(|ctx| FileFinder::new(settings, workspace.clone(), ctx)); + + // Even though there is only one worktree, that worktree's filename + // is included in the matching, because the worktree is a single file. + finder.update(&mut app, |f, ctx| f.spawn_search("thf".into(), ctx)); + finder.condition(&app, |f, _| f.matches.len() == 1).await; + + app.read(|ctx| { + let finder = finder.read(ctx); + let (file_name, file_name_positions, full_path, full_path_positions) = + finder.labels_for_match(&finder.matches[0], ctx).unwrap(); + + assert_eq!(file_name, "the-file"); + assert_eq!(file_name_positions, &[0, 1, 4]); + assert_eq!(full_path, "the-file"); + assert_eq!(full_path_positions, &[0, 1, 4]); + }); + + // Since the worktree root is a file, searching for its name followed by a slash does + // not match anything. + finder.update(&mut app, |f, ctx| f.spawn_search("thf/".into(), ctx)); + finder.condition(&app, |f, _| f.matches.len() == 0).await; + }); + } + + #[test] + fn test_multiple_matches_with_same_relative_path() { + App::test_async((), |mut app| async move { + let tmp_dir = temp_tree(json!({ + "dir1": { "a.txt": "" }, + "dir2": { "a.txt": "" } + })); + let settings = settings::channel(&app.font_cache()).unwrap().1; + let workspace = app.add_model(|ctx| { + Workspace::new( + vec![tmp_dir.path().join("dir1"), tmp_dir.path().join("dir2")], + ctx, + ) + }); + app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx)) + .await; + let (_, finder) = + app.add_window(|ctx| FileFinder::new(settings, workspace.clone(), ctx)); + + // Run a search that matches two files with the same relative path. + finder.update(&mut app, |f, ctx| f.spawn_search("a.t".into(), ctx)); + finder.condition(&app, |f, _| f.matches.len() == 2).await; + + // Can switch between different matches with the same relative path. + finder.update(&mut app, |f, ctx| { + assert_eq!(f.selected_index(), 0); + f.select_next(&(), ctx); + assert_eq!(f.selected_index(), 1); + f.select_prev(&(), ctx); + assert_eq!(f.selected_index(), 0); + }); + }); + } } diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index be1f1c2307df3572decf04da6f0271608965df10..7152316938a9a7b917d11c6ef61d18cbeb79d53c 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -68,16 +68,13 @@ struct FileHandleState { impl Worktree { pub fn new(path: impl Into>, ctx: &mut ModelContext) -> Self { let abs_path = path.into(); - let root_name = abs_path - .file_name() - .map_or(String::new(), |n| n.to_string_lossy().to_string() + "/"); let (scan_state_tx, scan_state_rx) = smol::channel::unbounded(); let id = ctx.model_id(); let snapshot = Snapshot { id, scan_id: 0, abs_path, - root_name, + root_name: Default::default(), ignores: Default::default(), entries: Default::default(), }; @@ -269,8 +266,8 @@ impl Snapshot { self.entry_for_path("").unwrap() } - /// Returns the filename of the snapshot's root directory, - /// with a trailing slash. + /// Returns the filename of the snapshot's root, plus a trailing slash if the snapshot's root is + /// a directory. pub fn root_name(&self) -> &str { &self.root_name } @@ -481,6 +478,10 @@ impl Entry { fn is_dir(&self) -> bool { matches!(self.kind, EntryKind::Dir | EntryKind::PendingDir) } + + fn is_file(&self) -> bool { + matches!(self.kind, EntryKind::File(_)) + } } impl sum_tree::Item for Entry { @@ -489,7 +490,7 @@ impl sum_tree::Item for Entry { fn summary(&self) -> Self::Summary { let file_count; let visible_file_count; - if matches!(self.kind, EntryKind::File(_)) { + if self.is_file() { file_count = 1; if self.is_ignored { visible_file_count = 0; @@ -631,14 +632,8 @@ impl BackgroundScanner { notify: Sender, worktree_id: usize, ) -> Self { - let root_char_bag = snapshot - .lock() - .root_name - .chars() - .map(|c| c.to_ascii_lowercase()) - .collect(); let mut scanner = Self { - root_char_bag, + root_char_bag: Default::default(), snapshot, notify, handles, @@ -699,7 +694,7 @@ impl BackgroundScanner { }); } - fn scan_dirs(&self) -> io::Result<()> { + fn scan_dirs(&mut self) -> io::Result<()> { self.snapshot.lock().scan_id += 1; let path: Arc = Arc::from(Path::new("")); @@ -707,19 +702,29 @@ impl BackgroundScanner { let metadata = fs::metadata(&abs_path)?; let inode = metadata.ino(); let is_symlink = fs::symlink_metadata(&abs_path)?.file_type().is_symlink(); + let is_dir = metadata.file_type().is_dir(); - if metadata.file_type().is_dir() { - let dir_entry = Entry { + // After determining whether the root entry is a file or a directory, populate the + // snapshot's "root name", which will be used for the purpose of fuzzy matching. + let mut root_name = abs_path + .file_name() + .map_or(String::new(), |f| f.to_string_lossy().to_string()); + if is_dir { + root_name.push('/'); + } + self.root_char_bag = root_name.chars().map(|c| c.to_ascii_lowercase()).collect(); + self.snapshot.lock().root_name = root_name; + + if is_dir { + self.snapshot.lock().insert_entry(Entry { kind: EntryKind::PendingDir, path: path.clone(), inode, is_symlink, is_ignored: false, - }; - self.snapshot.lock().insert_entry(dir_entry); + }); let (tx, rx) = crossbeam_channel::unbounded(); - tx.send(ScanJob { abs_path: abs_path.to_path_buf(), path, @@ -1541,7 +1546,7 @@ mod tests { scanner.snapshot().check_invariants(); let (notify_tx, _notify_rx) = smol::channel::unbounded(); - let new_scanner = BackgroundScanner::new( + let mut new_scanner = BackgroundScanner::new( Arc::new(Mutex::new(Snapshot { id: 0, scan_id: 0, @@ -1711,7 +1716,7 @@ mod tests { let mut files = self.files(0); let mut visible_files = self.visible_files(0); for entry in self.entries.cursor::<(), ()>() { - if matches!(entry.kind, EntryKind::File(_)) { + if entry.is_file() { assert_eq!(files.next().unwrap().inode(), entry.inode); if !entry.is_ignored { assert_eq!(visible_files.next().unwrap().inode(), entry.inode); diff --git a/zed/src/worktree/fuzzy.rs b/zed/src/worktree/fuzzy.rs index 813147d929b0f4ebad3f0bc839861148f1bad4b1..4fe81372cf4f053521138fb4243a331470b75095 100644 --- a/zed/src/worktree/fuzzy.rs +++ b/zed/src/worktree/fuzzy.rs @@ -24,6 +24,7 @@ pub struct PathMatch { pub positions: Vec, pub tree_id: usize, pub path: Arc, + pub include_root_name: bool, } impl PartialEq for PathMatch { @@ -84,7 +85,7 @@ where pool.scoped(|scope| { for (segment_idx, results) in segment_results.iter_mut().enumerate() { - let trees = snapshots.clone(); + let snapshots = snapshots.clone(); let cancel_flag = &cancel_flag; scope.execute(move || { let segment_start = segment_idx * segment_size; @@ -99,12 +100,14 @@ where let mut best_position_matrix = Vec::new(); let mut tree_start = 0; - for snapshot in trees { + for snapshot in snapshots { let tree_end = if include_ignored { tree_start + snapshot.file_count() } else { tree_start + snapshot.visible_file_count() }; + + let include_root_name = include_root_name || snapshot.root_entry().is_file(); if tree_start < segment_end && segment_start < tree_end { let start = max(tree_start, segment_start) - tree_start; let end = min(tree_end, segment_end) - tree_start; @@ -246,6 +249,7 @@ fn match_single_tree_paths<'a>( path: candidate.path.clone(), score, positions: match_positions.clone(), + include_root_name, }; if let Err(i) = results.binary_search_by(|m| mat.cmp(&m)) { if results.len() < max_results { From 5826a976ef9200c7ae54b3bdd8f997147272428c Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 29 Apr 2021 14:33:42 -0700 Subject: [PATCH 3/4] Return a future from WorkspaceView::open_paths Co-Authored-By: Nathan Sobo --- gpui/src/app.rs | 4 + zed/src/workspace/mod.rs | 6 +- zed/src/workspace/workspace_view.rs | 188 ++++++++++++++++++---------- 3 files changed, 129 insertions(+), 69 deletions(-) diff --git a/gpui/src/app.rs b/gpui/src/app.rs index c732475e8bf932cd6130c5431ecb75bd0843a66c..02a1626a304c546f89fd8385fbef057e643ab985 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -1717,6 +1717,10 @@ impl<'a, T: View> ViewContext<'a, T> { self.window_id } + pub fn foreground(&self) -> &Rc { + self.app.foreground_executor() + } + pub fn background_executor(&self) -> &Arc { &self.app.ctx.background } diff --git a/zed/src/workspace/mod.rs b/zed/src/workspace/mod.rs index ae415c0e71edbd873823fdfef1be5cd168010604..504bb9a8c05b4965487820d745682e49395a0165 100644 --- a/zed/src/workspace/mod.rs +++ b/zed/src/workspace/mod.rs @@ -52,7 +52,8 @@ fn open_paths(params: &OpenParams, app: &mut MutableAppContext) { if let Some(handle) = app.root_view::(window_id) { if handle.update(app, |view, ctx| { if view.contains_paths(¶ms.paths, ctx.as_ref()) { - view.open_paths(¶ms.paths, ctx); + let open_paths = view.open_paths(¶ms.paths, ctx); + ctx.foreground().spawn(open_paths).detach(); log::info!("open paths on existing workspace"); true } else { @@ -70,7 +71,8 @@ fn open_paths(params: &OpenParams, app: &mut MutableAppContext) { let workspace = app.add_model(|ctx| Workspace::new(vec![], ctx)); app.add_window(|ctx| { let view = WorkspaceView::new(workspace, params.settings.clone(), ctx); - view.open_paths(¶ms.paths, ctx); + let open_paths = view.open_paths(¶ms.paths, ctx); + ctx.foreground().spawn(open_paths).detach(); view }); } diff --git a/zed/src/workspace/workspace_view.rs b/zed/src/workspace/workspace_view.rs index e698a3a278119511e60503c3b8c7a9ec0de8bda2..0fadb7b453f9c7907443642e79b8b3370a29bbd8 100644 --- a/zed/src/workspace/workspace_view.rs +++ b/zed/src/workspace/workspace_view.rs @@ -1,6 +1,6 @@ use super::{pane, Pane, PaneGroup, SplitDirection, Workspace}; use crate::{settings::Settings, watch}; -use futures_core::future::LocalBoxFuture; +use futures_core::{future::LocalBoxFuture, Future}; use gpui::{ color::rgbu, elements::*, json::to_string_pretty, keymap::Binding, AnyViewHandle, AppContext, ClipboardItem, Entity, ModelHandle, MutableAppContext, View, ViewContext, ViewHandle, @@ -161,22 +161,38 @@ impl WorkspaceView { self.workspace.read(app).contains_paths(paths, app) } - pub fn open_paths(&self, paths: &[PathBuf], ctx: &mut ViewContext) { + pub fn open_paths( + &self, + paths: &[PathBuf], + ctx: &mut ViewContext, + ) -> impl Future { let entries = self .workspace .update(ctx, |workspace, ctx| workspace.open_paths(paths, ctx)); - for (i, entry) in entries.into_iter().enumerate() { - let path = paths[i].clone(); - ctx.spawn( - ctx.background_executor() - .spawn(async move { path.is_file() }), - |me, is_file, ctx| { - if is_file { - me.open_entry(entry, ctx) - } - }, - ) - .detach(); + let bg = ctx.background_executor().clone(); + let tasks = paths + .iter() + .cloned() + .zip(entries.into_iter()) + .map(|(path, entry)| { + ctx.spawn( + bg.spawn(async move { path.is_file() }), + |me, is_file, ctx| { + if is_file { + me.open_entry(entry, ctx) + } else { + None + } + }, + ) + }) + .collect::>(); + async move { + for task in tasks { + if let Some(task) = task.await { + task.await; + } + } } } @@ -207,16 +223,20 @@ impl WorkspaceView { } } - pub fn open_entry(&mut self, entry: (usize, Arc), ctx: &mut ViewContext) { + pub fn open_entry( + &mut self, + entry: (usize, Arc), + ctx: &mut ViewContext, + ) -> Option> { if self.loading_entries.contains(&entry) { - return; + return None; } if self .active_pane() .update(ctx, |pane, ctx| pane.activate_entry(entry.clone(), ctx)) { - return; + return None; } self.loading_entries.insert(entry.clone()); @@ -224,10 +244,13 @@ impl WorkspaceView { match self.workspace.update(ctx, |workspace, ctx| { workspace.open_entry(entry.clone(), ctx) }) { - Err(error) => error!("{}", error), + Err(error) => { + error!("{}", error); + None + } Ok(item) => { let settings = self.settings.clone(); - ctx.spawn(item, move |me, item, ctx| { + Some(ctx.spawn(item, move |me, item, ctx| { me.loading_entries.remove(&entry); match item { Ok(item) => { @@ -238,8 +261,7 @@ impl WorkspaceView { error!("{}", error); } } - }) - .detach(); + })) } } } @@ -423,13 +445,23 @@ mod tests { let pane = app.read(|ctx| workspace_view.read(ctx).active_pane().clone()); // Open the first entry - workspace_view.update(&mut app, |w, ctx| w.open_entry(file1.clone(), ctx)); - pane.condition(&app, |pane, _| pane.items().len() == 1) + workspace_view + .update(&mut app, |w, ctx| w.open_entry(file1.clone(), ctx)) + .unwrap() .await; + app.read(|ctx| { + let pane = pane.read(ctx); + assert_eq!( + pane.active_item().unwrap().entry_id(ctx), + Some(file1.clone()) + ); + assert_eq!(pane.items().len(), 1); + }); // Open the second entry - workspace_view.update(&mut app, |w, ctx| w.open_entry(file2.clone(), ctx)); - pane.condition(&app, |pane, _| pane.items().len() == 2) + workspace_view + .update(&mut app, |w, ctx| w.open_entry(file2.clone(), ctx)) + .unwrap() .await; app.read(|ctx| { let pane = pane.read(ctx); @@ -437,25 +469,38 @@ mod tests { pane.active_item().unwrap().entry_id(ctx), Some(file2.clone()) ); + assert_eq!(pane.items().len(), 2); }); - // Open the first entry again - workspace_view.update(&mut app, |w, ctx| w.open_entry(file1.clone(), ctx)); - pane.condition(&app, move |pane, ctx| { - pane.active_item().unwrap().entry_id(ctx) == Some(file1.clone()) - }) - .await; + // Open the first entry again. The existing pane item is activated. + workspace_view.update(&mut app, |w, ctx| { + assert!(w.open_entry(file1.clone(), ctx).is_none()) + }); app.read(|ctx| { - assert_eq!(pane.read(ctx).items().len(), 2); + let pane = pane.read(ctx); + assert_eq!( + pane.active_item().unwrap().entry_id(ctx), + Some(file1.clone()) + ); + assert_eq!(pane.items().len(), 2); }); - // Open the third entry twice concurrently - workspace_view.update(&mut app, |w, ctx| { - w.open_entry(file3.clone(), ctx); - w.open_entry(file3.clone(), ctx); - }); - pane.condition(&app, |pane, _| pane.items().len() == 3) + // Open the third entry twice concurrently. Only one pane item is added. + workspace_view + .update(&mut app, |w, ctx| { + let task = w.open_entry(file3.clone(), ctx).unwrap(); + assert!(w.open_entry(file3.clone(), ctx).is_none()); + task + }) .await; + app.read(|ctx| { + let pane = pane.read(ctx); + assert_eq!( + pane.active_item().unwrap().entry_id(ctx), + Some(file3.clone()) + ); + assert_eq!(pane.items().len(), 3); + }); }); } @@ -479,23 +524,29 @@ mod tests { // Open a file within an existing worktree. app.update(|ctx| { workspace_view.update(ctx, |view, ctx| { - view.open_paths(&[dir1.path().join("a.txt")], ctx); - }); - }); - workspace_view - .condition(&app, |view, ctx| { - view.active_pane() - .read(ctx) - .active_item() - .map_or(false, |item| item.title(&ctx) == "a.txt") + view.open_paths(&[dir1.path().join("a.txt")], ctx) }) - .await; + }) + .await; + app.read(|ctx| { + workspace_view + .read(ctx) + .active_pane() + .read(ctx) + .active_item() + .unwrap() + .title(ctx) + == "a.txt" + }); // Open a file outside of any existing worktree. app.update(|ctx| { workspace_view.update(ctx, |view, ctx| { - view.open_paths(&[dir2.path().join("b.txt")], ctx); - }); + view.open_paths(&[dir2.path().join("b.txt")], ctx) + }) + }) + .await; + app.update(|ctx| { let worktree_roots = workspace .read(ctx) .worktrees() @@ -509,14 +560,16 @@ mod tests { .collect(), ); }); - workspace_view - .condition(&app, |view, ctx| { - view.active_pane() - .read(ctx) - .active_item() - .map_or(false, |item| item.title(&ctx) == "b.txt") - }) - .await; + app.read(|ctx| { + workspace_view + .read(ctx) + .active_pane() + .read(ctx) + .active_item() + .unwrap() + .title(ctx) + == "b.txt" + }); }); } @@ -544,15 +597,16 @@ mod tests { app.add_window(|ctx| WorkspaceView::new(workspace.clone(), settings, ctx)); let pane_1 = app.read(|ctx| workspace_view.read(ctx).active_pane().clone()); - workspace_view.update(&mut app, |w, ctx| w.open_entry(file1.clone(), ctx)); - { - let file1 = file1.clone(); - pane_1 - .condition(&app, move |pane, ctx| { - pane.active_item().and_then(|i| i.entry_id(ctx)) == Some(file1.clone()) - }) - .await; - } + workspace_view + .update(&mut app, |w, ctx| w.open_entry(file1.clone(), ctx)) + .unwrap() + .await; + app.read(|ctx| { + assert_eq!( + pane_1.read(ctx).active_item().unwrap().entry_id(ctx), + Some(file1.clone()) + ); + }); app.dispatch_action(window_id, vec![pane_1.id()], "pane:split_right", ()); app.update(|ctx| { From 9b0cc0a032f92c0edc09663fe16fcab2308b1a20 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 29 Apr 2021 15:03:46 -0700 Subject: [PATCH 4/4] Avoid cancelling ::open_entry task in FileFinder --- zed/src/file_finder.rs | 4 +++- zed/src/workspace/workspace_view.rs | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/zed/src/file_finder.rs b/zed/src/file_finder.rs index 40bcfef5b9b43122deff564ebbbe7f8f4444cb19..0884cbdf5a118be6dd4a499cecc66ee92893c538 100644 --- a/zed/src/file_finder.rs +++ b/zed/src/file_finder.rs @@ -275,7 +275,9 @@ impl FileFinder { ) { match event { Event::Selected(tree_id, path) => { - workspace_view.open_entry((*tree_id, path.clone()), ctx); + workspace_view + .open_entry((*tree_id, path.clone()), ctx) + .map(|d| d.detach()); workspace_view.dismiss_modal(ctx); } Event::Dismissed => { diff --git a/zed/src/workspace/workspace_view.rs b/zed/src/workspace/workspace_view.rs index 0fadb7b453f9c7907443642e79b8b3370a29bbd8..6815a1e2c95778da781e5a671d802489dbc4e593 100644 --- a/zed/src/workspace/workspace_view.rs +++ b/zed/src/workspace/workspace_view.rs @@ -3,7 +3,8 @@ use crate::{settings::Settings, watch}; use futures_core::{future::LocalBoxFuture, Future}; use gpui::{ color::rgbu, elements::*, json::to_string_pretty, keymap::Binding, AnyViewHandle, AppContext, - ClipboardItem, Entity, ModelHandle, MutableAppContext, View, ViewContext, ViewHandle, + ClipboardItem, Entity, EntityTask, ModelHandle, MutableAppContext, View, ViewContext, + ViewHandle, }; use log::error; use std::{ @@ -223,11 +224,12 @@ impl WorkspaceView { } } + #[must_use] pub fn open_entry( &mut self, entry: (usize, Arc), ctx: &mut ViewContext, - ) -> Option> { + ) -> Option> { if self.loading_entries.contains(&entry) { return None; }