diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index dd4b1620272ec1976abe9322bda06b0176a1fcc0..a2413f248ad86f01be0adc0aa5999ae8b35772c3 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -122,13 +122,9 @@ impl ItemView for Editor { } fn title(&self, cx: &AppContext) -> String { - let filename = self - .buffer() - .read(cx) - .file(cx) - .and_then(|file| file.file_name()); - if let Some(name) = filename { - name.to_string_lossy().into() + let file = self.buffer().read(cx).file(cx); + if let Some(file) = file { + file.file_name(cx).to_string_lossy().into() } else { "untitled".into() } diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index f9f0192d9be9d48bb2cf172c3cc490d827814837..bd361e9731d2dc1bd8f9bb78b29e16ae992cbac1 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -156,21 +156,20 @@ pub enum Event { } pub trait File { + fn as_local(&self) -> Option<&dyn LocalFile>; + fn mtime(&self) -> SystemTime; /// Returns the path of this file relative to the worktree's root directory. fn path(&self) -> &Arc; - /// Returns the absolute path of this file. - fn abs_path(&self) -> Option; - /// Returns the path of this file relative to the worktree's parent directory (this means it /// includes the name of the worktree's root folder). - fn full_path(&self) -> PathBuf; + fn full_path(&self, cx: &AppContext) -> PathBuf; /// Returns the last component of this handle's absolute path. If this handle refers to the root /// of its worktree, then this method will return the name of the worktree itself. - fn file_name(&self) -> Option; + fn file_name(&self, cx: &AppContext) -> OsString; fn is_deleted(&self) -> bool; @@ -182,8 +181,6 @@ pub trait File { cx: &mut MutableAppContext, ) -> Task>; - fn load_local(&self, cx: &AppContext) -> Option>>; - fn format_remote(&self, buffer_id: u64, cx: &mut MutableAppContext) -> Option>>; @@ -192,6 +189,23 @@ pub trait File { fn buffer_removed(&self, buffer_id: u64, cx: &mut MutableAppContext); fn as_any(&self) -> &dyn Any; + + fn to_proto(&self) -> rpc::proto::File; +} + +pub trait LocalFile: File { + /// Returns the absolute path of this file. + fn abs_path(&self, cx: &AppContext) -> PathBuf; + + fn load(&self, cx: &AppContext) -> Task>; + + fn buffer_reloaded( + &self, + buffer_id: u64, + version: &clock::Global, + mtime: SystemTime, + cx: &mut MutableAppContext, + ); } pub(crate) struct QueryCursorHandle(Option); @@ -348,6 +362,7 @@ impl Buffer { pub fn to_proto(&self) -> proto::Buffer { proto::Buffer { id: self.remote_id(), + file: self.file.as_ref().map(|f| f.to_proto()), visible_text: self.text.text(), deleted_text: self.text.deleted_text(), undo_map: self @@ -457,7 +472,7 @@ impl Buffer { if let Some(LanguageServerState { server, .. }) = self.language_server.as_ref() { let server = server.clone(); - let abs_path = file.abs_path().unwrap(); + let abs_path = file.as_local().unwrap().abs_path(cx); let version = self.version(); cx.spawn(|this, mut cx| async move { let edits = server @@ -619,7 +634,7 @@ impl Buffer { None }; - self.update_language_server(); + self.update_language_server(cx); } pub fn did_save( @@ -634,7 +649,11 @@ impl Buffer { if let Some(new_file) = new_file { self.file = Some(new_file); } - if let Some(state) = &self.language_server { + if let Some((state, local_file)) = &self + .language_server + .as_ref() + .zip(self.file.as_ref().and_then(|f| f.as_local())) + { cx.background() .spawn( state @@ -642,10 +661,7 @@ impl Buffer { .notify::( lsp::DidSaveTextDocumentParams { text_document: lsp::TextDocumentIdentifier { - uri: lsp::Url::from_file_path( - self.file.as_ref().unwrap().abs_path().unwrap(), - ) - .unwrap(), + uri: lsp::Url::from_file_path(local_file.abs_path(cx)).unwrap(), }, text: None, }, @@ -656,14 +672,33 @@ impl Buffer { cx.emit(Event::Saved); } + pub fn did_reload( + &mut self, + version: clock::Global, + mtime: SystemTime, + cx: &mut ModelContext, + ) { + self.saved_mtime = mtime; + self.saved_version = version; + if let Some(file) = self.file.as_ref().and_then(|f| f.as_local()) { + file.buffer_reloaded(self.remote_id(), &self.saved_version, self.saved_mtime, cx); + } + cx.emit(Event::Reloaded); + cx.notify(); + } + pub fn file_updated( &mut self, new_file: Box, cx: &mut ModelContext, - ) -> Option> { - let old_file = self.file.as_ref()?; + ) -> Task<()> { + let old_file = if let Some(file) = self.file.as_ref() { + file + } else { + return Task::ready(()); + }; let mut file_changed = false; - let mut task = None; + let mut task = Task::ready(()); if new_file.path() != old_file.path() { file_changed = true; @@ -682,10 +717,12 @@ impl Buffer { file_changed = true; if !self.is_dirty() { - task = Some(cx.spawn(|this, mut cx| { + task = cx.spawn(|this, mut cx| { async move { let new_text = this.read_with(&cx, |this, cx| { - this.file.as_ref().and_then(|file| file.load_local(cx)) + this.file + .as_ref() + .and_then(|file| file.as_local().map(|f| f.load(cx))) }); if let Some(new_text) = new_text { let new_text = new_text.await?; @@ -694,9 +731,7 @@ impl Buffer { .await; this.update(&mut cx, |this, cx| { if this.apply_diff(diff, cx) { - this.saved_version = this.version(); - this.saved_mtime = new_mtime; - cx.emit(Event::Reloaded); + this.did_reload(this.version(), new_mtime, cx); } }); } @@ -704,7 +739,7 @@ impl Buffer { } .log_err() .map(drop) - })); + }); } } } @@ -1226,7 +1261,7 @@ impl Buffer { self.set_active_selections(Arc::from([]), cx); } - fn update_language_server(&mut self) { + fn update_language_server(&mut self, cx: &AppContext) { let language_server = if let Some(language_server) = self.language_server.as_mut() { language_server } else { @@ -1235,9 +1270,8 @@ impl Buffer { let abs_path = self .file .as_ref() - .map_or(Path::new("/").to_path_buf(), |file| { - file.abs_path().unwrap() - }); + .and_then(|f| f.as_local()) + .map_or(Path::new("/").to_path_buf(), |file| file.abs_path(cx)); let version = post_inc(&mut language_server.next_version); let snapshot = LanguageServerSnapshot { @@ -1381,7 +1415,7 @@ impl Buffer { } self.reparse(cx); - self.update_language_server(); + self.update_language_server(cx); cx.emit(Event::Edited); if !was_dirty { diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index 6d5e2790ef781ed04f0c261ee23911954509de8f..d302be874f810f460e17bd110bea0b82eb3aad0b 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "project" version = "0.1.0" -edition = "2018" +edition = "2021" [lib] path = "src/project.rs" diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index cd9ea34c3b960fb781fa7d2d495014b69bb2895f..04f8aa511aae340b03ad7c6e2753b8ed9361803b 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -298,6 +298,8 @@ impl Project { Self::handle_disk_based_diagnostics_updated, ), client.subscribe_to_entity(remote_id, cx, Self::handle_update_buffer), + client.subscribe_to_entity(remote_id, cx, Self::handle_update_buffer_file), + client.subscribe_to_entity(remote_id, cx, Self::handle_buffer_reloaded), client.subscribe_to_entity(remote_id, cx, Self::handle_buffer_saved), ], client, @@ -624,7 +626,7 @@ impl Project { ) -> Option<()> { let (path, full_path) = { let file = buffer.read(cx).file()?; - (file.path().clone(), file.full_path()) + (file.path().clone(), file.full_path(cx)) }; // If the buffer has a language, set it and start/assign the language server @@ -938,7 +940,7 @@ impl Project { let buffer_abs_path; if let Some(file) = File::from_dyn(buffer.file()) { worktree = file.worktree.clone(); - buffer_abs_path = file.abs_path(); + buffer_abs_path = file.as_local().map(|f| f.abs_path(cx)); } else { return Task::ready(Err(anyhow!("buffer does not belong to any worktree"))); }; @@ -1136,10 +1138,12 @@ impl Project { fn add_worktree(&mut self, worktree: &ModelHandle, cx: &mut ModelContext) { cx.observe(&worktree, |_, _, cx| cx.notify()).detach(); - cx.subscribe(&worktree, |this, worktree, _, cx| { - this.update_open_buffers(worktree, cx) - }) - .detach(); + if worktree.read(cx).is_local() { + cx.subscribe(&worktree, |this, worktree, _, cx| { + this.update_local_worktree_buffers(worktree, cx); + }) + .detach(); + } let push_weak_handle = { let worktree = worktree.read(cx); @@ -1161,14 +1165,12 @@ impl Project { cx.notify(); } - fn update_open_buffers( + fn update_local_worktree_buffers( &mut self, worktree_handle: ModelHandle, cx: &mut ModelContext, ) { - let local = worktree_handle.read(cx).is_local(); let snapshot = worktree_handle.read(cx).snapshot(); - let worktree_path = snapshot.abs_path(); let mut buffers_to_delete = Vec::new(); for (buffer_id, buffer) in &self.open_buffers { if let OpenBuffer::Loaded(buffer) = buffer { @@ -1184,8 +1186,7 @@ impl Project { .and_then(|entry_id| snapshot.entry_for_id(entry_id)) { File { - is_local: local, - worktree_path: worktree_path.clone(), + is_local: true, entry_id: Some(entry.id), mtime: entry.mtime, path: entry.path.clone(), @@ -1195,8 +1196,7 @@ impl Project { snapshot.entry_for_path(old_file.path().as_ref()) { File { - is_local: local, - worktree_path: worktree_path.clone(), + is_local: true, entry_id: Some(entry.id), mtime: entry.mtime, path: entry.path.clone(), @@ -1204,8 +1204,7 @@ impl Project { } } else { File { - is_local: local, - worktree_path: worktree_path.clone(), + is_local: true, entry_id: None, path: old_file.path().clone(), mtime: old_file.mtime(), @@ -1213,9 +1212,18 @@ impl Project { } }; - if let Some(task) = buffer.file_updated(Box::new(new_file), cx) { - task.detach(); + if let Some(project_id) = self.remote_id() { + let client = self.client.clone(); + let message = proto::UpdateBufferFile { + project_id, + buffer_id: *buffer_id as u64, + file: Some(new_file.to_proto()), + }; + cx.foreground() + .spawn(async move { client.send(message).await }) + .detach_and_log_err(cx); } + buffer.file_updated(Box::new(new_file), cx).detach(); } }); } else { @@ -1496,6 +1504,31 @@ impl Project { Ok(()) } + pub fn handle_update_buffer_file( + &mut self, + envelope: TypedEnvelope, + _: Arc, + cx: &mut ModelContext, + ) -> Result<()> { + let payload = envelope.payload.clone(); + let buffer_id = payload.buffer_id as usize; + let file = payload.file.ok_or_else(|| anyhow!("invalid file"))?; + let worktree = self + .worktree_for_id(WorktreeId::from_proto(file.worktree_id), cx) + .ok_or_else(|| anyhow!("no such worktree"))?; + let file = File::from_proto(file, worktree.clone(), cx)?; + let buffer = self + .open_buffers + .get_mut(&buffer_id) + .and_then(|b| b.upgrade(cx)) + .ok_or_else(|| anyhow!("no such buffer"))?; + buffer.update(cx, |buffer, cx| { + buffer.file_updated(Box::new(file), cx).detach(); + }); + + Ok(()) + } + pub fn handle_save_buffer( &mut self, envelope: TypedEnvelope, @@ -1662,6 +1695,37 @@ impl Project { Ok(()) } + pub fn handle_buffer_reloaded( + &mut self, + envelope: TypedEnvelope, + _: Arc, + cx: &mut ModelContext, + ) -> Result<()> { + let payload = envelope.payload.clone(); + let buffer = self + .open_buffers + .get(&(payload.buffer_id as usize)) + .and_then(|buf| { + if let OpenBuffer::Loaded(buffer) = buf { + buffer.upgrade(cx) + } else { + None + } + }); + if let Some(buffer) = buffer { + buffer.update(cx, |buffer, cx| { + let version = payload.version.try_into()?; + let mtime = payload + .mtime + .ok_or_else(|| anyhow!("missing mtime"))? + .into(); + buffer.did_reload(version, mtime, cx); + Result::<_, anyhow::Error>::Ok(()) + })?; + } + Ok(()) + } + pub fn match_paths<'a>( &self, query: &'a str, @@ -2185,8 +2249,13 @@ mod tests { cx.update(|cx| { let target_buffer = definition.target_buffer.read(cx); assert_eq!( - target_buffer.file().unwrap().abs_path(), - Some(dir.path().join("a.rs")) + target_buffer + .file() + .unwrap() + .as_local() + .unwrap() + .abs_path(cx), + dir.path().join("a.rs") ); assert_eq!(definition.target_range.to_offset(target_buffer), 9..10); assert_eq!( @@ -2432,7 +2501,7 @@ mod tests { .as_remote_mut() .unwrap() .snapshot - .apply_update(update_message) + .apply_remote_update(update_message) .unwrap(); assert_eq!( diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 719049ce308cb9c32cce31f80f8c50ba61d65cbc..fd33386ffcc5793c2572ba7815d8c370ef19c822 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -30,7 +30,7 @@ use std::{ ffi::{OsStr, OsString}, fmt, future::Future, - ops::Deref, + ops::{Deref, DerefMut}, path::{Path, PathBuf}, sync::{ atomic::{AtomicUsize, Ordering::SeqCst}, @@ -54,9 +54,9 @@ pub enum Worktree { } pub struct LocalWorktree { - snapshot: Snapshot, + snapshot: LocalSnapshot, config: WorktreeConfig, - background_snapshot: Arc>, + background_snapshot: Arc>, last_scan_state_rx: watch::Receiver, _background_scanner_task: Option>, poll_task: Option>, @@ -85,15 +85,34 @@ pub struct RemoteWorktree { #[derive(Clone)] pub struct Snapshot { id: WorktreeId, - scan_id: usize, - abs_path: Arc, root_name: String, root_char_bag: CharBag, - ignores: HashMap, (Arc, usize)>, entries_by_path: SumTree, entries_by_id: SumTree, +} + +#[derive(Clone)] +pub struct LocalSnapshot { + abs_path: Arc, + scan_id: usize, + ignores: HashMap, (Arc, usize)>, removed_entry_ids: HashMap, next_entry_id: Arc, + snapshot: Snapshot, +} + +impl Deref for LocalSnapshot { + type Target = Snapshot; + + fn deref(&self) -> &Self::Target { + &self.snapshot + } +} + +impl DerefMut for LocalSnapshot { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.snapshot + } } #[derive(Clone, Debug)] @@ -112,7 +131,7 @@ enum Registration { struct ShareState { project_id: u64, - snapshots_tx: Sender, + snapshots_tx: Sender, _maintain_remote_snapshot: Option>, } @@ -158,7 +177,7 @@ impl Worktree { let (tree, scan_states_tx) = LocalWorktree::new(client, path, weak, fs.clone(), cx).await?; tree.update(cx, |tree, cx| { let tree = tree.as_local_mut().unwrap(); - let abs_path = tree.snapshot.abs_path.clone(); + let abs_path = tree.abs_path().clone(); let background_snapshot = tree.background_snapshot.clone(); let background = cx.background().clone(); tree._background_scanner_task = Some(cx.background().spawn(async move { @@ -233,15 +252,10 @@ impl Worktree { cx.add_model(|cx: &mut ModelContext| { let snapshot = Snapshot { id: WorktreeId(remote_id as usize), - scan_id: 0, - abs_path: Path::new("").into(), root_name, root_char_bag, - ignores: Default::default(), entries_by_path, entries_by_id, - removed_entry_ids: Default::default(), - next_entry_id: Default::default(), }; let (updates_tx, mut updates_rx) = postage::mpsc::channel(64); @@ -251,7 +265,7 @@ impl Worktree { .spawn(async move { while let Some(update) = updates_rx.recv().await { let mut snapshot = snapshot_tx.borrow().clone(); - if let Err(error) = snapshot.apply_update(update) { + if let Err(error) = snapshot.apply_remote_update(update) { log::error!("error applying worktree update: {}", error); } *snapshot_tx.borrow_mut() = snapshot; @@ -328,7 +342,7 @@ impl Worktree { pub fn snapshot(&self) -> Snapshot { match self { - Worktree::Local(worktree) => worktree.snapshot(), + Worktree::Local(worktree) => worktree.snapshot().snapshot, Worktree::Remote(worktree) => worktree.snapshot(), } } @@ -469,28 +483,28 @@ impl LocalWorktree { let (scan_states_tx, scan_states_rx) = smol::channel::unbounded(); let (mut last_scan_state_tx, last_scan_state_rx) = watch::channel_with(ScanState::Scanning); let tree = cx.add_model(move |cx: &mut ModelContext| { - let mut snapshot = Snapshot { - id: WorktreeId::from_usize(cx.model_id()), - scan_id: 0, + let mut snapshot = LocalSnapshot { abs_path, - root_name: root_name.clone(), - root_char_bag, + scan_id: 0, ignores: Default::default(), - entries_by_path: Default::default(), - entries_by_id: Default::default(), removed_entry_ids: Default::default(), next_entry_id: Arc::new(next_entry_id), + snapshot: Snapshot { + id: WorktreeId::from_usize(cx.model_id()), + root_name: root_name.clone(), + root_char_bag, + entries_by_path: Default::default(), + entries_by_id: Default::default(), + }, }; if let Some(metadata) = metadata { - snapshot.insert_entry( - Entry::new( - path.into(), - &metadata, - &snapshot.next_entry_id, - snapshot.root_char_bag, - ), - fs.as_ref(), + let entry = Entry::new( + path.into(), + &metadata, + &snapshot.next_entry_id, + snapshot.root_char_bag, ); + snapshot.insert_entry(entry, fs.as_ref()); } let tree = Self { @@ -543,6 +557,22 @@ impl LocalWorktree { Ok((tree, scan_states_tx)) } + pub fn abs_path(&self) -> &Arc { + &self.abs_path + } + + pub fn contains_abs_path(&self, path: &Path) -> bool { + path.starts_with(&self.abs_path) + } + + fn absolutize(&self, path: &Path) -> PathBuf { + if path.file_name().is_some() { + self.abs_path.join(path) + } else { + self.abs_path.to_path_buf() + } + } + pub fn authorized_logins(&self) -> Vec { self.config.collaborators.clone() } @@ -624,14 +654,13 @@ impl LocalWorktree { } } - pub fn snapshot(&self) -> Snapshot { + pub fn snapshot(&self) -> LocalSnapshot { self.snapshot.clone() } fn load(&self, path: &Path, cx: &mut ModelContext) -> Task> { let handle = cx.handle(); let path = Arc::from(path); - let worktree_path = self.abs_path.clone(); let abs_path = self.absolutize(&path); let background_snapshot = self.background_snapshot.clone(); let fs = self.fs.clone(); @@ -644,7 +673,6 @@ impl LocalWorktree { File { entry_id: Some(entry.id), worktree: handle, - worktree_path, path: entry.path, mtime: entry.mtime, is_local: true, @@ -664,19 +692,16 @@ impl LocalWorktree { let text = buffer.as_rope().clone(); let version = buffer.version(); let save = self.save(path, text, cx); - cx.spawn(|this, mut cx| async move { + let handle = cx.handle(); + cx.as_mut().spawn(|mut cx| async move { let entry = save.await?; - let file = this.update(&mut cx, |this, cx| { - let this = this.as_local_mut().unwrap(); - File { - entry_id: Some(entry.id), - worktree: cx.handle(), - worktree_path: this.abs_path.clone(), - path: entry.path, - mtime: entry.mtime, - is_local: true, - } - }); + let file = File { + entry_id: Some(entry.id), + worktree: handle, + path: entry.path, + mtime: entry.mtime, + is_local: true, + }; buffer_handle.update(&mut cx, |buffer, cx| { buffer.did_save(version, file.mtime, Some(Box::new(file)), cx); @@ -755,7 +780,8 @@ impl LocalWorktree { let snapshot = self.snapshot(); let rpc = self.client.clone(); let worktree_id = cx.model_id() as u64; - let (snapshots_to_send_tx, snapshots_to_send_rx) = smol::channel::unbounded::(); + let (snapshots_to_send_tx, snapshots_to_send_rx) = + smol::channel::unbounded::(); let maintain_remote_snapshot = cx.background().spawn({ let rpc = rpc.clone(); let snapshot = snapshot.clone(); @@ -811,15 +837,9 @@ impl RemoteWorktree { let replica_id = self.replica_id; let project_id = self.project_id; let remote_worktree_id = self.id(); - let root_path = self.snapshot.abs_path.clone(); let path: Arc = Arc::from(path); let path_string = path.to_string_lossy().to_string(); cx.spawn_weak(move |this, mut cx| async move { - let entry = this - .upgrade(&cx) - .ok_or_else(|| anyhow!("worktree was closed"))? - .read_with(&cx, |tree, _| tree.entry_for_path(&path).cloned()) - .ok_or_else(|| anyhow!("file does not exist"))?; let response = rpc .request(proto::OpenBuffer { project_id, @@ -831,18 +851,15 @@ impl RemoteWorktree { let this = this .upgrade(&cx) .ok_or_else(|| anyhow!("worktree was closed"))?; - let file = File { - entry_id: Some(entry.id), - worktree: this.clone(), - worktree_path: root_path, - path: entry.path, - mtime: entry.mtime, - is_local: false, - }; - let remote_buffer = response.buffer.ok_or_else(|| anyhow!("empty buffer"))?; - Ok(cx.add_model(|cx| { - Buffer::from_proto(replica_id, remote_buffer, Some(Box::new(file)), cx).unwrap() - })) + let mut remote_buffer = response.buffer.ok_or_else(|| anyhow!("empty buffer"))?; + let file = remote_buffer + .file + .take() + .map(|proto| cx.read(|cx| File::from_proto(proto, this.clone(), cx))) + .transpose()? + .map(|file| Box::new(file) as Box); + + Ok(cx.add_model(|cx| Buffer::from_proto(replica_id, remote_buffer, file, cx).unwrap())) }) } @@ -976,10 +993,7 @@ impl Snapshot { } } - pub(crate) fn apply_update(&mut self, update: proto::UpdateWorktree) -> Result<()> { - self.scan_id += 1; - let scan_id = self.scan_id; - + pub(crate) fn apply_remote_update(&mut self, update: proto::UpdateWorktree) -> Result<()> { let mut entries_by_path_edits = Vec::new(); let mut entries_by_id_edits = Vec::new(); for entry_id in update.removed_entries { @@ -1000,7 +1014,7 @@ impl Snapshot { id: entry.id, path: entry.path.clone(), is_ignored: entry.is_ignored, - scan_id, + scan_id: 0, })); entries_by_path_edits.push(Edit::Insert(entry)); } @@ -1087,22 +1101,6 @@ impl Snapshot { } } - pub fn contains_abs_path(&self, path: &Path) -> bool { - path.starts_with(&self.abs_path) - } - - fn absolutize(&self, path: &Path) -> PathBuf { - if path.file_name().is_some() { - self.abs_path.join(path) - } else { - self.abs_path.to_path_buf() - } - } - - pub fn abs_path(&self) -> &Arc { - &self.abs_path - } - pub fn root_entry(&self) -> Option<&Entry> { self.entry_for_path("") } @@ -1132,7 +1130,9 @@ impl Snapshot { pub fn inode_for_path(&self, path: impl AsRef) -> Option { self.entry_for_path(path.as_ref()).map(|e| e.inode) } +} +impl LocalSnapshot { fn insert_entry(&mut self, mut entry: Entry, fs: &dyn Fs) -> Entry { if !entry.is_dir() && entry.path.file_name() == Some(&GITIGNORE) { let abs_path = self.abs_path.join(&entry.path); @@ -1154,12 +1154,13 @@ impl Snapshot { self.reuse_entry_id(&mut entry); self.entries_by_path.insert_or_replace(entry.clone(), &()); + let scan_id = self.scan_id; self.entries_by_id.insert_or_replace( PathEntry { id: entry.id, path: entry.path.clone(), is_ignored: entry.is_ignored, - scan_id: self.scan_id, + scan_id, }, &(), ); @@ -1315,7 +1316,7 @@ impl Deref for Worktree { } impl Deref for LocalWorktree { - type Target = Snapshot; + type Target = LocalSnapshot; fn deref(&self) -> &Self::Target { &self.snapshot @@ -1354,11 +1355,18 @@ pub struct File { pub path: Arc, pub mtime: SystemTime, pub(crate) entry_id: Option, - pub(crate) worktree_path: Arc, pub(crate) is_local: bool, } impl language::File for File { + fn as_local(&self) -> Option<&dyn language::LocalFile> { + if self.is_local { + Some(self) + } else { + None + } + } + fn mtime(&self) -> SystemTime { self.mtime } @@ -1367,30 +1375,20 @@ impl language::File for File { &self.path } - fn abs_path(&self) -> Option { - if self.is_local { - Some(self.worktree_path.join(&self.path)) - } else { - None - } - } - - fn full_path(&self) -> PathBuf { + fn full_path(&self, cx: &AppContext) -> PathBuf { let mut full_path = PathBuf::new(); - if let Some(worktree_name) = self.worktree_path.file_name() { - full_path.push(worktree_name); - } + full_path.push(self.worktree.read(cx).root_name()); full_path.push(&self.path); full_path } /// Returns the last component of this handle's absolute path. If this handle refers to the root /// of its worktree, then this method will return the name of the worktree itself. - fn file_name<'a>(&'a self) -> Option { + fn file_name(&self, cx: &AppContext) -> OsString { self.path .file_name() - .or_else(|| self.worktree_path.file_name()) - .map(Into::into) + .map(|name| name.into()) + .unwrap_or_else(|| OsString::from(&self.worktree.read(cx).root_name)) } fn is_deleted(&self) -> bool { @@ -1444,16 +1442,6 @@ impl language::File for File { }) } - fn load_local(&self, cx: &AppContext) -> Option>> { - let worktree = self.worktree.read(cx).as_local()?; - let abs_path = worktree.absolutize(&self.path); - let fs = worktree.fs.clone(); - Some( - cx.background() - .spawn(async move { fs.load(&abs_path).await }), - ) - } - fn format_remote( &self, buffer_id: u64, @@ -1504,9 +1492,83 @@ impl language::File for File { fn as_any(&self) -> &dyn Any { self } + + fn to_proto(&self) -> rpc::proto::File { + rpc::proto::File { + worktree_id: self.worktree.id() as u64, + entry_id: self.entry_id.map(|entry_id| entry_id as u64), + path: self.path.to_string_lossy().into(), + mtime: Some(self.mtime.into()), + } + } +} + +impl language::LocalFile for File { + fn abs_path(&self, cx: &AppContext) -> PathBuf { + self.worktree + .read(cx) + .as_local() + .unwrap() + .abs_path + .join(&self.path) + } + + fn load(&self, cx: &AppContext) -> Task> { + let worktree = self.worktree.read(cx).as_local().unwrap(); + let abs_path = worktree.absolutize(&self.path); + let fs = worktree.fs.clone(); + cx.background() + .spawn(async move { fs.load(&abs_path).await }) + } + + fn buffer_reloaded( + &self, + buffer_id: u64, + version: &clock::Global, + mtime: SystemTime, + cx: &mut MutableAppContext, + ) { + let worktree = self.worktree.read(cx).as_local().unwrap(); + if let Some(project_id) = worktree.share.as_ref().map(|share| share.project_id) { + let rpc = worktree.client.clone(); + let message = proto::BufferReloaded { + project_id, + buffer_id, + version: version.into(), + mtime: Some(mtime.into()), + }; + cx.background() + .spawn(async move { rpc.send(message).await }) + .detach_and_log_err(cx); + } + } } impl File { + pub fn from_proto( + proto: rpc::proto::File, + worktree: ModelHandle, + cx: &AppContext, + ) -> Result { + let worktree_id = worktree + .read(cx) + .as_remote() + .ok_or_else(|| anyhow!("not remote"))? + .id(); + + if worktree_id.to_proto() != proto.worktree_id { + return Err(anyhow!("worktree id does not match file")); + } + + Ok(Self { + worktree, + path: Path::new(&proto.path).into(), + mtime: proto.mtime.ok_or_else(|| anyhow!("no timestamp"))?.into(), + entry_id: proto.entry_id.map(|entry_id| entry_id as usize), + is_local: false, + }) + } + pub fn from_dyn(file: Option<&dyn language::File>) -> Option<&Self> { file.and_then(|f| f.as_any().downcast_ref()) } @@ -1690,14 +1752,14 @@ impl<'a> sum_tree::Dimension<'a, EntrySummary> for PathKey { struct BackgroundScanner { fs: Arc, - snapshot: Arc>, + snapshot: Arc>, notify: Sender, executor: Arc, } impl BackgroundScanner { fn new( - snapshot: Arc>, + snapshot: Arc>, notify: Sender, fs: Arc, executor: Arc, @@ -1714,7 +1776,7 @@ impl BackgroundScanner { self.snapshot.lock().abs_path.clone() } - fn snapshot(&self) -> Snapshot { + fn snapshot(&self) -> LocalSnapshot { self.snapshot.lock().clone() } @@ -2057,7 +2119,7 @@ impl BackgroundScanner { .await; } - async fn update_ignore_status(&self, job: UpdateIgnoreStatusJob, snapshot: &Snapshot) { + async fn update_ignore_status(&self, job: UpdateIgnoreStatusJob, snapshot: &LocalSnapshot) { let mut ignore_stack = job.ignore_stack; if let Some((ignore, _)) = snapshot.ignores.get(&job.path) { ignore_stack = ignore_stack.append(job.path.clone(), ignore.clone()); @@ -2101,7 +2163,7 @@ impl BackgroundScanner { async fn refresh_entry( fs: &dyn Fs, - snapshot: &Mutex, + snapshot: &Mutex, path: Arc, abs_path: &Path, ) -> Result { @@ -2169,7 +2231,7 @@ impl WorktreeHandle for ModelHandle { use smol::future::FutureExt; let filename = "fs-event-sentinel"; - let root_path = cx.read(|cx| self.read(cx).abs_path.clone()); + let root_path = cx.read(|cx| self.read(cx).as_local().unwrap().abs_path().clone()); let tree = self.clone(); async move { std::fs::write(root_path.join(filename), "").unwrap(); @@ -2521,17 +2583,19 @@ mod tests { let (notify_tx, _notify_rx) = smol::channel::unbounded(); let fs = Arc::new(RealFs); let next_entry_id = Arc::new(AtomicUsize::new(0)); - let mut initial_snapshot = Snapshot { - id: WorktreeId::from_usize(0), - scan_id: 0, + let mut initial_snapshot = LocalSnapshot { abs_path: root_dir.path().into(), - entries_by_path: Default::default(), - entries_by_id: Default::default(), + scan_id: 0, removed_entry_ids: Default::default(), ignores: Default::default(), - root_name: Default::default(), - root_char_bag: Default::default(), next_entry_id: next_entry_id.clone(), + snapshot: Snapshot { + id: WorktreeId::from_usize(0), + entries_by_path: Default::default(), + entries_by_id: Default::default(), + root_name: Default::default(), + root_char_bag: Default::default(), + }, }; initial_snapshot.insert_entry( Entry::new( @@ -2612,7 +2676,7 @@ mod tests { let update = scanner .snapshot() .build_update(&prev_snapshot, 0, 0, include_ignored); - prev_snapshot.apply_update(update).unwrap(); + prev_snapshot.apply_remote_update(update).unwrap(); assert_eq!( prev_snapshot.to_vec(true), scanner.snapshot().to_vec(include_ignored) @@ -2767,7 +2831,7 @@ mod tests { .collect() } - impl Snapshot { + impl LocalSnapshot { fn check_invariants(&self) { let mut files = self.files(true, 0); let mut visible_files = self.files(false, 0); diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 79e41f7704fea718f16fa62362df493849665a4f..cbe7ca5d1adf80c6844186efbccd2396a4f48884 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -33,25 +33,27 @@ message Envelope { OpenBufferResponse open_buffer_response = 25; CloseBuffer close_buffer = 26; UpdateBuffer update_buffer = 27; - SaveBuffer save_buffer = 28; - BufferSaved buffer_saved = 29; - FormatBuffer format_buffer = 30; - - GetChannels get_channels = 31; - GetChannelsResponse get_channels_response = 32; - JoinChannel join_channel = 33; - JoinChannelResponse join_channel_response = 34; - LeaveChannel leave_channel = 35; - SendChannelMessage send_channel_message = 36; - SendChannelMessageResponse send_channel_message_response = 37; - ChannelMessageSent channel_message_sent = 38; - GetChannelMessages get_channel_messages = 39; - GetChannelMessagesResponse get_channel_messages_response = 40; - - UpdateContacts update_contacts = 41; - - GetUsers get_users = 42; - GetUsersResponse get_users_response = 43; + UpdateBufferFile update_buffer_file = 28; + SaveBuffer save_buffer = 29; + BufferSaved buffer_saved = 30; + BufferReloaded buffer_reloaded = 31; + FormatBuffer format_buffer = 32; + + GetChannels get_channels = 33; + GetChannelsResponse get_channels_response = 34; + JoinChannel join_channel = 35; + JoinChannelResponse join_channel_response = 36; + LeaveChannel leave_channel = 37; + SendChannelMessage send_channel_message = 38; + SendChannelMessageResponse send_channel_message_response = 39; + ChannelMessageSent channel_message_sent = 40; + GetChannelMessages get_channel_messages = 41; + GetChannelMessagesResponse get_channel_messages_response = 42; + + UpdateContacts update_contacts = 43; + + GetUsers get_users = 44; + GetUsersResponse get_users_response = 45; } } @@ -88,9 +90,9 @@ message JoinProject { } message JoinProjectResponse { - uint32 replica_id = 2; - repeated Worktree worktrees = 3; - repeated Collaborator collaborators = 4; + uint32 replica_id = 1; + repeated Worktree worktrees = 2; + repeated Collaborator collaborators = 3; } message LeaveProject { @@ -150,7 +152,13 @@ message CloseBuffer { message UpdateBuffer { uint64 project_id = 1; uint64 buffer_id = 2; - repeated Operation operations = 4; + repeated Operation operations = 3; +} + +message UpdateBufferFile { + uint64 project_id = 1; + uint64 buffer_id = 2; + File file = 3; } message SaveBuffer { @@ -165,6 +173,13 @@ message BufferSaved { Timestamp mtime = 4; } +message BufferReloaded { + uint64 project_id = 1; + uint64 buffer_id = 2; + repeated VectorClockEntry version = 3; + Timestamp mtime = 4; +} + message FormatBuffer { uint64 project_id = 1; uint64 buffer_id = 2; @@ -177,11 +192,11 @@ message UpdateDiagnosticSummary { } message DiagnosticSummary { - string path = 3; - uint32 error_count = 4; - uint32 warning_count = 5; - uint32 info_count = 6; - uint32 hint_count = 7; + string path = 1; + uint32 error_count = 2; + uint32 warning_count = 3; + uint32 info_count = 4; + uint32 hint_count = 5; } message DiskBasedDiagnosticsUpdating { @@ -270,6 +285,13 @@ message Worktree { bool weak = 5; } +message File { + uint64 worktree_id = 1; + optional uint64 entry_id = 2; + string path = 3; + Timestamp mtime = 4; +} + message Entry { uint64 id = 1; bool is_dir = 2; @@ -282,15 +304,16 @@ message Entry { message Buffer { uint64 id = 1; - string visible_text = 2; - string deleted_text = 3; - repeated BufferFragment fragments = 4; - repeated UndoMapEntry undo_map = 5; - repeated VectorClockEntry version = 6; - repeated SelectionSet selections = 7; - repeated Diagnostic diagnostics = 8; - uint32 lamport_timestamp = 9; - repeated Operation deferred_operations = 10; + optional File file = 2; + string visible_text = 3; + string deleted_text = 4; + repeated BufferFragment fragments = 5; + repeated UndoMapEntry undo_map = 6; + repeated VectorClockEntry version = 7; + repeated SelectionSet selections = 8; + repeated Diagnostic diagnostics = 9; + uint32 lamport_timestamp = 10; + repeated Operation deferred_operations = 11; } message BufferFragment { @@ -299,7 +322,7 @@ message BufferFragment { uint32 lamport_timestamp = 3; uint32 insertion_offset = 4; uint32 len = 5; - bool visible = 6; + bool visible = 6; repeated VectorClockEntry deletions = 7; repeated VectorClockEntry max_undos = 8; } @@ -383,8 +406,8 @@ message Operation { message UpdateSelections { uint32 replica_id = 1; - uint32 lamport_timestamp = 3; - repeated Selection selections = 4; + uint32 lamport_timestamp = 2; + repeated Selection selections = 3; } } diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 8860bc5f0549b0a4341e5fe85526c299a5f1fa24..cebe3504e9b3ce99d554163fbee026b4e9f50a14 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -122,6 +122,7 @@ macro_rules! entity_messages { messages!( Ack, AddProjectCollaborator, + BufferReloaded, BufferSaved, ChannelMessageSent, CloseBuffer, @@ -157,6 +158,7 @@ messages!( UnregisterWorktree, UnshareProject, UpdateBuffer, + UpdateBufferFile, UpdateContacts, UpdateDiagnosticSummary, UpdateWorktree, @@ -183,6 +185,7 @@ request_messages!( entity_messages!( project_id, AddProjectCollaborator, + BufferReloaded, BufferSaved, CloseBuffer, DiskBasedDiagnosticsUpdated, @@ -197,6 +200,7 @@ entity_messages!( UnregisterWorktree, UnshareProject, UpdateBuffer, + UpdateBufferFile, UpdateDiagnosticSummary, UpdateWorktree, ); diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 4ad23826999063335669da3e8d829ea4bc917dee..c8af5ce1fd934bc10cf7f378317ca2a407e91f93 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -77,6 +77,8 @@ impl Server { .add_handler(Server::open_buffer) .add_handler(Server::close_buffer) .add_handler(Server::update_buffer) + .add_handler(Server::update_buffer_file) + .add_handler(Server::buffer_reloaded) .add_handler(Server::buffer_saved) .add_handler(Server::save_buffer) .add_handler(Server::format_buffer) @@ -704,6 +706,38 @@ impl Server { Ok(()) } + async fn update_buffer_file( + self: Arc, + request: TypedEnvelope, + ) -> tide::Result<()> { + let receiver_ids = self + .state() + .project_connection_ids(request.payload.project_id, request.sender_id) + .ok_or_else(|| anyhow!(NO_SUCH_PROJECT))?; + broadcast(request.sender_id, receiver_ids, |connection_id| { + self.peer + .forward_send(request.sender_id, connection_id, request.payload.clone()) + }) + .await?; + Ok(()) + } + + async fn buffer_reloaded( + self: Arc, + request: TypedEnvelope, + ) -> tide::Result<()> { + let receiver_ids = self + .state() + .project_connection_ids(request.payload.project_id, request.sender_id) + .ok_or_else(|| anyhow!(NO_SUCH_PROJECT))?; + broadcast(request.sender_id, receiver_ids, |connection_id| { + self.peer + .forward_send(request.sender_id, connection_id, request.payload.clone()) + }) + .await?; + Ok(()) + } + async fn buffer_saved( self: Arc, request: TypedEnvelope, @@ -1470,9 +1504,7 @@ mod tests { .condition(&mut cx_a, |buf, _| buf.text() == "i-am-c, i-am-b, i-am-a") .await; buffer_b - .condition(&mut cx_b, |buf, _| { - dbg!(buf.text()) == "i-am-c, i-am-b, i-am-a" - }) + .condition(&mut cx_b, |buf, _| buf.text() == "i-am-c, i-am-b, i-am-a") .await; buffer_c .condition(&mut cx_c, |buf, _| buf.text() == "i-am-c, i-am-b, i-am-a") @@ -1490,7 +1522,10 @@ mod tests { buffer_b.read_with(&cx_b, |buf, _| assert!(!buf.is_dirty())); buffer_c.condition(&cx_c, |buf, _| !buf.is_dirty()).await; - // Make changes on host's file system, see those changes on the guests. + // Make changes on host's file system, see those changes on guest worktrees. + fs.rename("/a/file1".as_ref(), "/a/file1-renamed".as_ref()) + .await + .unwrap(); fs.rename("/a/file2".as_ref(), "/a/file3".as_ref()) .await .unwrap(); @@ -1498,18 +1533,29 @@ mod tests { .await .unwrap(); + worktree_a + .condition(&cx_a, |tree, _| tree.file_count() == 4) + .await; worktree_b .condition(&cx_b, |tree, _| tree.file_count() == 4) .await; worktree_c .condition(&cx_c, |tree, _| tree.file_count() == 4) .await; + worktree_a.read_with(&cx_a, |tree, _| { + assert_eq!( + tree.paths() + .map(|p| p.to_string_lossy()) + .collect::>(), + &[".zed.toml", "file1-renamed", "file3", "file4"] + ) + }); worktree_b.read_with(&cx_b, |tree, _| { assert_eq!( tree.paths() .map(|p| p.to_string_lossy()) .collect::>(), - &[".zed.toml", "file1", "file3", "file4"] + &[".zed.toml", "file1-renamed", "file3", "file4"] ) }); worktree_c.read_with(&cx_c, |tree, _| { @@ -1517,9 +1563,26 @@ mod tests { tree.paths() .map(|p| p.to_string_lossy()) .collect::>(), - &[".zed.toml", "file1", "file3", "file4"] + &[".zed.toml", "file1-renamed", "file3", "file4"] ) }); + + // Ensure buffer files are updated as well. + buffer_a + .condition(&cx_a, |buf, _| { + buf.file().unwrap().path().to_str() == Some("file1-renamed") + }) + .await; + buffer_b + .condition(&cx_b, |buf, _| { + buf.file().unwrap().path().to_str() == Some("file1-renamed") + }) + .await; + buffer_c + .condition(&cx_c, |buf, _| { + buf.file().unwrap().path().to_str() == Some("file1-renamed") + }) + .await; } #[gpui::test] @@ -1615,6 +1678,88 @@ mod tests { }); } + #[gpui::test] + async fn test_buffer_reloading(mut cx_a: TestAppContext, mut cx_b: TestAppContext) { + cx_a.foreground().forbid_parking(); + let lang_registry = Arc::new(LanguageRegistry::new()); + let fs = Arc::new(FakeFs::new()); + + // Connect to a server as 2 clients. + let mut server = TestServer::start(cx_a.foreground()).await; + let client_a = server.create_client(&mut cx_a, "user_a").await; + let client_b = server.create_client(&mut cx_b, "user_b").await; + + // Share a project as client A + fs.insert_tree( + "/dir", + json!({ + ".zed.toml": r#"collaborators = ["user_b", "user_c"]"#, + "a.txt": "a-contents", + }), + ) + .await; + + let project_a = cx_a.update(|cx| { + Project::local( + client_a.clone(), + client_a.user_store.clone(), + lang_registry.clone(), + fs.clone(), + cx, + ) + }); + let (worktree_a, _) = project_a + .update(&mut cx_a, |p, cx| { + p.find_or_create_local_worktree("/dir", false, cx) + }) + .await + .unwrap(); + worktree_a + .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) + .await; + let project_id = project_a.update(&mut cx_a, |p, _| p.next_remote_id()).await; + let worktree_id = worktree_a.read_with(&cx_a, |tree, _| tree.id()); + project_a + .update(&mut cx_a, |p, cx| p.share(cx)) + .await + .unwrap(); + + // Join that project as client B + let project_b = Project::remote( + project_id, + client_b.clone(), + client_b.user_store.clone(), + lang_registry.clone(), + fs.clone(), + &mut cx_b.to_async(), + ) + .await + .unwrap(); + let _worktree_b = project_b.update(&mut cx_b, |p, cx| p.worktrees(cx).next().unwrap()); + + // Open a buffer as client B + let buffer_b = project_b + .update(&mut cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) + .await + .unwrap(); + buffer_b.read_with(&cx_b, |buf, _| { + assert!(!buf.is_dirty()); + assert!(!buf.has_conflict()); + }); + + fs.save(Path::new("/dir/a.txt"), &"new contents".into()) + .await + .unwrap(); + buffer_b + .condition(&cx_b, |buf, _| { + buf.text() == "new contents" && !buf.is_dirty() + }) + .await; + buffer_b.read_with(&cx_b, |buf, _| { + assert!(!buf.has_conflict()); + }); + } + #[gpui::test] async fn test_editing_while_guest_opens_buffer( mut cx_a: TestAppContext,