Merge pull request #31 from zed-industries/open-files
Allow opening files from the CLI and from the File > Open menu
This commit is contained in:
@@ -1717,6 +1717,10 @@ impl<'a, T: View> ViewContext<'a, T> {
|
||||
self.window_id
|
||||
}
|
||||
|
||||
pub fn foreground(&self) -> &Rc<executor::Foreground> {
|
||||
self.app.foreground_executor()
|
||||
}
|
||||
|
||||
pub fn background_executor(&self) -> &Arc<executor::Background> {
|
||||
&self.app.ctx.background
|
||||
}
|
||||
|
||||
@@ -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<OsString> {
|
||||
self.file.as_ref().and_then(|file| file.file_name(ctx))
|
||||
}
|
||||
|
||||
pub fn path(&self) -> Option<Arc<Path>> {
|
||||
self.file.as_ref().map(|file| file.path())
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
+185
-112
@@ -33,8 +33,7 @@ pub struct FileFinder {
|
||||
latest_search_did_cancel: bool,
|
||||
latest_search_query: String,
|
||||
matches: Vec<PathMatch>,
|
||||
include_root_name: bool,
|
||||
selected: Option<Arc<Path>>,
|
||||
selected: Option<(usize, Arc<Path>)>,
|
||||
cancel_flag: Arc<AtomicBool>,
|
||||
list_state: UniformListState,
|
||||
}
|
||||
@@ -147,101 +146,110 @@ impl FileFinder {
|
||||
index: usize,
|
||||
app: &AppContext,
|
||||
) -> Option<ElementBox> {
|
||||
let tree_id = path_match.tree_id;
|
||||
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_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_child(
|
||||
Label::new(
|
||||
full_path,
|
||||
settings.ui_font_family,
|
||||
settings.ui_font_size,
|
||||
)
|
||||
.with_highlights(highlight_color, bold, full_path_positions)
|
||||
.boxed(),
|
||||
)
|
||||
.boxed(),
|
||||
)
|
||||
.boxed(),
|
||||
)
|
||||
.boxed(),
|
||||
)
|
||||
.with_uniform_padding(6.0);
|
||||
|
||||
self.worktree(tree_id, app).map(|tree| {
|
||||
let prefix = if self.include_root_name {
|
||||
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));
|
||||
}
|
||||
|
||||
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<usize>, String, Vec<usize>)> {
|
||||
self.worktree(path_match.tree_id, app).map(|tree| {
|
||||
let prefix = if path_match.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 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.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(),
|
||||
)
|
||||
.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_highlights(highlight_color, bold, file_name_positions)
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(
|
||||
Label::new(
|
||||
full_path,
|
||||
settings.ui_font_family,
|
||||
settings.ui_font_size,
|
||||
)
|
||||
.with_highlights(highlight_color, bold, path_positions)
|
||||
.boxed(),
|
||||
)
|
||||
.boxed(),
|
||||
)
|
||||
.boxed(),
|
||||
)
|
||||
.boxed(),
|
||||
)
|
||||
.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)));
|
||||
}
|
||||
|
||||
if index == selected_index {
|
||||
container = container.with_background_color(ColorU::from_u32(0xdbdbdcff));
|
||||
}
|
||||
|
||||
EventHandler::new(container.boxed())
|
||||
.on_mouse_down(move |ctx| {
|
||||
ctx.dispatch_action("file_finder:select", (tree_id, path.clone()));
|
||||
true
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -267,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 => {
|
||||
@@ -298,7 +308,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 +344,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 +358,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 +369,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 +416,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 +424,7 @@ impl FileFinder {
|
||||
|
||||
fn update_matches(
|
||||
&mut self,
|
||||
(search_id, include_root_name, did_cancel, query, matches): (
|
||||
usize,
|
||||
bool,
|
||||
bool,
|
||||
String,
|
||||
Vec<PathMatch>,
|
||||
),
|
||||
(search_id, did_cancel, query, matches): (usize, bool, String, Vec<PathMatch>),
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if search_id >= self.latest_search_id {
|
||||
@@ -429,7 +436,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 +460,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 +562,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 +574,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 +585,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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,8 @@ fn open_paths(params: &OpenParams, app: &mut MutableAppContext) {
|
||||
if let Some(handle) = app.root_view::<WorkspaceView>(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());
|
||||
let open_paths = view.open_paths(¶ms.paths, ctx);
|
||||
ctx.foreground().spawn(open_paths).detach();
|
||||
log::info!("open paths on existing workspace");
|
||||
true
|
||||
} else {
|
||||
@@ -67,8 +68,13 @@ 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);
|
||||
let open_paths = view.open_paths(¶ms.paths, ctx);
|
||||
ctx.foreground().spawn(open_paths).detach();
|
||||
view
|
||||
});
|
||||
}
|
||||
|
||||
fn quit(_: &(), app: &mut MutableAppContext) {
|
||||
|
||||
@@ -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<Self>) {
|
||||
for path in paths.iter().cloned() {
|
||||
self.open_path(path, ctx);
|
||||
}
|
||||
pub fn open_paths(
|
||||
&mut self,
|
||||
paths: &[PathBuf],
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) -> Vec<(usize, Arc<Path>)> {
|
||||
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<Self>) {
|
||||
fn open_path(&mut self, path: PathBuf, ctx: &mut ModelContext<Self>) -> (usize, Arc<Path>) {
|
||||
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));
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
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,
|
||||
ClipboardItem, Entity, EntityTask, ModelHandle, MutableAppContext, View, ViewContext,
|
||||
ViewHandle,
|
||||
};
|
||||
use log::error;
|
||||
use std::{
|
||||
@@ -161,9 +162,39 @@ 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<Self>,
|
||||
) -> impl Future<Output = ()> {
|
||||
let entries = self
|
||||
.workspace
|
||||
.update(ctx, |workspace, ctx| workspace.open_paths(paths, ctx));
|
||||
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::<Vec<_>>();
|
||||
async move {
|
||||
for task in tasks {
|
||||
if let Some(task) = task.await {
|
||||
task.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_modal<V, F>(&mut self, ctx: &mut ViewContext<Self>, add_view: F)
|
||||
@@ -193,16 +224,21 @@ impl WorkspaceView {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn open_entry(&mut self, entry: (usize, Arc<Path>), ctx: &mut ViewContext<Self>) {
|
||||
#[must_use]
|
||||
pub fn open_entry(
|
||||
&mut self,
|
||||
entry: (usize, Arc<Path>),
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) -> Option<EntityTask<()>> {
|
||||
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());
|
||||
@@ -210,10 +246,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) => {
|
||||
@@ -224,8 +263,7 @@ impl WorkspaceView {
|
||||
error!("{}", error);
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -382,6 +420,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() {
|
||||
@@ -408,13 +447,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);
|
||||
@@ -422,25 +471,107 @@ 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())
|
||||
// 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| {
|
||||
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. 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[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)
|
||||
})
|
||||
})
|
||||
.await;
|
||||
app.read(|ctx| {
|
||||
assert_eq!(pane.read(ctx).items().len(), 2);
|
||||
workspace_view
|
||||
.read(ctx)
|
||||
.active_pane()
|
||||
.read(ctx)
|
||||
.active_item()
|
||||
.unwrap()
|
||||
.title(ctx)
|
||||
== "a.txt"
|
||||
});
|
||||
|
||||
// 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);
|
||||
// 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)
|
||||
})
|
||||
})
|
||||
.await;
|
||||
app.update(|ctx| {
|
||||
let worktree_roots = workspace
|
||||
.read(ctx)
|
||||
.worktrees()
|
||||
.iter()
|
||||
.map(|w| w.read(ctx).abs_path())
|
||||
.collect::<HashSet<_>>();
|
||||
assert_eq!(
|
||||
worktree_roots,
|
||||
vec![dir1.path(), &dir2.path().join("b.txt")]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
);
|
||||
});
|
||||
app.read(|ctx| {
|
||||
workspace_view
|
||||
.read(ctx)
|
||||
.active_pane()
|
||||
.read(ctx)
|
||||
.active_item()
|
||||
.unwrap()
|
||||
.title(ctx)
|
||||
== "b.txt"
|
||||
});
|
||||
pane.condition(&app, |pane, _| pane.items().len() == 3)
|
||||
.await;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -468,15 +599,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| {
|
||||
|
||||
+49
-24
@@ -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},
|
||||
@@ -68,16 +68,13 @@ struct FileHandleState {
|
||||
impl Worktree {
|
||||
pub fn new(path: impl Into<Arc<Path>>, ctx: &mut ModelContext<Self>) -> 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(),
|
||||
};
|
||||
@@ -163,6 +160,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 +173,11 @@ impl Worktree {
|
||||
path: &Path,
|
||||
ctx: &AppContext,
|
||||
) -> impl Future<Output = Result<History>> {
|
||||
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();
|
||||
@@ -261,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
|
||||
}
|
||||
@@ -381,10 +386,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<Path> {
|
||||
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<OsString> {
|
||||
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
|
||||
}
|
||||
@@ -461,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 {
|
||||
@@ -469,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;
|
||||
@@ -611,14 +632,8 @@ impl BackgroundScanner {
|
||||
notify: Sender<ScanState>,
|
||||
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,
|
||||
@@ -679,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<Path> = Arc::from(Path::new(""));
|
||||
@@ -687,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,
|
||||
@@ -1521,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,
|
||||
@@ -1691,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);
|
||||
|
||||
@@ -24,6 +24,7 @@ pub struct PathMatch {
|
||||
pub positions: Vec<usize>,
|
||||
pub tree_id: usize,
|
||||
pub path: Arc<Path>,
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user