diff --git a/crates/editor/src/selections_collection.rs b/crates/editor/src/selections_collection.rs index 35df9c1b53f72..c85e60fdaa92e 100644 --- a/crates/editor/src/selections_collection.rs +++ b/crates/editor/src/selections_collection.rs @@ -109,7 +109,7 @@ impl SelectionsCollection { pub fn all<'a, D>(&self, cx: &AppContext) -> Vec> where - D: 'a + TextDimension + Ord + Sub + std::fmt::Debug, + D: 'a + TextDimension + Ord + Sub, { let disjoint_anchors = &self.disjoint; let mut disjoint = @@ -850,7 +850,7 @@ pub(crate) fn resolve_multiple<'a, D, I>( snapshot: &MultiBufferSnapshot, ) -> impl 'a + Iterator> where - D: TextDimension + Ord + Sub + std::fmt::Debug, + D: TextDimension + Ord + Sub, I: 'a + IntoIterator>, { let (to_summarize, selections) = selections.into_iter().tee(); diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 957f39a51680a..454a7586c8856 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -2963,15 +2963,11 @@ impl Project { buffer: &Model, cx: &mut ModelContext, ) -> Task> { - // TODO: ssh based remoting. - if self.ssh_session.is_some() { - return Task::ready(None); - } - - if self.is_local_or_ssh() { - let expanded = PathBuf::from(shellexpand::tilde(&path).into_owned()); + let path_buf = PathBuf::from(path); + if path_buf.is_absolute() || path.starts_with("~") { + if self.is_local() { + let expanded = PathBuf::from(shellexpand::tilde(&path).into_owned()); - if expanded.is_absolute() { let fs = self.fs.clone(); cx.background_executor().spawn(async move { let path = expanded.as_path(); @@ -2979,16 +2975,24 @@ impl Project { exists.then(|| ResolvedPath::AbsPath(expanded)) }) + } else if let Some(ssh_session) = self.ssh_session.as_ref() { + let request = ssh_session.request(proto::CheckFileExists { + project_id: SSH_PROJECT_ID, + path: path.to_string(), + }); + cx.background_executor().spawn(async move { + let response = request.await.log_err()?; + if response.exists { + Some(ResolvedPath::AbsPath(PathBuf::from(response.path))) + } else { + None + } + }) } else { - self.resolve_path_in_worktrees(expanded, buffer, cx) - } - } else { - let path = PathBuf::from(path); - if path.is_absolute() || path.starts_with("~") { return Task::ready(None); } - - self.resolve_path_in_worktrees(path, buffer, cx) + } else { + self.resolve_path_in_worktrees(path_buf, buffer, cx) } } @@ -3907,17 +3911,7 @@ impl Project { } pub fn worktree_metadata_protos(&self, cx: &AppContext) -> Vec { - self.worktrees(cx) - .map(|worktree| { - let worktree = worktree.read(cx); - proto::WorktreeMetadata { - id: worktree.id().to_proto(), - root_name: worktree.root_name().into(), - visible: worktree.is_visible(), - abs_path: worktree.abs_path().to_string_lossy().into(), - } - }) - .collect() + self.worktree_store.read(cx).worktree_metadata_protos(cx) } fn set_worktrees_from_proto( @@ -3926,10 +3920,9 @@ impl Project { cx: &mut ModelContext, ) -> Result<()> { cx.notify(); - let result = self.worktree_store.update(cx, |worktree_store, cx| { + self.worktree_store.update(cx, |worktree_store, cx| { worktree_store.set_worktrees_from_proto(worktrees, self.replica_id(), cx) - }); - result + }) } fn set_collaborators_from_proto( @@ -4438,6 +4431,22 @@ pub enum ResolvedPath { AbsPath(PathBuf), } +impl ResolvedPath { + pub fn abs_path(&self) -> Option<&Path> { + match self { + Self::AbsPath(path) => Some(path.as_path()), + _ => None, + } + } + + pub fn project_path(&self) -> Option<&ProjectPath> { + match self { + Self::ProjectPath(path) => Some(&path), + _ => None, + } + } +} + impl Item for Buffer { fn try_open( project: &Model, diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index a18bbe8ecf514..475ed139edfb8 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -293,7 +293,10 @@ message Envelope { TryExec try_exec = 252; ReadTextFile read_text_file = 253; - ReadTextFileResponse read_text_file_response = 254; // current max + ReadTextFileResponse read_text_file_response = 254; + + CheckFileExists check_file_exists = 255; + CheckFileExistsResponse check_file_exists_response = 256; // current max } reserved 158 to 161; @@ -2574,3 +2577,13 @@ message TryExec { message TryExecResponse { string text = 1; } + +message CheckFileExists { + uint64 project_id = 1; + string path = 2; +} + +message CheckFileExistsResponse { + bool exists = 1; + string path = 2; +} diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index b5a00d16704c4..4146a47409ad7 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -372,7 +372,9 @@ messages!( (ShellEnvResponse, Foreground), (TryExec, Foreground), (ReadTextFile, Foreground), - (ReadTextFileResponse, Foreground) + (ReadTextFileResponse, Foreground), + (CheckFileExists, Background), + (CheckFileExistsResponse, Background) ); request_messages!( @@ -501,6 +503,7 @@ request_messages!( (ShellEnv, ShellEnvResponse), (ReadTextFile, ReadTextFileResponse), (TryExec, Ack), + (CheckFileExists, CheckFileExistsResponse) ); entity_messages!( @@ -578,7 +581,8 @@ entity_messages!( WhichCommand, ShellEnv, TryExec, - ReadTextFile + ReadTextFile, + CheckFileExists, ); entity_messages!( diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index 948536aa49bfe..0af0d6bb1570d 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -107,6 +107,7 @@ impl HeadlessProject { session.subscribe_to_entity(SSH_PROJECT_ID, &settings_observer); client.add_request_handler(cx.weak_model(), Self::handle_list_remote_directory); + client.add_request_handler(cx.weak_model(), Self::handle_check_file_exists); client.add_model_request_handler(Self::handle_add_worktree); client.add_model_request_handler(Self::handle_open_buffer_by_path); @@ -297,4 +298,20 @@ impl HeadlessProject { } Ok(proto::ListRemoteDirectoryResponse { entries }) } + + pub async fn handle_check_file_exists( + this: Model, + envelope: TypedEnvelope, + cx: AsyncAppContext, + ) -> Result { + let fs = cx.read_model(&this, |this, _| this.fs.clone())?; + let expanded = shellexpand::tilde(&envelope.payload.path).to_string(); + + let exists = fs.is_file(&PathBuf::from(expanded.clone())).await; + + Ok(proto::CheckFileExistsResponse { + exists, + path: expanded, + }) + } } diff --git a/crates/remote_server/src/remote_editing_tests.rs b/crates/remote_server/src/remote_editing_tests.rs index 2fbd86dbe86ca..808567910bf61 100644 --- a/crates/remote_server/src/remote_editing_tests.rs +++ b/crates/remote_server/src/remote_editing_tests.rs @@ -13,7 +13,7 @@ use lsp::{CompletionContext, CompletionResponse, CompletionTriggerKind}; use node_runtime::NodeRuntime; use project::{ search::{SearchQuery, SearchResult}, - Project, + Project, ProjectPath, }; use remote::SshSession; use serde_json::json; @@ -492,6 +492,49 @@ async fn test_remote_reload(cx: &mut TestAppContext, server_cx: &mut TestAppCont }); } +#[gpui::test] +async fn test_remote_resolve_file_path(cx: &mut TestAppContext, server_cx: &mut TestAppContext) { + let (project, _headless, _fs) = init_test(cx, server_cx).await; + let (worktree, _) = project + .update(cx, |project, cx| { + project.find_or_create_worktree("/code/project1", true, cx) + }) + .await + .unwrap(); + + let worktree_id = cx.update(|cx| worktree.read(cx).id()); + + let buffer = project + .update(cx, |project, cx| { + project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx) + }) + .await + .unwrap(); + + let path = project + .update(cx, |project, cx| { + project.resolve_existing_file_path("/code/project1/README.md", &buffer, cx) + }) + .await + .unwrap(); + assert_eq!( + path.abs_path().unwrap().to_string_lossy(), + "/code/project1/README.md" + ); + + let path = project + .update(cx, |project, cx| { + project.resolve_existing_file_path("../README.md", &buffer, cx) + }) + .await + .unwrap(); + + assert_eq!( + path.project_path().unwrap().clone(), + ProjectPath::from((worktree_id, "README.md")) + ); +} + fn init_logger() { if std::env::var("RUST_LOG").is_ok() { env_logger::try_init().ok(); diff --git a/crates/sum_tree/src/cursor.rs b/crates/sum_tree/src/cursor.rs index 6da43a8de5ce3..773e7db88bad3 100644 --- a/crates/sum_tree/src/cursor.rs +++ b/crates/sum_tree/src/cursor.rs @@ -431,11 +431,9 @@ where aggregate: &mut dyn SeekAggregate<'a, T>, cx: &::Context, ) -> bool { - debug_assert!( + assert!( target.cmp(&self.position, cx) >= Ordering::Equal, - "cannot seek backward from {:?} to {:?}", - self.position, - target + "cannot seek backward", ); if !self.did_seek { diff --git a/crates/sum_tree/src/sum_tree.rs b/crates/sum_tree/src/sum_tree.rs index ca351d67cea76..965413d3190aa 100644 --- a/crates/sum_tree/src/sum_tree.rs +++ b/crates/sum_tree/src/sum_tree.rs @@ -34,7 +34,7 @@ pub trait KeyedItem: Item { /// /// Each Summary type can have multiple [`Dimensions`] that it measures, /// which can be used to navigate the tree -pub trait Summary: Clone + fmt::Debug { +pub trait Summary: Clone { type Context; fn zero(cx: &Self::Context) -> Self; @@ -49,7 +49,7 @@ pub trait Summary: Clone + fmt::Debug { /// # Example: /// Zed's rope has a `TextSummary` type that summarizes lines, characters, and bytes. /// Each of these are different dimensions we may want to seek to -pub trait Dimension<'a, S: Summary>: Clone + fmt::Debug { +pub trait Dimension<'a, S: Summary>: Clone { fn zero(cx: &S::Context) -> Self; fn add_summary(&mut self, summary: &'a S, cx: &S::Context); @@ -71,7 +71,7 @@ impl<'a, T: Summary> Dimension<'a, T> for T { } } -pub trait SeekTarget<'a, S: Summary, D: Dimension<'a, S>>: fmt::Debug { +pub trait SeekTarget<'a, S: Summary, D: Dimension<'a, S>> { fn cmp(&self, cursor_location: &D, cx: &S::Context) -> Ordering; } @@ -173,9 +173,19 @@ impl Bias { /// The maximum number of items per node is `TREE_BASE * 2`. /// /// Any [`Dimension`] supported by the [`Summary`] type can be used to seek to a specific location in the tree. -#[derive(Debug, Clone)] +#[derive(Clone)] pub struct SumTree(Arc>); +impl fmt::Debug for SumTree +where + T: fmt::Debug + Item, + T::Summary: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_tuple("SumTree").field(&self.0).finish() + } +} + impl SumTree { pub fn new(cx: &::Context) -> Self { SumTree(Arc::new(Node::Leaf { @@ -763,7 +773,7 @@ where } } -#[derive(Clone, Debug)] +#[derive(Clone)] pub enum Node { Internal { height: u8, @@ -778,6 +788,39 @@ pub enum Node { }, } +impl fmt::Debug for Node +where + T: Item + fmt::Debug, + T::Summary: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Node::Internal { + height, + summary, + child_summaries, + child_trees, + } => f + .debug_struct("Internal") + .field("height", height) + .field("summary", summary) + .field("child_summaries", child_summaries) + .field("child_trees", child_trees) + .finish(), + Node::Leaf { + summary, + items, + item_summaries, + } => f + .debug_struct("Leaf") + .field("summary", summary) + .field("items", items) + .field("item_summaries", item_summaries) + .finish(), + } + } +} + impl Node { fn is_leaf(&self) -> bool { matches!(self, Node::Leaf { .. }) diff --git a/crates/sum_tree/src/tree_map.rs b/crates/sum_tree/src/tree_map.rs index 72465b1a99cab..b7eadb566d3ed 100644 --- a/crates/sum_tree/src/tree_map.rs +++ b/crates/sum_tree/src/tree_map.rs @@ -5,8 +5,8 @@ use crate::{Bias, Dimension, Edit, Item, KeyedItem, SeekTarget, SumTree, Summary #[derive(Clone, PartialEq, Eq)] pub struct TreeMap(SumTree>) where - K: Clone + Debug + Ord, - V: Clone + Debug; + K: Clone + Ord, + V: Clone; #[derive(Clone, Debug, PartialEq, Eq)] pub struct MapEntry { @@ -35,9 +35,9 @@ impl<'a, K> Default for MapKeyRef<'a, K> { #[derive(Clone)] pub struct TreeSet(TreeMap) where - K: Clone + Debug + Ord; + K: Clone + Ord; -impl TreeMap { +impl TreeMap { pub fn from_ordered_entries(entries: impl IntoIterator) -> Self { let tree = SumTree::from_iter( entries @@ -172,7 +172,7 @@ impl TreeMap { } } -impl Debug for TreeMap +impl Debug for TreeMap where K: Clone + Debug + Ord, V: Clone + Debug, @@ -185,7 +185,7 @@ where #[derive(Debug)] struct MapSeekTargetAdaptor<'a, T>(&'a T); -impl<'a, K: Debug + Clone + Ord, T: MapSeekTarget> SeekTarget<'a, MapKey, MapKeyRef<'a, K>> +impl<'a, K: Clone + Ord, T: MapSeekTarget> SeekTarget<'a, MapKey, MapKeyRef<'a, K>> for MapSeekTargetAdaptor<'_, T> { fn cmp(&self, cursor_location: &MapKeyRef, _: &()) -> Ordering { @@ -197,11 +197,11 @@ impl<'a, K: Debug + Clone + Ord, T: MapSeekTarget> SeekTarget<'a, MapKey, } } -pub trait MapSeekTarget: Debug { +pub trait MapSeekTarget { fn cmp_cursor(&self, cursor_location: &K) -> Ordering; } -impl MapSeekTarget for K { +impl MapSeekTarget for K { fn cmp_cursor(&self, cursor_location: &K) -> Ordering { self.cmp(cursor_location) } @@ -209,8 +209,8 @@ impl MapSeekTarget for K { impl Default for TreeMap where - K: Clone + Debug + Ord, - V: Clone + Debug, + K: Clone + Ord, + V: Clone, { fn default() -> Self { Self(Default::default()) @@ -219,7 +219,7 @@ where impl Item for MapEntry where - K: Clone + Debug + Ord, + K: Clone + Ord, V: Clone, { type Summary = MapKey; @@ -231,7 +231,7 @@ where impl KeyedItem for MapEntry where - K: Clone + Debug + Ord, + K: Clone + Ord, V: Clone, { type Key = MapKey; @@ -243,7 +243,7 @@ where impl Summary for MapKey where - K: Clone + Debug, + K: Clone, { type Context = (); @@ -258,7 +258,7 @@ where impl<'a, K> Dimension<'a, MapKey> for MapKeyRef<'a, K> where - K: Clone + Debug + Ord, + K: Clone + Ord, { fn zero(_cx: &()) -> Self { Default::default() @@ -271,7 +271,7 @@ where impl<'a, K> SeekTarget<'a, MapKey, MapKeyRef<'a, K>> for MapKeyRef<'_, K> where - K: Clone + Debug + Ord, + K: Clone + Ord, { fn cmp(&self, cursor_location: &MapKeyRef, _: &()) -> Ordering { Ord::cmp(&self.0, &cursor_location.0) @@ -280,7 +280,7 @@ where impl Default for TreeSet where - K: Clone + Debug + Ord, + K: Clone + Ord, { fn default() -> Self { Self(Default::default()) @@ -289,7 +289,7 @@ where impl TreeSet where - K: Clone + Debug + Ord, + K: Clone + Ord, { pub fn from_ordered_entries(entries: impl IntoIterator) -> Self { Self(TreeMap::from_ordered_entries(