Merge pull request #1769 from zed-industries/breadcrumbs

Mikayla Maki created

Fix breadcrumbs

Change summary

crates/call/src/room.rs                |  3 +
crates/collab/src/integration_tests.rs |  6 ++-
crates/collab/src/rpc.rs               |  3 ++
crates/collab/src/rpc/store.rs         |  1 
crates/editor/src/element.rs           | 13 +++++++-
crates/editor/src/items.rs             | 22 ++++++---------
crates/fs/src/fs.rs                    | 12 ++++++++
crates/language/src/buffer.rs          | 12 ++++++++
crates/project/src/project.rs          |  5 ++
crates/project/src/project_tests.rs    |  1 
crates/project/src/worktree.rs         | 40 +++++++++++++++++++++++----
crates/rpc/proto/zed.proto             |  2 +
crates/rpc/src/proto.rs                |  1 
crates/rpc/src/rpc.rs                  |  2 
crates/terminal/src/terminal_view.rs   | 13 --------
crates/workspace/src/workspace.rs      |  3 ++
crates/zed/src/main.rs                 |  4 ++
crates/zed/src/paths.rs                |  2 
18 files changed, 105 insertions(+), 40 deletions(-)

Detailed changes

crates/call/src/room.rs 🔗

@@ -8,7 +8,7 @@ use collections::{BTreeMap, HashSet};
 use futures::StreamExt;
 use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task};
 use project::Project;
-use std::sync::Arc;
+use std::{os::unix::prelude::OsStrExt, sync::Arc};
 use util::ResultExt;
 
 #[derive(Clone, Debug, PartialEq, Eq)]
@@ -389,6 +389,7 @@ impl Room {
                         id: worktree.id().to_proto(),
                         root_name: worktree.root_name().into(),
                         visible: worktree.is_visible(),
+                        abs_path: worktree.abs_path().as_os_str().as_bytes().to_vec(),
                     }
                 })
                 .collect(),

crates/collab/src/integration_tests.rs 🔗

@@ -15,7 +15,7 @@ use editor::{
     self, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Redo, Rename, ToOffset,
     ToggleCodeActions, Undo,
 };
-use fs::{FakeFs, Fs as _, LineEnding};
+use fs::{FakeFs, Fs as _, HomeDir, LineEnding};
 use futures::{channel::mpsc, Future, StreamExt as _};
 use gpui::{
     executor::{self, Deterministic},
@@ -3038,7 +3038,7 @@ async fn test_references(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
         assert_eq!(references[1].buffer, references[0].buffer);
         assert_eq!(
             three_buffer.file().unwrap().full_path(cx),
-            Path::new("three.rs")
+            Path::new("/root/dir-2/three.rs")
         );
 
         assert_eq!(references[0].range.to_offset(two_buffer), 24..27);
@@ -6130,6 +6130,8 @@ impl TestServer {
 
     async fn create_client(&mut self, cx: &mut TestAppContext, name: &str) -> TestClient {
         cx.update(|cx| {
+            cx.set_global(HomeDir(Path::new("/tmp/").to_path_buf()));
+
             let mut settings = Settings::test(cx);
             settings.projects_online_by_default = false;
             cx.set_global(settings);

crates/collab/src/rpc.rs 🔗

@@ -42,6 +42,7 @@ use std::{
     marker::PhantomData,
     net::SocketAddr,
     ops::{Deref, DerefMut},
+    os::unix::prelude::OsStrExt,
     rc::Rc,
     sync::{
         atomic::{AtomicBool, Ordering::SeqCst},
@@ -941,6 +942,7 @@ impl Server {
                 id: *id,
                 root_name: worktree.root_name.clone(),
                 visible: worktree.visible,
+                abs_path: worktree.abs_path.as_os_str().as_bytes().to_vec(),
             })
             .collect::<Vec<_>>();
 
@@ -989,6 +991,7 @@ impl Server {
             let message = proto::UpdateWorktree {
                 project_id: project_id.to_proto(),
                 worktree_id: *worktree_id,
+                abs_path: worktree.abs_path.as_os_str().as_bytes().to_vec(),
                 root_name: worktree.root_name.clone(),
                 updated_entries: worktree.entries.values().cloned().collect(),
                 removed_entries: Default::default(),

crates/collab/src/rpc/store.rs 🔗

@@ -66,6 +66,7 @@ pub struct Collaborator {
 
 #[derive(Default, Serialize)]
 pub struct Worktree {
+    pub abs_path: PathBuf,
     pub root_name: String,
     pub visible: bool,
     #[serde(skip)]

crates/editor/src/element.rs 🔗

@@ -1324,6 +1324,7 @@ impl EditorElement {
         line_height: f32,
         style: &EditorStyle,
         line_layouts: &[text_layout::Line],
+        include_root: bool,
         cx: &mut LayoutContext,
     ) -> (f32, Vec<BlockLayout>) {
         let editor = if let Some(editor) = self.view.upgrade(cx) {
@@ -1427,10 +1428,11 @@ impl EditorElement {
                         let font_size =
                             (style.text_scale_factor * self.style.text.font_size).round();
 
+                        let path = buffer.resolve_file_path(cx, include_root);
                         let mut filename = None;
                         let mut parent_path = None;
-                        if let Some(file) = buffer.file() {
-                            let path = file.path();
+                        // Can't use .and_then() because `.file_name()` and `.parent()` return references :(
+                        if let Some(path) = path {
                             filename = path.file_name().map(|f| f.to_string_lossy().to_string());
                             parent_path =
                                 path.parent().map(|p| p.to_string_lossy().to_string() + "/");
@@ -1634,6 +1636,7 @@ impl Element for EditorElement {
         let mut highlighted_rows = None;
         let mut highlighted_ranges = Vec::new();
         let mut show_scrollbars = false;
+        let mut include_root = false;
         self.update_view(cx.app, |view, cx| {
             let display_map = view.display_map.update(cx, |map, cx| map.snapshot(cx));
 
@@ -1706,6 +1709,11 @@ impl Element for EditorElement {
             }
 
             show_scrollbars = view.show_scrollbars();
+            include_root = view
+                .project
+                .as_ref()
+                .map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
+                .unwrap_or_default()
         });
 
         let line_number_layouts =
@@ -1745,6 +1753,7 @@ impl Element for EditorElement {
             line_height,
             &style,
             &line_layouts,
+            include_root,
             cx,
         );
 

crates/editor/src/items.rs 🔗

@@ -532,21 +532,17 @@ impl Item for Editor {
         let buffer = multibuffer.buffer(buffer_id)?;
 
         let buffer = buffer.read(cx);
-        let filename = if let Some(file) = buffer.file() {
-            if file.path().file_name().is_none()
-                || self
-                    .project
+        let filename = buffer
+            .snapshot()
+            .resolve_file_path(
+                cx,
+                self.project
                     .as_ref()
                     .map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
-                    .unwrap_or_default()
-            {
-                file.full_path(cx).to_string_lossy().to_string()
-            } else {
-                file.path().to_string_lossy().to_string()
-            }
-        } else {
-            "untitled".to_string()
-        };
+                    .unwrap_or_default(),
+            )
+            .map(|path| path.to_string_lossy().to_string())
+            .unwrap_or_else(|| "untitled".to_string());
 
         let mut breadcrumbs = vec![Label::new(filename, theme.breadcrumbs.text.clone()).boxed()];
         breadcrumbs.extend(symbols.into_iter().map(|symbol| {

crates/fs/src/fs.rs 🔗

@@ -13,6 +13,7 @@ use smol::io::{AsyncReadExt, AsyncWriteExt};
 use std::borrow::Cow;
 use std::cmp;
 use std::io::Write;
+use std::ops::Deref;
 use std::sync::Arc;
 use std::{
     io,
@@ -92,6 +93,17 @@ impl LineEnding {
         }
     }
 }
+
+pub struct HomeDir(pub PathBuf);
+
+impl Deref for HomeDir {
+    type Target = PathBuf;
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
 #[async_trait::async_trait]
 pub trait Fs: Send + Sync {
     async fn create_dir(&self, path: &Path) -> Result<()>;

crates/language/src/buffer.rs 🔗

@@ -2334,6 +2334,18 @@ impl BufferSnapshot {
         self.file.as_deref()
     }
 
+    pub fn resolve_file_path(&self, cx: &AppContext, include_root: bool) -> Option<PathBuf> {
+        if let Some(file) = self.file() {
+            if file.path().file_name().is_none() || include_root {
+                Some(file.full_path(cx))
+            } else {
+                Some(file.path().to_path_buf())
+            }
+        } else {
+            None
+        }
+    }
+
     pub fn file_update_count(&self) -> usize {
         self.file_update_count
     }

crates/project/src/project.rs 🔗

@@ -583,7 +583,10 @@ impl Project {
         cx: &mut gpui::TestAppContext,
     ) -> ModelHandle<Project> {
         if !cx.read(|cx| cx.has_global::<Settings>()) {
-            cx.update(|cx| cx.set_global(Settings::test(cx)));
+            cx.update(|cx| {
+                cx.set_global(Settings::test(cx));
+                cx.set_global(HomeDir(Path::new("/tmp/").to_path_buf()))
+            });
         }
 
         let languages = Arc::new(LanguageRegistry::test());

crates/project/src/project_tests.rs 🔗

@@ -2164,6 +2164,7 @@ async fn test_rescan_and_remote_updates(
             proto::WorktreeMetadata {
                 id: initial_snapshot.id().to_proto(),
                 root_name: initial_snapshot.root_name().into(),
+                abs_path: initial_snapshot.abs_path().as_os_str().as_bytes().to_vec(),
                 visible: true,
             },
             rpc.clone(),

crates/project/src/worktree.rs 🔗

@@ -5,8 +5,8 @@ use anyhow::{anyhow, Context, Result};
 use client::{proto, Client};
 use clock::ReplicaId;
 use collections::{HashMap, VecDeque};
-use fs::LineEnding;
 use fs::{repository::GitRepository, Fs};
+use fs::{HomeDir, LineEnding};
 use futures::{
     channel::{
         mpsc::{self, UnboundedSender},
@@ -87,6 +87,7 @@ pub struct RemoteWorktree {
 #[derive(Clone)]
 pub struct Snapshot {
     id: WorktreeId,
+    abs_path: Arc<Path>,
     root_name: String,
     root_char_bag: CharBag,
     entries_by_path: SumTree<Entry>,
@@ -118,7 +119,6 @@ impl std::fmt::Debug for GitRepositoryEntry {
 }
 
 pub struct LocalSnapshot {
-    abs_path: Arc<Path>,
     ignores_by_parent_abs_path: HashMap<Arc<Path>, (Arc<Gitignore>, usize)>,
     git_repositories: Vec<GitRepositoryEntry>,
     removed_entry_ids: HashMap<u64, ProjectEntryId>,
@@ -130,7 +130,6 @@ pub struct LocalSnapshot {
 impl Clone for LocalSnapshot {
     fn clone(&self) -> Self {
         Self {
-            abs_path: self.abs_path.clone(),
             ignores_by_parent_abs_path: self.ignores_by_parent_abs_path.clone(),
             git_repositories: self.git_repositories.iter().cloned().collect(),
             removed_entry_ids: self.removed_entry_ids.clone(),
@@ -221,8 +220,11 @@ impl Worktree {
             .collect();
         let root_name = worktree.root_name.clone();
         let visible = worktree.visible;
+
+        let abs_path = PathBuf::from(OsString::from_vec(worktree.abs_path));
         let snapshot = Snapshot {
             id: WorktreeId(remote_id as usize),
+            abs_path: Arc::from(abs_path.deref()),
             root_name,
             root_char_bag,
             entries_by_path: Default::default(),
@@ -372,6 +374,13 @@ impl Worktree {
             Self::Remote(worktree) => worktree.poll_snapshot(cx),
         };
     }
+
+    pub fn abs_path(&self) -> Arc<Path> {
+        match self {
+            Worktree::Local(worktree) => worktree.abs_path.clone(),
+            Worktree::Remote(worktree) => worktree.abs_path.clone(),
+        }
+    }
 }
 
 impl LocalWorktree {
@@ -402,13 +411,13 @@ impl LocalWorktree {
             watch::channel_with(ScanState::Initializing);
         let tree = cx.add_model(move |cx: &mut ModelContext<Worktree>| {
             let mut snapshot = LocalSnapshot {
-                abs_path,
                 ignores_by_parent_abs_path: Default::default(),
                 git_repositories: Default::default(),
                 removed_entry_ids: Default::default(),
                 next_entry_id,
                 snapshot: Snapshot {
                     id: WorktreeId::from_usize(cx.model_id()),
+                    abs_path,
                     root_name: root_name.clone(),
                     root_char_bag,
                     entries_by_path: Default::default(),
@@ -647,6 +656,7 @@ impl LocalWorktree {
             id: self.id().to_proto(),
             root_name: self.root_name().to_string(),
             visible: self.visible,
+            abs_path: self.abs_path().as_os_str().as_bytes().to_vec(),
         }
     }
 
@@ -980,6 +990,7 @@ impl LocalWorktree {
                             let update = proto::UpdateWorktree {
                                 project_id,
                                 worktree_id,
+                                abs_path: snapshot.abs_path().as_os_str().as_bytes().to_vec(),
                                 root_name: snapshot.root_name().to_string(),
                                 updated_entries: snapshot
                                     .entries_by_path
@@ -1389,6 +1400,7 @@ impl LocalSnapshot {
         proto::UpdateWorktree {
             project_id,
             worktree_id: self.id().to_proto(),
+            abs_path: self.abs_path().as_os_str().as_bytes().to_vec(),
             root_name,
             updated_entries: self.entries_by_path.iter().map(Into::into).collect(),
             removed_entries: Default::default(),
@@ -1456,6 +1468,7 @@ impl LocalSnapshot {
         proto::UpdateWorktree {
             project_id,
             worktree_id,
+            abs_path: self.abs_path().as_os_str().as_bytes().to_vec(),
             root_name: self.root_name().to_string(),
             updated_entries,
             removed_entries,
@@ -1839,10 +1852,25 @@ impl language::File for File {
 
     fn full_path(&self, cx: &AppContext) -> PathBuf {
         let mut full_path = PathBuf::new();
-        full_path.push(self.worktree.read(cx).root_name());
+        let worktree = self.worktree.read(cx);
+
+        if worktree.is_visible() {
+            full_path.push(worktree.root_name());
+        } else {
+            let path = worktree.abs_path();
+
+            if worktree.is_local() && path.starts_with(cx.global::<HomeDir>().as_path()) {
+                full_path.push("~");
+                full_path.push(path.strip_prefix(cx.global::<HomeDir>().as_path()).unwrap());
+            } else {
+                full_path.push(path)
+            }
+        }
+
         if self.path.components().next().is_some() {
             full_path.push(&self.path);
         }
+
         full_path
     }
 
@@ -3455,7 +3483,6 @@ mod tests {
         let fs = Arc::new(RealFs);
         let next_entry_id = Arc::new(AtomicUsize::new(0));
         let mut initial_snapshot = LocalSnapshot {
-            abs_path: root_dir.path().into(),
             removed_entry_ids: Default::default(),
             ignores_by_parent_abs_path: Default::default(),
             git_repositories: Default::default(),
@@ -3464,6 +3491,7 @@ mod tests {
                 id: WorktreeId::from_usize(0),
                 entries_by_path: Default::default(),
                 entries_by_id: Default::default(),
+                abs_path: root_dir.path().into(),
                 root_name: Default::default(),
                 root_char_bag: Default::default(),
                 scan_id: 0,

crates/rpc/proto/zed.proto 🔗

@@ -266,6 +266,7 @@ message UpdateWorktree {
     repeated uint64 removed_entries = 5;
     uint64 scan_id = 6;
     bool is_last_update = 7;
+    bytes abs_path = 8;
 }
 
 message UpdateWorktreeExtensions {
@@ -1061,6 +1062,7 @@ message WorktreeMetadata {
     uint64 id = 1;
     string root_name = 2;
     bool visible = 3;
+    bytes abs_path = 4;
 }
 
 message UpdateDiffBase {

crates/rpc/src/proto.rs 🔗

@@ -435,6 +435,7 @@ pub fn split_worktree_update(
             project_id: message.project_id,
             worktree_id: message.worktree_id,
             root_name: message.root_name.clone(),
+            abs_path: message.abs_path.clone(),
             updated_entries,
             removed_entries: mem::take(&mut message.removed_entries),
             scan_id: message.scan_id,

crates/rpc/src/rpc.rs 🔗

@@ -6,4 +6,4 @@ pub use conn::Connection;
 pub use peer::*;
 mod macros;
 
-pub const PROTOCOL_VERSION: u32 = 36;
+pub const PROTOCOL_VERSION: u32 = 37;

crates/terminal/src/terminal_view.rs 🔗

@@ -38,18 +38,7 @@ pub struct SendKeystroke(String);
 
 actions!(
     terminal,
-    [
-        Up,
-        Down,
-        CtrlC,
-        Escape,
-        Enter,
-        Clear,
-        Copy,
-        Paste,
-        ShowCharacterPalette,
-        SearchTest
-    ]
+    [Clear, Copy, Paste, ShowCharacterPalette, SearchTest]
 );
 
 impl_actions!(terminal, [SendText, SendKeystroke]);

crates/workspace/src/workspace.rs 🔗

@@ -927,6 +927,9 @@ impl From<&dyn NotificationHandle> for AnyViewHandle {
 impl AppState {
     #[cfg(any(test, feature = "test-support"))]
     pub fn test(cx: &mut MutableAppContext) -> Arc<Self> {
+        use fs::HomeDir;
+
+        cx.set_global(HomeDir(Path::new("/tmp/").to_path_buf()));
         let settings = Settings::test(cx);
         cx.set_global(settings);
 

crates/zed/src/main.rs 🔗

@@ -23,7 +23,7 @@ use isahc::{config::Configurable, Request};
 use language::LanguageRegistry;
 use log::LevelFilter;
 use parking_lot::Mutex;
-use project::{Fs, ProjectStore};
+use project::{Fs, HomeDir, ProjectStore};
 use serde_json::json;
 use settings::{
     self, settings_file::SettingsFile, KeymapFileContent, Settings, SettingsFileContent,
@@ -99,6 +99,8 @@ fn main() {
 
         let (settings_file_content, keymap_file) = cx.background().block(config_files).unwrap();
 
+        cx.set_global(HomeDir(zed::paths::HOME.to_path_buf()));
+
         //Setup settings global before binding actions
         cx.set_global(SettingsFile::new(
             &*zed::paths::SETTINGS,

crates/zed/src/paths.rs 🔗

@@ -1,7 +1,7 @@
 use std::path::PathBuf;
 
 lazy_static::lazy_static! {
-    static ref HOME: PathBuf = dirs::home_dir().expect("failed to determine home directory");
+    pub static ref HOME: PathBuf = dirs::home_dir().expect("failed to determine home directory");
     pub static ref CONFIG_DIR: PathBuf = HOME.join(".config").join("zed");
     pub static ref LOGS_DIR: PathBuf = HOME.join("Library/Logs/Zed");
     pub static ref LANGUAGES_DIR: PathBuf = HOME.join("Library/Application Support/Zed/languages");