Start work on opening files

Max Brunsfeld created

Change summary

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(-)

Detailed changes

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<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())
     }

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()
         }

zed/src/workspace/mod.rs 🔗

@@ -52,7 +52,7 @@ 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(&params.paths, ctx.as_ref()) {
-                    view.open_paths(&params.paths, ctx.as_mut());
+                    view.open_paths(&params.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(&params.paths, ctx);
+        view
+    });
 }
 
 fn quit(_: &(), app: &mut MutableAppContext) {

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<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));

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<Self>) {
+        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<V, F>(&mut self, ctx: &mut ViewContext<Self>, 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::<HashSet<_>>();
+                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 {

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<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();
@@ -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<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
     }