Identify Worktree entries by their inode

Nathan Sobo created

This will allow us to re-parent elements when re-scanning when the file system changes.

Change summary

zed/src/editor/buffer/mod.rs        |   2 
zed/src/editor/buffer_view.rs       |   2 
zed/src/file_finder.rs              |   6 
zed/src/workspace/pane.rs           |  15 +--
zed/src/workspace/workspace.rs      |   8 
zed/src/workspace/workspace_view.rs |  10 +-
zed/src/worktree/fuzzy.rs           |  15 ++-
zed/src/worktree/worktree.rs        | 126 +++++++++++++++---------------
8 files changed, 95 insertions(+), 89 deletions(-)

Detailed changes

zed/src/editor/buffer/mod.rs 🔗

@@ -433,7 +433,7 @@ impl Buffer {
         self.file.as_ref().map(|file| file.path(app))
     }
 
-    pub fn entry_id(&self) -> Option<(usize, usize)> {
+    pub fn entry_id(&self) -> Option<(usize, u64)> {
         self.file.as_ref().map(|file| file.entry_id())
     }
 

zed/src/editor/buffer_view.rs 🔗

@@ -1225,7 +1225,7 @@ impl workspace::ItemView for BufferView {
         }
     }
 
-    fn entry_id(&self, app: &AppContext) -> Option<(usize, usize)> {
+    fn entry_id(&self, app: &AppContext) -> Option<(usize, u64)> {
         self.buffer.read(app).entry_id()
     }
 

zed/src/file_finder.rs 🔗

@@ -44,7 +44,7 @@ pub fn init(app: &mut MutableAppContext) {
 }
 
 pub enum Event {
-    Selected(usize, usize),
+    Selected(usize, u64),
     Dismissed,
 }
 
@@ -339,7 +339,7 @@ impl FileFinder {
         }
     }
 
-    fn select(&mut self, entry: &(usize, usize), ctx: &mut ViewContext<Self>) {
+    fn select(&mut self, entry: &(usize, u64), ctx: &mut ViewContext<Self>) {
         let (tree_id, entry_id) = *entry;
         ctx.emit(Event::Selected(tree_id, entry_id));
     }
@@ -347,7 +347,7 @@ impl FileFinder {
     fn spawn_search(&mut self, query: String, ctx: &mut ViewContext<Self>) {
         let worktrees = self.worktrees(ctx.as_ref());
         let search_id = util::post_inc(&mut self.search_count);
-        let pool = ctx.app().scoped_pool().clone();
+        let pool = ctx.as_ref().scoped_pool().clone();
         let task = ctx.background_executor().spawn(async move {
             let matches = match_paths(worktrees.as_slice(), &query, false, false, 100, pool);
             (search_id, matches)

zed/src/workspace/pane.rs 🔗

@@ -105,15 +105,12 @@ impl Pane {
         self.items.get(self.active_item).cloned()
     }
 
-    pub fn activate_entry(
-        &mut self,
-        entry_id: (usize, usize),
-        ctx: &mut ViewContext<Self>,
-    ) -> bool {
-        if let Some(index) = self.items.iter().position(|item| {
-            item.entry_id(ctx.as_ref())
-                .map_or(false, |id| id == entry_id)
-        }) {
+    pub fn activate_entry(&mut self, entry_id: (usize, u64), ctx: &mut ViewContext<Self>) -> bool {
+        if let Some(index) = self
+            .items
+            .iter()
+            .position(|item| item.entry_id(ctx.as_ref()).map_or(false, |id| id == entry_id))
+        {
             self.activate_item(index, ctx);
             true
         } else {

zed/src/workspace/workspace.rs 🔗

@@ -76,7 +76,7 @@ enum OpenedItem {
 pub struct Workspace {
     replica_id: ReplicaId,
     worktrees: HashSet<ModelHandle<Worktree>>,
-    items: HashMap<(usize, usize), OpenedItem>,
+    items: HashMap<(usize, u64), OpenedItem>,
 }
 
 impl Workspace {
@@ -125,7 +125,7 @@ impl Workspace {
 
     pub fn open_entry(
         &mut self,
-        entry: (usize, usize),
+        entry: (usize, u64),
         ctx: &mut ModelContext<'_, Self>,
     ) -> anyhow::Result<Pin<Box<dyn Future<Output = OpenResult> + Send>>> {
         if let Some(item) = self.items.get(&entry).cloned() {
@@ -200,12 +200,12 @@ impl Entity for Workspace {
 
 #[cfg(test)]
 pub trait WorkspaceHandle {
-    fn file_entries(&self, app: &AppContext) -> Vec<(usize, usize)>;
+    fn file_entries(&self, app: &AppContext) -> Vec<(usize, u64)>;
 }
 
 #[cfg(test)]
 impl WorkspaceHandle for ModelHandle<Workspace> {
-    fn file_entries(&self, app: &AppContext) -> Vec<(usize, usize)> {
+    fn file_entries(&self, app: &AppContext) -> Vec<(usize, u64)> {
         self.read(app)
             .worktrees()
             .iter()

zed/src/workspace/workspace_view.rs 🔗

@@ -19,7 +19,7 @@ pub fn init(app: &mut MutableAppContext) {
 
 pub trait ItemView: View {
     fn title(&self, app: &AppContext) -> String;
-    fn entry_id(&self, app: &AppContext) -> Option<(usize, usize)>;
+    fn entry_id(&self, app: &AppContext) -> Option<(usize, u64)>;
     fn clone_on_split(&self, _: &mut ViewContext<Self>) -> Option<Self>
     where
         Self: Sized,
@@ -42,7 +42,7 @@ pub trait ItemView: View {
 
 pub trait ItemViewHandle: Send + Sync {
     fn title(&self, app: &AppContext) -> String;
-    fn entry_id(&self, app: &AppContext) -> Option<(usize, usize)>;
+    fn entry_id(&self, app: &AppContext) -> Option<(usize, u64)>;
     fn boxed_clone(&self) -> Box<dyn ItemViewHandle>;
     fn clone_on_split(&self, app: &mut MutableAppContext) -> Option<Box<dyn ItemViewHandle>>;
     fn set_parent_pane(&self, pane: &ViewHandle<Pane>, app: &mut MutableAppContext);
@@ -57,7 +57,7 @@ impl<T: ItemView> ItemViewHandle for ViewHandle<T> {
         self.read(app).title(app)
     }
 
-    fn entry_id(&self, app: &AppContext) -> Option<(usize, usize)> {
+    fn entry_id(&self, app: &AppContext) -> Option<(usize, u64)> {
         self.read(app).entry_id(app)
     }
 
@@ -124,7 +124,7 @@ pub struct WorkspaceView {
     center: PaneGroup,
     panes: Vec<ViewHandle<Pane>>,
     active_pane: ViewHandle<Pane>,
-    loading_entries: HashSet<(usize, usize)>,
+    loading_entries: HashSet<(usize, u64)>,
 }
 
 impl WorkspaceView {
@@ -189,7 +189,7 @@ impl WorkspaceView {
         }
     }
 
-    pub fn open_entry(&mut self, entry: (usize, usize), ctx: &mut ViewContext<Self>) {
+    pub fn open_entry(&mut self, entry: (usize, u64), ctx: &mut ViewContext<Self>) {
         if self.loading_entries.contains(&entry) {
             return;
         }

zed/src/worktree/fuzzy.rs 🔗

@@ -12,7 +12,7 @@ const ADDITIONAL_DISTANCE_PENALTY: f64 = 0.05;
 const MIN_DISTANCE_PENALTY: f64 = 0.2;
 
 pub struct PathEntry {
-    pub entry_id: usize,
+    pub ino: u64,
     pub path_chars: CharBag,
     pub path: Vec<char>,
     pub lowercase_path: Vec<char>,
@@ -24,7 +24,7 @@ pub struct PathMatch {
     pub score: f64,
     pub positions: Vec<usize>,
     pub tree_id: usize,
-    pub entry_id: usize,
+    pub entry_id: u64,
     pub skipped_prefix_len: usize,
 }
 
@@ -191,7 +191,7 @@ fn match_single_tree_paths(
         if score > 0.0 {
             results.push(Reverse(PathMatch {
                 tree_id,
-                entry_id: path_entry.entry_id,
+                entry_id: path_entry.ino,
                 score,
                 positions: match_positions.clone(),
                 skipped_prefix_len,
@@ -453,7 +453,7 @@ mod tests {
             let path_chars = CharBag::from(&lowercase_path[..]);
             let path = path.chars().collect();
             path_entries.push(PathEntry {
-                entry_id: i,
+                ino: i as u64,
                 path_chars,
                 path,
                 lowercase_path,
@@ -490,7 +490,12 @@ mod tests {
         results
             .into_iter()
             .rev()
-            .map(|result| (paths[result.0.entry_id].clone(), result.0.positions))
+            .map(|result| {
+                (
+                    paths[result.0.entry_id as usize].clone(),
+                    result.0.positions,
+                )
+            })
             .collect()
     }
 }

zed/src/worktree/worktree.rs 🔗

@@ -33,15 +33,15 @@ pub struct Worktree(Arc<RwLock<WorktreeState>>);
 struct WorktreeState {
     id: usize,
     path: PathBuf,
-    root_ino: Option<usize>,
-    entries: HashMap<usize, Entry>,
+    root_ino: Option<u64>,
+    entries: HashMap<u64, Entry>,
     file_paths: Vec<PathEntry>,
-    histories: HashMap<usize, History>,
+    histories: HashMap<u64, History>,
     scanning: bool,
 }
 
 struct DirToScan {
-    id: usize,
+    ino: u64,
     path: PathBuf,
     relative_path: PathBuf,
     ignore: Option<Ignore>,
@@ -102,14 +102,13 @@ impl Worktree {
         }
         let is_ignored = ignore.matched(&path, metadata.is_dir()).is_ignore();
 
-        self.0.write().root_ino = Some(0);
         if metadata.file_type().is_dir() {
             let is_ignored = is_ignored || name == ".git";
-            let id = self.insert_dir(None, name, ino, is_symlink, is_ignored);
+            self.insert_dir(None, name, ino, is_symlink, is_ignored);
             let (tx, rx) = channel::unbounded();
 
             tx.send(Ok(DirToScan {
-                id,
+                ino,
                 path,
                 relative_path,
                 ignore: Some(ignore),
@@ -131,6 +130,7 @@ impl Worktree {
         } else {
             self.insert_file(None, name, ino, is_symlink, is_ignored, relative_path);
         }
+        self.0.write().root_ino = Some(ino);
 
         Ok(())
     }
@@ -159,12 +159,12 @@ impl Worktree {
                     }
                 }
 
-                let id = self.insert_dir(Some(to_scan.id), name, ino, is_symlink, is_ignored);
-                new_children.push(id);
+                self.insert_dir(Some(to_scan.ino), name, ino, is_symlink, is_ignored);
+                new_children.push(ino);
 
                 let dirs_to_scan = to_scan.dirs_to_scan.clone();
                 let _ = to_scan.dirs_to_scan.send(Ok(DirToScan {
-                    id,
+                    ino,
                     path,
                     relative_path,
                     ignore,
@@ -175,18 +175,19 @@ impl Worktree {
                     i.matched(to_scan.path.join(&name), false).is_ignore()
                 });
 
-                new_children.push(self.insert_file(
-                    Some(to_scan.id),
+                self.insert_file(
+                    Some(to_scan.ino),
                     name,
                     ino,
                     is_symlink,
                     is_ignored,
                     relative_path,
-                ));
+                );
+                new_children.push(ino);
             };
         }
 
-        if let Some(Entry::Dir { children, .. }) = &mut self.0.write().entries.get_mut(&to_scan.id)
+        if let Some(Entry::Dir { children, .. }) = &mut self.0.write().entries.get_mut(&to_scan.ino)
         {
             *children = new_children.clone();
         }
@@ -196,16 +197,15 @@ impl Worktree {
 
     fn insert_dir(
         &self,
-        parent: Option<usize>,
+        parent: Option<u64>,
         name: OsString,
         ino: u64,
         is_symlink: bool,
         is_ignored: bool,
-    ) -> usize {
+    ) {
         let entries = &mut self.0.write().entries;
-        let dir_id = entries.len();
         entries.insert(
-            dir_id,
+            ino,
             Entry::Dir {
                 parent,
                 name,
@@ -215,27 +215,25 @@ impl Worktree {
                 children: Vec::new(),
             },
         );
-        dir_id
     }
 
     fn insert_file(
         &self,
-        parent: Option<usize>,
+        parent: Option<u64>,
         name: OsString,
         ino: u64,
         is_symlink: bool,
         is_ignored: bool,
         path: PathBuf,
-    ) -> usize {
+    ) {
         let path = path.to_string_lossy();
         let lowercase_path = path.to_lowercase().chars().collect::<Vec<_>>();
         let path = path.chars().collect::<Vec<_>>();
         let path_chars = CharBag::from(&path[..]);
 
         let mut state = self.0.write();
-        let entry_id = state.entries.len();
         state.entries.insert(
-            entry_id,
+            ino,
             Entry::File {
                 parent,
                 name,
@@ -245,25 +243,23 @@ impl Worktree {
             },
         );
         state.file_paths.push(PathEntry {
-            entry_id,
+            ino,
             path_chars,
             path,
             lowercase_path,
             is_ignored,
         });
-        entry_id
     }
 
-    pub fn entry_path(&self, mut entry_id: usize) -> Result<PathBuf> {
+    pub fn entry_path(&self, mut entry_id: u64) -> Result<PathBuf> {
         let state = self.0.read();
 
-        if entry_id >= state.entries.len() {
-            return Err(anyhow!("Entry does not exist in tree"));
-        }
-
         let mut entries = Vec::new();
         loop {
-            let entry = &state.entries[&entry_id];
+            let entry = state
+                .entries
+                .get(&entry_id)
+                .ok_or_else(|| anyhow!("entry does not exist in worktree"))?;
             entries.push(entry);
             if let Some(parent_id) = entry.parent() {
                 entry_id = parent_id;
@@ -279,13 +275,13 @@ impl Worktree {
         Ok(path)
     }
 
-    pub fn abs_entry_path(&self, entry_id: usize) -> Result<PathBuf> {
+    pub fn abs_entry_path(&self, entry_id: u64) -> Result<PathBuf> {
         let mut path = self.0.read().path.clone();
         path.pop();
         Ok(path.join(self.entry_path(entry_id)?))
     }
 
-    fn fmt_entry(&self, f: &mut fmt::Formatter<'_>, entry_id: usize, indent: usize) -> fmt::Result {
+    fn fmt_entry(&self, f: &mut fmt::Formatter<'_>, entry_id: u64, indent: usize) -> fmt::Result {
         match &self.0.read().entries[&entry_id] {
             Entry::Dir { name, children, .. } => {
                 write!(
@@ -333,6 +329,10 @@ impl Worktree {
         }
     }
 
+    pub fn has_entry(&self, entry_id: u64) -> bool {
+        self.0.read().entries.contains_key(&entry_id)
+    }
+
     pub fn entry_count(&self) -> usize {
         self.0.read().entries.len()
     }
@@ -341,7 +341,7 @@ impl Worktree {
         self.0.read().file_paths.len()
     }
 
-    pub fn load_history(&self, entry_id: usize) -> impl Future<Output = Result<History>> {
+    pub fn load_history(&self, entry_id: u64) -> impl Future<Output = Result<History>> {
         let tree = self.clone();
 
         async move {
@@ -360,12 +360,7 @@ impl Worktree {
         }
     }
 
-    pub fn save<'a>(
-        &self,
-        entry_id: usize,
-        content: Snapshot,
-        ctx: &AppContext,
-    ) -> Task<Result<()>> {
+    pub fn save<'a>(&self, entry_id: u64, content: Snapshot, ctx: &AppContext) -> Task<Result<()>> {
         let path = self.abs_entry_path(entry_id);
         ctx.background_executor().spawn(async move {
             let buffer_size = content.text_summary().bytes.min(10 * 1024);
@@ -420,34 +415,34 @@ impl WorktreeState {
 }
 
 pub trait WorktreeHandle {
-    fn file(&self, entry_id: usize, app: &AppContext) -> Result<FileHandle>;
+    fn file(&self, entry_id: u64, app: &AppContext) -> Result<FileHandle>;
 }
 
 impl WorktreeHandle for ModelHandle<Worktree> {
-    fn file(&self, entry_id: usize, app: &AppContext) -> Result<FileHandle> {
-        if entry_id >= self.read(app).entry_count() {
-            return Err(anyhow!("Entry does not exist in tree"));
+    fn file(&self, entry_id: u64, app: &AppContext) -> Result<FileHandle> {
+        if self.read(app).has_entry(entry_id) {
+            Err(anyhow!("entry does not exist in tree"))
+        } else {
+            Ok(FileHandle {
+                worktree: self.clone(),
+                entry_id,
+            })
         }
-
-        Ok(FileHandle {
-            worktree: self.clone(),
-            entry_id,
-        })
     }
 }
 
 #[derive(Clone, Debug)]
 pub enum Entry {
     Dir {
-        parent: Option<usize>,
+        parent: Option<u64>,
         name: OsString,
         ino: u64,
         is_symlink: bool,
         is_ignored: bool,
-        children: Vec<usize>,
+        children: Vec<u64>,
     },
     File {
-        parent: Option<usize>,
+        parent: Option<u64>,
         name: OsString,
         ino: u64,
         is_symlink: bool,
@@ -456,12 +451,18 @@ pub enum Entry {
 }
 
 impl Entry {
-    fn parent(&self) -> Option<usize> {
+    fn parent(&self) -> Option<u64> {
         match self {
             Entry::Dir { parent, .. } | Entry::File { parent, .. } => *parent,
         }
     }
 
+    fn ino(&self) -> u64 {
+        match self {
+            Entry::Dir { ino, .. } | Entry::File { ino, .. } => *ino,
+        }
+    }
+
     fn name(&self) -> &OsStr {
         match self {
             Entry::Dir { name, .. } | Entry::File { name, .. } => name,
@@ -472,7 +473,7 @@ impl Entry {
 #[derive(Clone)]
 pub struct FileHandle {
     worktree: ModelHandle<Worktree>,
-    entry_id: usize,
+    entry_id: u64,
 }
 
 impl FileHandle {
@@ -489,13 +490,13 @@ impl FileHandle {
         worktree.save(self.entry_id, content, ctx)
     }
 
-    pub fn entry_id(&self) -> (usize, usize) {
+    pub fn entry_id(&self) -> (usize, u64) {
         (self.worktree.id(), self.entry_id)
     }
 }
 
 struct IterStackEntry {
-    entry_id: usize,
+    entry_id: u64,
     child_idx: usize,
 }
 
@@ -516,18 +517,21 @@ impl Iterator for Iter {
 
             return if let Some(entry) = state.root_entry().cloned() {
                 self.stack.push(IterStackEntry {
-                    entry_id: 0,
+                    entry_id: entry.ino(),
                     child_idx: 0,
                 });
 
-                Some(Traversal::Push { entry_id: 0, entry })
+                Some(Traversal::Push {
+                    entry_id: entry.ino(),
+                    entry,
+                })
             } else {
                 None
             };
         }
 
         while let Some(parent) = self.stack.last_mut() {
-            if let Entry::Dir { children, .. } = &state.entries[&parent.entry_id] {
+            if let Some(Entry::Dir { children, .. }) = &state.entries.get(&parent.entry_id) {
                 if parent.child_idx < children.len() {
                     let child_id = children[post_inc(&mut parent.child_idx)];
 
@@ -558,7 +562,7 @@ impl Iterator for Iter {
 
 #[derive(Debug)]
 pub enum Traversal {
-    Push { entry_id: usize, entry: Entry },
+    Push { entry_id: u64, entry: Entry },
     Pop,
 }
 
@@ -568,7 +572,7 @@ pub struct FilesIter {
 }
 
 pub struct FilesIterItem {
-    pub entry_id: usize,
+    pub entry_id: u64,
     pub path: PathBuf,
 }