diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 5eec2f9b9e..3de0d564df 100644 --- a/crates/project/src/project.rs +++ b/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>, + pub target_buffer: ModelHandle, + pub target_range: Range, +} + impl DiagnosticSummary { fn new<'a, T: 'a>(diagnostics: impl IntoIterator>) -> Self { let mut this = Self { @@ -487,7 +497,7 @@ impl Project { abs_path: PathBuf, cx: &mut ModelContext, ) -> Task> { - 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( + &self, + source_buffer_handle: &ModelHandle, + position: T, + cx: &mut ModelContext, + ) -> Task>> { + 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::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, ) -> Task, 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, + ) -> Task, 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, 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 { diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 9c322f267e..d734a62bc3 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -1947,7 +1947,7 @@ impl fmt::Debug for Snapshot { #[derive(Clone, PartialEq)] pub struct File { entry_id: Option, - worktree: ModelHandle, + pub worktree: ModelHandle, worktree_path: Arc, pub path: Arc, pub mtime: SystemTime, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 242fbcf560..e865c7f2b1 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -684,7 +684,7 @@ impl Workspace { cx: &mut ViewContext, ) -> Task> { 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?;