Route `save_as` via the `Project`

Antonio Scandurra and Max Brunsfeld created

Co-Authored-By: Max Brunsfeld <max@zed.dev>

Change summary

crates/diagnostics/src/diagnostics.rs |  6 +-
crates/editor/src/items.rs            | 46 +++--------------
crates/project/src/project.rs         | 47 +++++++++++++++++
crates/project/src/worktree.rs        | 48 +++++++++++++++---
crates/workspace/src/workspace.rs     | 74 ++++------------------------
5 files changed, 108 insertions(+), 113 deletions(-)

Detailed changes

crates/diagnostics/src/diagnostics.rs 🔗

@@ -15,7 +15,7 @@ use gpui::{
 use language::{Bias, Buffer, Diagnostic, DiagnosticEntry, Point, Selection, SelectionGoal};
 use postage::watch;
 use project::{Project, ProjectPath, WorktreeId};
-use std::{cmp::Ordering, mem, ops::Range, rc::Rc, sync::Arc};
+use std::{cmp::Ordering, mem, ops::Range, path::PathBuf, rc::Rc, sync::Arc};
 use util::TryFutureExt;
 use workspace::{NavHistory, Workspace};
 
@@ -570,8 +570,8 @@ impl workspace::ItemView for ProjectDiagnosticsEditor {
 
     fn save_as(
         &mut self,
-        _: ModelHandle<project::Worktree>,
-        _: &std::path::Path,
+        _: ModelHandle<Project>,
+        _: PathBuf,
         _: &mut ViewContext<Self>,
     ) -> Task<Result<()>> {
         unreachable!()

crates/editor/src/items.rs 🔗

@@ -4,12 +4,12 @@ use gpui::{
     elements::*, AppContext, Entity, ModelContext, ModelHandle, MutableAppContext, RenderContext,
     Subscription, Task, View, ViewContext, ViewHandle, WeakModelHandle,
 };
-use language::{Bias, Buffer, Diagnostic, File as _};
+use language::{Bias, Buffer, Diagnostic};
 use postage::watch;
-use project::{File, ProjectEntry, ProjectPath, Worktree};
-use std::fmt::Write;
-use std::path::Path;
+use project::worktree::File;
+use project::{Project, ProjectEntry, ProjectPath, Worktree};
 use std::rc::Rc;
+use std::{fmt::Write, path::PathBuf};
 use text::{Point, Selection};
 use util::TryFutureExt;
 use workspace::{
@@ -182,8 +182,8 @@ impl ItemView for Editor {
 
     fn save_as(
         &mut self,
-        worktree: ModelHandle<Worktree>,
-        path: &Path,
+        project: ModelHandle<Project>,
+        abs_path: PathBuf,
         cx: &mut ViewContext<Self>,
     ) -> Task<Result<()>> {
         let buffer = self
@@ -193,38 +193,8 @@ impl ItemView for Editor {
             .expect("cannot call save_as on an excerpt list")
             .clone();
 
-        buffer.update(cx, |buffer, cx| {
-            let handle = cx.handle();
-            let text = buffer.as_rope().clone();
-            let version = buffer.version();
-
-            let save_as = worktree.update(cx, |worktree, cx| {
-                worktree
-                    .as_local_mut()
-                    .unwrap()
-                    .save_buffer_as(handle, path, text, cx)
-            });
-
-            cx.spawn(|buffer, mut cx| async move {
-                save_as.await.map(|new_file| {
-                    let (language, language_server) = worktree.update(&mut cx, |worktree, cx| {
-                        let worktree = worktree.as_local_mut().unwrap();
-                        let language = worktree
-                            .language_registry()
-                            .select_language(new_file.full_path())
-                            .cloned();
-                        let language_server = language
-                            .as_ref()
-                            .and_then(|language| worktree.register_language(language, cx));
-                        (language, language_server.clone())
-                    });
-
-                    buffer.update(&mut cx, |buffer, cx| {
-                        buffer.did_save(version, new_file.mtime, Some(Box::new(new_file)), cx);
-                        buffer.set_language(language, language_server, cx);
-                    });
-                })
-            })
+        project.update(cx, |project, cx| {
+            project.save_buffer_as(buffer, &abs_path, cx)
         })
     }
 

crates/project/src/project.rs 🔗

@@ -15,7 +15,7 @@ use language::{Buffer, DiagnosticEntry, LanguageRegistry};
 use lsp::DiagnosticSeverity;
 use postage::{prelude::Stream, watch};
 use std::{
-    path::Path,
+    path::{Path, PathBuf},
     sync::{atomic::AtomicBool, Arc},
 };
 use util::TryFutureExt as _;
@@ -468,6 +468,49 @@ impl Project {
         }
     }
 
+    pub fn save_buffer_as(
+        &self,
+        buffer: ModelHandle<Buffer>,
+        abs_path: &Path,
+        cx: &mut ModelContext<Project>,
+    ) -> Task<Result<()>> {
+        let result = self.worktree_for_abs_path(abs_path, cx);
+        cx.spawn(|_, mut cx| async move {
+            let (worktree, path) = result.await?;
+            worktree
+                .update(&mut cx, |worktree, cx| {
+                    worktree
+                        .as_local()
+                        .unwrap()
+                        .save_buffer_as(buffer.clone(), path, cx)
+                })
+                .await?;
+            Ok(())
+        })
+    }
+
+    pub fn worktree_for_abs_path(
+        &self,
+        abs_path: &Path,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<(ModelHandle<Worktree>, PathBuf)>> {
+        for tree in &self.worktrees {
+            if let Some(relative_path) = tree
+                .read(cx)
+                .as_local()
+                .and_then(|t| abs_path.strip_prefix(t.abs_path()).ok())
+            {
+                return Task::ready(Ok((tree.clone(), relative_path.into())));
+            }
+        }
+
+        let worktree = self.add_local_worktree(abs_path, cx);
+        cx.background().spawn(async move {
+            let worktree = worktree.await?;
+            Ok((worktree, PathBuf::new()))
+        })
+    }
+
     pub fn is_shared(&self) -> bool {
         match &self.client_state {
             ProjectClientState::Local { is_shared, .. } => *is_shared,
@@ -476,7 +519,7 @@ impl Project {
     }
 
     pub fn add_local_worktree(
-        &mut self,
+        &self,
         abs_path: impl AsRef<Path>,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<ModelHandle<Worktree>>> {

crates/project/src/worktree.rs 🔗

@@ -1002,6 +1002,7 @@ pub struct LocalWorktree {
     client: Arc<Client>,
     user_store: ModelHandle<UserStore>,
     fs: Arc<dyn Fs>,
+    languages: Vec<Arc<Language>>,
     language_servers: HashMap<String, Arc<LanguageServer>>,
 }
 
@@ -1109,6 +1110,7 @@ impl LocalWorktree {
                 client,
                 user_store,
                 fs,
+                languages: Default::default(),
                 language_servers: Default::default(),
             };
 
@@ -1153,11 +1155,19 @@ impl LocalWorktree {
         &self.language_registry
     }
 
+    pub fn languages(&self) -> &[Arc<Language>] {
+        &self.languages
+    }
+
     pub fn register_language(
         &mut self,
         language: &Arc<Language>,
         cx: &mut ModelContext<Worktree>,
     ) -> Option<Arc<LanguageServer>> {
+        if !self.languages.iter().any(|l| Arc::ptr_eq(l, language)) {
+            self.languages.push(language.clone());
+        }
+
         if let Some(server) = self.language_servers.get(language.name()) {
             return Some(server.clone());
         }
@@ -1498,26 +1508,48 @@ impl LocalWorktree {
 
     pub fn save_buffer_as(
         &self,
-        buffer: ModelHandle<Buffer>,
+        buffer_handle: ModelHandle<Buffer>,
         path: impl Into<Arc<Path>>,
-        text: Rope,
         cx: &mut ModelContext<Worktree>,
-    ) -> Task<Result<File>> {
+    ) -> Task<Result<()>> {
+        let buffer = buffer_handle.read(cx);
+        let text = buffer.as_rope().clone();
+        let version = buffer.version();
         let save = self.save(path, text, cx);
         cx.spawn(|this, mut cx| async move {
             let entry = save.await?;
-            this.update(&mut cx, |this, cx| {
+            let file = this.update(&mut cx, |this, cx| {
                 let this = this.as_local_mut().unwrap();
-                this.open_buffers.insert(buffer.id(), buffer.downgrade());
-                Ok(File {
+                this.open_buffers
+                    .insert(buffer_handle.id(), buffer_handle.downgrade());
+                File {
                     entry_id: Some(entry.id),
                     worktree: cx.handle(),
                     worktree_path: this.abs_path.clone(),
                     path: entry.path,
                     mtime: entry.mtime,
                     is_local: true,
-                })
-            })
+                }
+            });
+
+            let (language, language_server) = this.update(&mut cx, |worktree, cx| {
+                let worktree = worktree.as_local_mut().unwrap();
+                let language = worktree
+                    .language_registry()
+                    .select_language(file.full_path())
+                    .cloned();
+                let language_server = language
+                    .as_ref()
+                    .and_then(|language| worktree.register_language(language, cx));
+                (language, language_server.clone())
+            });
+
+            buffer_handle.update(&mut cx, |buffer, cx| {
+                buffer.did_save(version, file.mtime, Some(Box::new(file)), cx);
+                buffer.set_language(language, language_server, cx);
+            });
+
+            Ok(())
         })
     }
 

crates/workspace/src/workspace.rs 🔗

@@ -168,8 +168,8 @@ pub trait ItemView: View {
     fn can_save_as(&self, cx: &AppContext) -> bool;
     fn save_as(
         &mut self,
-        worktree: ModelHandle<Worktree>,
-        path: &Path,
+        project: ModelHandle<Project>,
+        abs_path: PathBuf,
         cx: &mut ViewContext<Self>,
     ) -> Task<anyhow::Result<()>>;
     fn should_activate_item_on_event(_: &Self::Event) -> bool {
@@ -221,8 +221,8 @@ pub trait ItemViewHandle {
     fn save(&self, cx: &mut MutableAppContext) -> Result<Task<Result<()>>>;
     fn save_as(
         &self,
-        worktree: ModelHandle<Worktree>,
-        path: &Path,
+        project: ModelHandle<Project>,
+        abs_path: PathBuf,
         cx: &mut MutableAppContext,
     ) -> Task<anyhow::Result<()>>;
 }
@@ -379,11 +379,11 @@ impl<T: ItemView> ItemViewHandle for ViewHandle<T> {
 
     fn save_as(
         &self,
-        worktree: ModelHandle<Worktree>,
-        path: &Path,
+        project: ModelHandle<Project>,
+        abs_path: PathBuf,
         cx: &mut MutableAppContext,
     ) -> Task<anyhow::Result<()>> {
-        self.update(cx, |item, cx| item.save_as(worktree, path, cx))
+        self.update(cx, |item, cx| item.save_as(project, abs_path, cx))
     }
 
     fn is_dirty(&self, cx: &AppContext) -> bool {
@@ -674,44 +674,14 @@ impl Workspace {
         })
     }
 
-    fn worktree_for_abs_path(
-        &self,
-        abs_path: &Path,
-        cx: &mut ViewContext<Self>,
-    ) -> Task<Result<(ModelHandle<Worktree>, PathBuf)>> {
-        let abs_path: Arc<Path> = Arc::from(abs_path);
-        cx.spawn(|this, mut cx| async move {
-            let mut entry_id = None;
-            this.read_with(&cx, |this, cx| {
-                for tree in this.worktrees(cx) {
-                    if let Some(relative_path) = tree
-                        .read(cx)
-                        .as_local()
-                        .and_then(|t| abs_path.strip_prefix(t.abs_path()).ok())
-                    {
-                        entry_id = Some((tree.clone(), relative_path.into()));
-                        break;
-                    }
-                }
-            });
-
-            if let Some(entry_id) = entry_id {
-                Ok(entry_id)
-            } else {
-                let worktree = this
-                    .update(&mut cx, |this, cx| this.add_worktree(&abs_path, cx))
-                    .await?;
-                Ok((worktree, PathBuf::new()))
-            }
-        })
-    }
-
     fn project_path_for_path(
         &self,
         abs_path: &Path,
         cx: &mut ViewContext<Self>,
     ) -> Task<Result<ProjectPath>> {
-        let entry = self.worktree_for_abs_path(abs_path, cx);
+        let entry = self.project().update(cx, |project, cx| {
+            project.worktree_for_abs_path(abs_path, cx)
+        });
         cx.spawn(|_, cx| async move {
             let (worktree, path) = entry.await?;
             Ok(ProjectPath {
@@ -880,28 +850,8 @@ impl Workspace {
                     .to_path_buf();
                 cx.prompt_for_new_path(&start_abs_path, move |abs_path, cx| {
                     if let Some(abs_path) = abs_path {
-                        cx.spawn(|mut cx| async move {
-                            let result = match handle
-                                .update(&mut cx, |this, cx| {
-                                    this.worktree_for_abs_path(&abs_path, cx)
-                                })
-                                .await
-                            {
-                                Ok((worktree, path)) => {
-                                    handle
-                                        .update(&mut cx, |_, cx| {
-                                            item.save_as(worktree, &path, cx.as_mut())
-                                        })
-                                        .await
-                                }
-                                Err(error) => Err(error),
-                            };
-
-                            if let Err(error) = result {
-                                error!("failed to save item: {:?}, ", error);
-                            }
-                        })
-                        .detach()
+                        let project = handle.read(cx).project().clone();
+                        cx.update(|cx| item.save_as(project, abs_path, cx).detach_and_log_err(cx));
                     }
                 });
             }