Start on `Project::definition` that only works locally (for now)

Antonio Scandurra created

Change summary

crates/project/src/project.rs     | 184 +++++++++++++++++++++++++++++++-
crates/project/src/worktree.rs    |   2 
crates/workspace/src/workspace.rs |   2 
3 files changed, 176 insertions(+), 12 deletions(-)

Detailed changes

crates/project/src/project.rs 🔗

@@ -11,11 +11,14 @@ use fuzzy::{PathMatch, PathMatchCandidate, PathMatchCandidateSet};
 use gpui::{
     AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task,
 };
-use language::{Buffer, DiagnosticEntry, Language, LanguageRegistry};
+use language::{
+    Bias, Buffer, DiagnosticEntry, File as _, Language, LanguageRegistry, ToOffset, ToPointUtf16,
+};
 use lsp::{DiagnosticSeverity, LanguageServer};
 use postage::{prelude::Stream, watch};
 use smol::block_on;
 use std::{
+    ops::Range,
     path::{Path, PathBuf},
     sync::{atomic::AtomicBool, Arc},
 };
@@ -83,6 +86,13 @@ pub struct DiagnosticSummary {
     pub hint_count: usize,
 }
 
+#[derive(Debug)]
+pub struct Definition {
+    pub source_range: Option<Range<language::Anchor>>,
+    pub target_buffer: ModelHandle<Buffer>,
+    pub target_range: Range<language::Anchor>,
+}
+
 impl DiagnosticSummary {
     fn new<'a, T: 'a>(diagnostics: impl IntoIterator<Item = &'a DiagnosticEntry<T>>) -> Self {
         let mut this = Self {
@@ -487,7 +497,7 @@ impl Project {
         abs_path: PathBuf,
         cx: &mut ModelContext<Project>,
     ) -> Task<Result<()>> {
-        let worktree_task = self.worktree_for_abs_path(&abs_path, cx);
+        let worktree_task = self.find_or_create_worktree_for_abs_path(&abs_path, cx);
         cx.spawn(|this, mut cx| async move {
             let (worktree, path) = worktree_task.await?;
             worktree
@@ -691,26 +701,180 @@ impl Project {
         Ok(())
     }
 
-    pub fn worktree_for_abs_path(
+    pub fn definition<T: ToOffset>(
+        &self,
+        source_buffer_handle: &ModelHandle<Buffer>,
+        position: T,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<Vec<Definition>>> {
+        let source_buffer_handle = source_buffer_handle.clone();
+        let buffer = source_buffer_handle.read(cx);
+        let worktree;
+        let buffer_abs_path;
+        if let Some(file) = File::from_dyn(buffer.file()) {
+            worktree = file.worktree.clone();
+            buffer_abs_path = file.abs_path();
+        } else {
+            return Task::ready(Err(anyhow!("buffer does not belong to any worktree")));
+        };
+
+        if worktree.read(cx).as_local().is_some() {
+            let point = buffer.offset_to_point_utf16(position.to_offset(buffer));
+            let buffer_abs_path = buffer_abs_path.unwrap();
+            let lang_name;
+            let lang_server;
+            if let Some(lang) = buffer.language() {
+                lang_name = lang.name().to_string();
+                if let Some(server) = self
+                    .language_servers
+                    .get(&(worktree.read(cx).id(), lang_name.clone()))
+                {
+                    lang_server = server.clone();
+                } else {
+                    return Task::ready(Err(anyhow!("buffer does not have a language server")));
+                };
+            } else {
+                return Task::ready(Err(anyhow!("buffer does not have a language")));
+            }
+
+            cx.spawn(|this, mut cx| async move {
+                let response = lang_server
+                    .request::<lsp::request::GotoDefinition>(lsp::GotoDefinitionParams {
+                        text_document_position_params: lsp::TextDocumentPositionParams {
+                            text_document: lsp::TextDocumentIdentifier::new(
+                                lsp::Url::from_file_path(&buffer_abs_path).unwrap(),
+                            ),
+                            position: lsp::Position::new(point.row, point.column),
+                        },
+                        work_done_progress_params: Default::default(),
+                        partial_result_params: Default::default(),
+                    })
+                    .await?;
+
+                let mut definitions = Vec::new();
+                if let Some(response) = response {
+                    let mut unresolved_locations = Vec::new();
+                    match response {
+                        lsp::GotoDefinitionResponse::Scalar(loc) => {
+                            unresolved_locations.push((None, loc.uri, loc.range));
+                        }
+                        lsp::GotoDefinitionResponse::Array(locs) => {
+                            unresolved_locations
+                                .extend(locs.into_iter().map(|l| (None, l.uri, l.range)));
+                        }
+                        lsp::GotoDefinitionResponse::Link(links) => {
+                            unresolved_locations.extend(links.into_iter().map(|l| {
+                                (
+                                    l.origin_selection_range,
+                                    l.target_uri,
+                                    l.target_selection_range,
+                                )
+                            }));
+                        }
+                    }
+
+                    for (source_range, target_uri, target_range) in unresolved_locations {
+                        let abs_path = target_uri
+                            .to_file_path()
+                            .map_err(|_| anyhow!("invalid target path"))?;
+
+                        let (worktree, relative_path) = if let Some(result) = this
+                            .read_with(&cx, |this, cx| {
+                                this.find_worktree_for_abs_path(&abs_path, cx)
+                            }) {
+                            result
+                        } else {
+                            let (worktree, relative_path) = this
+                                .update(&mut cx, |this, cx| {
+                                    this.create_worktree_for_abs_path(&abs_path, cx)
+                                })
+                                .await?;
+                            this.update(&mut cx, |this, cx| {
+                                this.language_servers.insert(
+                                    (worktree.read(cx).id(), lang_name.clone()),
+                                    lang_server.clone(),
+                                );
+                            });
+                            (worktree, relative_path)
+                        };
+
+                        let project_path = ProjectPath {
+                            worktree_id: worktree.read_with(&cx, |worktree, _| worktree.id()),
+                            path: relative_path.into(),
+                        };
+                        let target_buffer_handle = this
+                            .update(&mut cx, |this, cx| this.open_buffer(project_path, cx))
+                            .await?;
+                        cx.read(|cx| {
+                            let source_buffer = source_buffer_handle.read(cx);
+                            let target_buffer = target_buffer_handle.read(cx);
+                            let source_range = source_range.map(|range| {
+                                let start = source_buffer
+                                    .clip_point_utf16(range.start.to_point_utf16(), Bias::Left);
+                                let end = source_buffer
+                                    .clip_point_utf16(range.end.to_point_utf16(), Bias::Left);
+                                source_buffer.anchor_after(start)..source_buffer.anchor_before(end)
+                            });
+                            let target_start = target_buffer
+                                .clip_point_utf16(target_range.start.to_point_utf16(), Bias::Left);
+                            let target_end = target_buffer
+                                .clip_point_utf16(target_range.end.to_point_utf16(), Bias::Left);
+                            definitions.push(Definition {
+                                source_range,
+                                target_buffer: target_buffer_handle,
+                                target_range: target_buffer.anchor_after(target_start)
+                                    ..target_buffer.anchor_before(target_end),
+                            });
+                        });
+                    }
+                }
+
+                Ok(definitions)
+            })
+        } else {
+            todo!()
+        }
+    }
+
+    pub fn find_or_create_worktree_for_abs_path(
+        &self,
+        abs_path: &Path,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Result<(ModelHandle<Worktree>, PathBuf)>> {
+        if let Some((tree, relative_path)) = self.find_worktree_for_abs_path(abs_path, cx) {
+            Task::ready(Ok((tree.clone(), relative_path.into())))
+        } else {
+            self.create_worktree_for_abs_path(abs_path, cx)
+        }
+    }
+
+    fn create_worktree_for_abs_path(
         &self,
         abs_path: &Path,
         cx: &mut ModelContext<Self>,
     ) -> Task<Result<(ModelHandle<Worktree>, PathBuf)>> {
+        let worktree = self.add_local_worktree(abs_path, cx);
+        cx.background().spawn(async move {
+            let worktree = worktree.await?;
+            Ok((worktree, PathBuf::new()))
+        })
+    }
+
+    fn find_worktree_for_abs_path(
+        &self,
+        abs_path: &Path,
+        cx: &AppContext,
+    ) -> Option<(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())));
+                return Some((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()))
-        })
+        None
     }
 
     pub fn is_shared(&self) -> bool {

crates/project/src/worktree.rs 🔗

@@ -1947,7 +1947,7 @@ impl fmt::Debug for Snapshot {
 #[derive(Clone, PartialEq)]
 pub struct File {
     entry_id: Option<usize>,
-    worktree: ModelHandle<Worktree>,
+    pub worktree: ModelHandle<Worktree>,
     worktree_path: Arc<Path>,
     pub path: Arc<Path>,
     pub mtime: SystemTime,

crates/workspace/src/workspace.rs 🔗

@@ -684,7 +684,7 @@ impl Workspace {
         cx: &mut ViewContext<Self>,
     ) -> Task<Result<ProjectPath>> {
         let entry = self.project().update(cx, |project, cx| {
-            project.worktree_for_abs_path(abs_path, cx)
+            project.find_or_create_worktree_for_abs_path(abs_path, cx)
         });
         cx.spawn(|_, cx| async move {
             let (worktree, path) = entry.await?;