Introduce LocalFile trait

Nathan Sobo created

If you want to call `abs_path` or `load`, the file needs to be local. You call `as_local` which returns `Option<dyn LocalFile>` with those local-only methods. I think this makes it more explicit what works only locally vs everywhere.

Change summary

crates/language/src/buffer.rs  | 36 +++++++++++++++++------------
crates/project/src/project.rs  |  6 ++--
crates/project/src/worktree.rs | 44 ++++++++++++++++++++++-------------
3 files changed, 51 insertions(+), 35 deletions(-)

Detailed changes

crates/language/src/buffer.rs 🔗

@@ -156,14 +156,13 @@ pub enum Event {
 }
 
 pub trait File {
+    fn as_local(&self) -> Option<&dyn LocalFile>;
+
     fn mtime(&self) -> SystemTime;
 
     /// Returns the path of this file relative to the worktree's root directory.
     fn path(&self) -> &Arc<Path>;
 
-    /// Returns the absolute path of this file.
-    fn abs_path(&self, cx: &AppContext) -> Option<PathBuf>;
-
     /// Returns the path of this file relative to the worktree's parent directory (this means it
     /// includes the name of the worktree's root folder).
     fn full_path(&self, cx: &AppContext) -> PathBuf;
@@ -182,8 +181,6 @@ pub trait File {
         cx: &mut MutableAppContext,
     ) -> Task<Result<(clock::Global, SystemTime)>>;
 
-    fn load_local(&self, cx: &AppContext) -> Option<Task<Result<String>>>;
-
     fn format_remote(&self, buffer_id: u64, cx: &mut MutableAppContext)
         -> Option<Task<Result<()>>>;
 
@@ -194,6 +191,13 @@ pub trait File {
     fn as_any(&self) -> &dyn Any;
 }
 
+pub trait LocalFile: File {
+    /// Returns the absolute path of this file.
+    fn abs_path(&self, cx: &AppContext) -> PathBuf;
+
+    fn load(&self, cx: &AppContext) -> Task<Result<String>>;
+}
+
 pub(crate) struct QueryCursorHandle(Option<QueryCursor>);
 
 #[derive(Clone)]
@@ -457,7 +461,7 @@ impl Buffer {
 
         if let Some(LanguageServerState { server, .. }) = self.language_server.as_ref() {
             let server = server.clone();
-            let abs_path = file.abs_path(cx).unwrap();
+            let abs_path = file.as_local().unwrap().abs_path(cx);
             let version = self.version();
             cx.spawn(|this, mut cx| async move {
                 let edits = server
@@ -634,7 +638,11 @@ impl Buffer {
         if let Some(new_file) = new_file {
             self.file = Some(new_file);
         }
-        if let Some(state) = &self.language_server {
+        if let Some((state, local_file)) = &self
+            .language_server
+            .as_ref()
+            .zip(self.file.as_ref().and_then(|f| f.as_local()))
+        {
             cx.background()
                 .spawn(
                     state
@@ -642,10 +650,7 @@ impl Buffer {
                         .notify::<lsp::notification::DidSaveTextDocument>(
                             lsp::DidSaveTextDocumentParams {
                                 text_document: lsp::TextDocumentIdentifier {
-                                    uri: lsp::Url::from_file_path(
-                                        self.file.as_ref().unwrap().abs_path(cx).unwrap(),
-                                    )
-                                    .unwrap(),
+                                    uri: lsp::Url::from_file_path(local_file.abs_path(cx)).unwrap(),
                                 },
                                 text: None,
                             },
@@ -685,7 +690,9 @@ impl Buffer {
                     task = Some(cx.spawn(|this, mut cx| {
                         async move {
                             let new_text = this.read_with(&cx, |this, cx| {
-                                this.file.as_ref().and_then(|file| file.load_local(cx))
+                                this.file
+                                    .as_ref()
+                                    .and_then(|file| file.as_local().map(|f| f.load(cx)))
                             });
                             if let Some(new_text) = new_text {
                                 let new_text = new_text.await?;
@@ -1235,9 +1242,8 @@ impl Buffer {
         let abs_path = self
             .file
             .as_ref()
-            .map_or(Path::new("/").to_path_buf(), |file| {
-                file.abs_path(cx).unwrap()
-            });
+            .and_then(|f| f.as_local())
+            .map_or(Path::new("/").to_path_buf(), |file| file.abs_path(cx));
 
         let version = post_inc(&mut language_server.next_version);
         let snapshot = LanguageServerSnapshot {

crates/project/src/project.rs 🔗

@@ -938,7 +938,7 @@ impl Project {
         let buffer_abs_path;
         if let Some(file) = File::from_dyn(buffer.file()) {
             worktree = file.worktree.clone();
-            buffer_abs_path = file.abs_path(cx);
+            buffer_abs_path = file.as_local().map(|f| f.abs_path(cx));
         } else {
             return Task::ready(Err(anyhow!("buffer does not belong to any worktree")));
         };
@@ -2181,8 +2181,8 @@ mod tests {
         cx.update(|cx| {
             let target_buffer = definition.target_buffer.read(cx);
             assert_eq!(
-                target_buffer.file().unwrap().abs_path(cx),
-                Some(dir.path().join("a.rs"))
+                target_buffer.file().unwrap().as_local().unwrap().abs_path(cx),
+                dir.path().join("a.rs")
             );
             assert_eq!(definition.target_range.to_offset(target_buffer), 9..10);
             assert_eq!(

crates/project/src/worktree.rs 🔗

@@ -1366,6 +1366,14 @@ pub struct File {
 }
 
 impl language::File for File {
+    fn as_local(&self) -> Option<&dyn language::LocalFile> {
+        if self.is_local {
+            Some(self)
+        } else {
+            None
+        }
+    }
+
     fn mtime(&self) -> SystemTime {
         self.mtime
     }
@@ -1374,13 +1382,6 @@ impl language::File for File {
         &self.path
     }
 
-    fn abs_path(&self, cx: &AppContext) -> Option<PathBuf> {
-        self.worktree
-            .read(cx)
-            .as_local()
-            .map(|local_worktree| local_worktree.abs_path.join(&self.path))
-    }
-
     fn full_path(&self, cx: &AppContext) -> PathBuf {
         let mut full_path = PathBuf::new();
         full_path.push(self.worktree.read(cx).root_name());
@@ -1448,16 +1449,6 @@ impl language::File for File {
         })
     }
 
-    fn load_local(&self, cx: &AppContext) -> Option<Task<Result<String>>> {
-        let worktree = self.worktree.read(cx).as_local()?;
-        let abs_path = worktree.absolutize(&self.path);
-        let fs = worktree.fs.clone();
-        Some(
-            cx.background()
-                .spawn(async move { fs.load(&abs_path).await }),
-        )
-    }
-
     fn format_remote(
         &self,
         buffer_id: u64,
@@ -1510,6 +1501,25 @@ impl language::File for File {
     }
 }
 
+impl language::LocalFile for File {
+    fn abs_path(&self, cx: &AppContext) -> PathBuf {
+        self.worktree
+            .read(cx)
+            .as_local()
+            .unwrap()
+            .abs_path
+            .join(&self.path)
+    }
+
+    fn load(&self, cx: &AppContext) -> Task<Result<String>> {
+        let worktree = self.worktree.read(cx).as_local().unwrap();
+        let abs_path = worktree.absolutize(&self.path);
+        let fs = worktree.fs.clone();
+        cx.background()
+            .spawn(async move { fs.load(&abs_path).await })
+    }
+}
+
 impl File {
     pub fn from_dyn(file: Option<&dyn language::File>) -> Option<&Self> {
         file.and_then(|f| f.as_any().downcast_ref())