From d5218fbac5432f71c9480a2e0f24ab1ed665b131 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 11 May 2021 11:38:10 -0700 Subject: [PATCH 1/9] Add failing test for buffer detecting on-disk changes --- zed/src/editor/buffer/mod.rs | 97 ++++++++++++++++++++++++++++++++++-- 1 file changed, 94 insertions(+), 3 deletions(-) diff --git a/zed/src/editor/buffer/mod.rs b/zed/src/editor/buffer/mod.rs index d42c3460cf470565a5f5f66f30abadf35f43bea4..2d578c2c0bf6f6d85e9415aef51532807082ffbf 100644 --- a/zed/src/editor/buffer/mod.rs +++ b/zed/src/editor/buffer/mod.rs @@ -509,6 +509,10 @@ impl Buffer { self.version > self.saved_version || self.file.as_ref().map_or(false, |f| f.is_deleted()) } + pub fn has_conflict(&self) -> bool { + false + } + pub fn version(&self) -> time::Global { self.version.clone() } @@ -2381,7 +2385,10 @@ impl ToPoint for usize { #[cfg(test)] mod tests { use super::*; - use crate::{test::temp_tree, worktree::Worktree}; + use crate::{ + test::temp_tree, + worktree::{Worktree, WorktreeHandle}, + }; use cmp::Ordering; use gpui::App; use serde_json::json; @@ -2989,8 +2996,6 @@ mod tests { #[test] fn test_is_dirty() { - use crate::worktree::WorktreeHandle; - App::test_async((), |mut app| async move { let dir = temp_tree(json!({ "file1": "", @@ -3105,6 +3110,92 @@ mod tests { }); } + #[test] + fn test_file_changes_on_disk() { + App::test_async((), |mut app| async move { + let initial_contents = "aaa\nbbb\nccc\n"; + let dir = temp_tree(json!({ "the-file": initial_contents })); + let tree = app.add_model(|ctx| Worktree::new(dir.path(), ctx)); + app.read(|ctx| tree.read(ctx).scan_complete()).await; + + let abs_path = dir.path().join("the-file"); + let file = app.read(|ctx| tree.file("the-file", ctx)); + let buffer = app.add_model(|ctx| { + Buffer::from_history(0, History::new(initial_contents.into()), Some(file), ctx) + }); + + // Add a cursor at the start of each row. + let (selection_set_id, _) = buffer.update(&mut app, |buffer, ctx| { + assert!(!buffer.is_dirty()); + buffer.add_selection_set( + (0..3) + .map(|row| { + let anchor = buffer + .anchor_at(Point::new(row, 0), AnchorBias::Left) + .unwrap(); + Selection { + id: row as usize, + start: anchor.clone(), + end: anchor, + reversed: false, + goal: SelectionGoal::None, + } + }) + .collect::>(), + Some(ctx), + ) + }); + + // Change the file on disk, adding a new line of text before each existing line. + buffer.update(&mut app, |buffer, _| { + assert!(!buffer.is_dirty()); + assert!(!buffer.has_conflict()); + }); + tree.flush_fs_events(&app).await; + let new_contents = "AAA\naaa\nBBB\nbbb\nCCC\nccc\n"; + fs::write(&abs_path, new_contents).unwrap(); + + // Because the buffer was not modified, it is reloaded from disk. Its + // contents are edited according to the diff between the old and new + // file contents. + buffer + .condition(&app, |buffer, _| buffer.text() == new_contents) + .await; + buffer.update(&mut app, |buffer, _| { + let selections = buffer.selections(selection_set_id).unwrap(); + let cursor_positions = selections + .iter() + .map(|selection| { + assert_eq!(selection.start, selection.end); + selection.start.to_point(&buffer).unwrap() + }) + .collect::>(); + assert_eq!( + cursor_positions, + &[Point::new(1, 0), Point::new(3, 0), Point::new(5, 0),] + ); + }); + + // Modify the buffer + buffer.update(&mut app, |buffer, ctx| { + assert!(!buffer.is_dirty()); + assert!(!buffer.has_conflict()); + + buffer.edit(vec![0..0], " ", Some(ctx)).unwrap(); + assert!(buffer.is_dirty()); + }); + + // Change the file on disk again, adding blank lines to the beginning. + fs::write(&abs_path, "\n\n\nAAA\naaa\nBBB\nbbb\nCCC\nccc\n").unwrap(); + + // Becaues the buffer is modified, it doesn't reload from disk, but is + // marked as having a conflict. + buffer + .condition(&app, |buffer, _| buffer.has_conflict()) + .await; + }); + } + #[test] fn test_undo_redo() { App::test((), |app| { From 040189db3c543f937a4f0dceaabb9b81311d4476 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 11 May 2021 14:48:57 -0700 Subject: [PATCH 2/9] Add basic handling for buffer's files changing on disk --- Cargo.lock | 7 ++ zed/Cargo.toml | 1 + zed/src/editor/buffer/mod.rs | 135 ++++++++++++++++++++++++++++++----- zed/src/worktree.rs | 81 ++++++++++++++------- 4 files changed, 182 insertions(+), 42 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1fba36fd1210a8ddb0427046923b468f11463463..06ce5843d4df58b2f24d15479184e49b283b986b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2442,6 +2442,12 @@ dependencies = [ "libc", ] +[[package]] +name = "similar" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad1d488a557b235fc46dae55512ffbfc429d2482b08b4d9435ab07384ca8aec" + [[package]] name = "simplecss" version = "0.2.0" @@ -2967,6 +2973,7 @@ dependencies = [ "seahash", "serde 1.0.125", "serde_json 1.0.64", + "similar", "simplelog", "smallvec", "smol", diff --git a/zed/Cargo.toml b/zed/Cargo.toml index d146387752a789fd97cf9cf429ea78d2c3bf4fd4..2302fc650954c18cb5ee21b902087436575e8006 100644 --- a/zed/Cargo.toml +++ b/zed/Cargo.toml @@ -34,6 +34,7 @@ rand = "0.8.3" rust-embed = "5.9.0" seahash = "4.1" serde = {version = "1", features = ["derive"]} +similar = "1.3" simplelog = "0.9" smallvec = "1.6.1" smol = "1.2.5" diff --git a/zed/src/editor/buffer/mod.rs b/zed/src/editor/buffer/mod.rs index 2d578c2c0bf6f6d85e9415aef51532807082ffbf..82881afca3975f2259ea1b4a123c8cc7e13c198d 100644 --- a/zed/src/editor/buffer/mod.rs +++ b/zed/src/editor/buffer/mod.rs @@ -8,6 +8,7 @@ use futures_core::future::LocalBoxFuture; pub use point::*; use seahash::SeaHasher; pub use selection::*; +use similar::{ChangeTag, TextDiff}; use smol::future::FutureExt; pub use text::*; @@ -19,7 +20,7 @@ use crate::{ worktree::FileHandle, }; use anyhow::{anyhow, Result}; -use gpui::{Entity, ModelContext}; +use gpui::{Entity, EntityTask, ModelContext}; use lazy_static::lazy_static; use rand::prelude::*; use std::{ @@ -29,7 +30,7 @@ use std::{ ops::{AddAssign, Range}, str, sync::Arc, - time::{Duration, Instant}, + time::{Duration, Instant, SystemTime, UNIX_EPOCH}, }; const UNDO_GROUP_INTERVAL: Duration = Duration::from_millis(300); @@ -62,6 +63,7 @@ pub struct Buffer { insertion_splits: HashMap>, pub version: time::Global, saved_version: time::Global, + saved_mtime: SystemTime, last_edit: time::Local, undo_map: UndoMap, history: History, @@ -378,8 +380,27 @@ impl Buffer { ) -> Self { if let Some(file) = file.as_ref() { file.observe_from_model(ctx, |this, file, ctx| { - if this.version == this.saved_version && file.is_deleted() { - ctx.emit(Event::Dirtied); + let version = this.version.clone(); + if this.version == this.saved_version { + if file.is_deleted() { + ctx.emit(Event::Dirtied); + } else { + ctx.spawn( + file.load_history(ctx.as_ref()), + move |this, history, ctx| { + if let (Ok(history), true) = (history, this.version == version) { + let task = this.set_text_via_diff(history.base_text, ctx); + ctx.spawn(task, |this, ops, _| { + if ops.is_some() { + this.saved_version = this.version.clone(); + } + }) + .detach(); + } + }, + ) + .detach() + } } ctx.emit(Event::FileHandleChanged); }); @@ -447,6 +468,7 @@ impl Buffer { insertion_splits, version: time::Global::new(), saved_version: time::Global::new(), + saved_mtime: UNIX_EPOCH, last_edit: time::Local::default(), undo_map: Default::default(), history, @@ -501,16 +523,70 @@ impl Buffer { if file.is_some() { self.file = file; } + if let Some(file) = &self.file { + self.saved_mtime = file.mtime(); + } self.saved_version = version; ctx.emit(Event::Saved); } + fn set_text_via_diff( + &mut self, + new_text: Arc, + ctx: &mut ModelContext, + ) -> EntityTask>> { + let version = self.version.clone(); + let old_text = self.text(); + ctx.spawn( + ctx.background_executor().spawn({ + let new_text = new_text.clone(); + async move { + TextDiff::from_lines(old_text.as_str(), new_text.as_ref()) + .iter_all_changes() + .map(|c| (c.tag(), c.value().len())) + .collect::>() + } + }), + move |this, diff, ctx| { + if this.version == version { + this.start_transaction(None).unwrap(); + let mut operations = Vec::new(); + let mut offset = 0; + for (tag, len) in diff { + let range = offset..(offset + len); + match tag { + ChangeTag::Equal => offset += len, + ChangeTag::Delete => operations + .extend_from_slice(&this.edit(Some(range), "", Some(ctx)).unwrap()), + ChangeTag::Insert => { + operations.extend_from_slice( + &this + .edit(Some(offset..offset), &new_text[range], Some(ctx)) + .unwrap(), + ); + offset += len; + } + } + } + this.end_transaction(None, Some(ctx)).unwrap(); + Some(operations) + } else { + None + } + }, + ) + } + pub fn is_dirty(&self) -> bool { self.version > self.saved_version || self.file.as_ref().map_or(false, |f| f.is_deleted()) } pub fn has_conflict(&self) -> bool { - false + self.version > self.saved_version + && self + .file + .as_ref() + .map_or(false, |f| f.mtime() != self.saved_mtime) } pub fn version(&self) -> time::Global { @@ -1823,6 +1899,7 @@ impl Clone for Buffer { insertion_splits: self.insertion_splits.clone(), version: self.version.clone(), saved_version: self.saved_version.clone(), + saved_mtime: self.saved_mtime, last_edit: self.last_edit.clone(), undo_map: self.undo_map.clone(), history: self.history.clone(), @@ -3003,6 +3080,7 @@ mod tests { "file3": "", })); let tree = app.add_model(|ctx| Worktree::new(dir.path(), ctx)); + tree.flush_fs_events(&app).await; app.read(|ctx| tree.read(ctx).scan_complete()).await; let file1 = app.read(|ctx| tree.file("file1", ctx)); @@ -3075,7 +3153,6 @@ mod tests { Buffer::from_history(0, History::new("abc".into()), Some(file2), ctx) }); - tree.flush_fs_events(&app).await; fs::remove_file(dir.path().join("file2")).unwrap(); tree.update(&mut app, |tree, ctx| tree.next_scan_complete(ctx)) .await; @@ -3113,7 +3190,7 @@ mod tests { #[test] fn test_file_changes_on_disk() { App::test_async((), |mut app| async move { - let initial_contents = "aaa\nbbb\nccc\n"; + let initial_contents = "aaa\nbbbbb\nc\n"; let dir = temp_tree(json!({ "the-file": initial_contents })); let tree = app.add_model(|ctx| Worktree::new(dir.path(), ctx)); app.read(|ctx| tree.read(ctx).scan_complete()).await; @@ -3131,7 +3208,7 @@ mod tests { (0..3) .map(|row| { let anchor = buffer - .anchor_at(Point::new(row, 0), AnchorBias::Left) + .anchor_at(Point::new(row, 0), AnchorBias::Right) .unwrap(); Selection { id: row as usize, @@ -3146,22 +3223,31 @@ mod tests { ) }); - // Change the file on disk, adding a new line of text before each existing line. + // Change the file on disk, adding two new lines of text, and removing + // one line. buffer.update(&mut app, |buffer, _| { assert!(!buffer.is_dirty()); assert!(!buffer.has_conflict()); }); tree.flush_fs_events(&app).await; - let new_contents = "AAA\naaa\nBBB\nbbb\nCCC\nccc\n"; + let new_contents = "AAAA\naaa\nBB\nbbbbb\n"; + fs::write(&abs_path, new_contents).unwrap(); // Because the buffer was not modified, it is reloaded from disk. Its // contents are edited according to the diff between the old and new // file contents. buffer - .condition(&app, |buffer, _| buffer.text() == new_contents) + .condition_with_duration(Duration::from_millis(500), &app, |buffer, _| { + buffer.text() != initial_contents + }) .await; + buffer.update(&mut app, |buffer, _| { + assert_eq!(buffer.text(), new_contents); + assert!(!buffer.is_dirty()); + assert!(!buffer.has_conflict()); + let selections = buffer.selections(selection_set_id).unwrap(); let cursor_positions = selections .iter() @@ -3172,21 +3258,18 @@ mod tests { .collect::>(); assert_eq!( cursor_positions, - &[Point::new(1, 0), Point::new(3, 0), Point::new(5, 0),] + &[Point::new(1, 0), Point::new(3, 0), Point::new(4, 0),] ); }); // Modify the buffer buffer.update(&mut app, |buffer, ctx| { - assert!(!buffer.is_dirty()); - assert!(!buffer.has_conflict()); - buffer.edit(vec![0..0], " ", Some(ctx)).unwrap(); assert!(buffer.is_dirty()); }); // Change the file on disk again, adding blank lines to the beginning. - fs::write(&abs_path, "\n\n\nAAA\naaa\nBBB\nbbb\nCCC\nccc\n").unwrap(); + fs::write(&abs_path, "\n\n\nAAAA\naaa\nBB\nbbbbb\n").unwrap(); // Becaues the buffer is modified, it doesn't reload from disk, but is // marked as having a conflict. @@ -3196,6 +3279,26 @@ mod tests { }); } + #[test] + fn test_set_text_via_diff() { + App::test_async((), |mut app| async move { + let text = "a\nbb\nccc\ndddd\neeeee\nffffff\n"; + let buffer = app.add_model(|ctx| Buffer::new(0, text, ctx)); + + let text = "a\nccc\ndddd\nffffff\n"; + buffer + .update(&mut app, |b, ctx| b.set_text_via_diff(text.into(), ctx)) + .await; + app.read(|ctx| assert_eq!(buffer.read(ctx).text(), text)); + + let text = "a\n1\n\nccc\ndd2dd\nffffff\n"; + buffer + .update(&mut app, |b, ctx| b.set_text_via_diff(text.into(), ctx)) + .await; + app.read(|ctx| assert_eq!(buffer.read(ctx).text(), text)); + }); + } + #[test] fn test_undo_redo() { App::test((), |app| { diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index be895533f123a1fcd713bf26122f1da739101e4c..a653501b517cd736dc5cc139a3840b3bcd06da52 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -28,7 +28,7 @@ use std::{ os::unix::{ffi::OsStrExt, fs::MetadataExt}, path::{Path, PathBuf}, sync::{Arc, Weak}, - time::Duration, + time::{Duration, SystemTime, UNIX_EPOCH}, }; use self::{char_bag::CharBag, ignore::IgnoreStack}; @@ -63,6 +63,7 @@ pub struct FileHandle { struct FileHandleState { path: Arc, is_deleted: bool, + mtime: SystemTime, } impl Worktree { @@ -435,6 +436,10 @@ impl FileHandle { self.state.lock().is_deleted } + pub fn mtime(&self) -> SystemTime { + self.state.lock().mtime + } + pub fn exists(&self) -> bool { !self.is_deleted() } @@ -905,41 +910,63 @@ impl BackgroundScanner { }; let mut renamed_paths: HashMap = HashMap::new(); + let mut handles = self.handles.lock(); let mut updated_handles = HashMap::new(); for event in &events { + let path = if let Ok(path) = event.path.strip_prefix(&root_abs_path) { + path + } else { + continue; + }; + + let metadata = fs::metadata(&event.path); if event.flags.contains(fsevent::StreamFlags::ITEM_RENAMED) { - if let Ok(path) = event.path.strip_prefix(&root_abs_path) { - if let Some(inode) = snapshot.inode_for_path(path) { - renamed_paths.insert(inode, path.to_path_buf()); - } else if let Ok(metadata) = fs::metadata(&event.path) { - let new_path = path; - let mut handles = self.handles.lock(); - if let Some(old_path) = renamed_paths.get(&metadata.ino()) { - handles.retain(|handle_path, handle_state| { - if let Ok(path_suffix) = handle_path.strip_prefix(&old_path) { - let new_handle_path: Arc = - if path_suffix.file_name().is_some() { - new_path.join(path_suffix) - } else { - new_path.to_path_buf() - } - .into(); - if let Some(handle_state) = Weak::upgrade(&handle_state) { - handle_state.lock().path = new_handle_path.clone(); - updated_handles - .insert(new_handle_path, Arc::downgrade(&handle_state)); + if let Some(inode) = snapshot.inode_for_path(path) { + renamed_paths.insert(inode, path.to_path_buf()); + } else if let Ok(metadata) = &metadata { + let new_path = path; + if let Some(old_path) = renamed_paths.get(&metadata.ino()) { + handles.retain(|handle_path, handle_state| { + if let Ok(path_suffix) = handle_path.strip_prefix(&old_path) { + let new_handle_path: Arc = + if path_suffix.file_name().is_some() { + new_path.join(path_suffix) + } else { + new_path.to_path_buf() } - false - } else { - true + .into(); + if let Some(handle_state) = Weak::upgrade(&handle_state) { + let mut state = handle_state.lock(); + state.path = new_handle_path.clone(); + updated_handles + .insert(new_handle_path, Arc::downgrade(&handle_state)); } - }); - handles.extend(updated_handles.drain()); + false + } else { + true + } + }); + handles.extend(updated_handles.drain()); + } + } + } + + for state in handles.values_mut() { + if let Some(state) = Weak::upgrade(&state) { + let mut state = state.lock(); + if state.path.as_ref() == path { + if let Ok(metadata) = &metadata { + state.mtime = metadata.modified().unwrap(); + } + } else if state.path.starts_with(path) { + if let Ok(metadata) = fs::metadata(state.path.as_ref()) { + state.mtime = metadata.modified().unwrap(); } } } } } + drop(handles); events.sort_unstable_by(|a, b| a.path.cmp(&b.path)); let mut abs_paths = events.into_iter().map(|e| e.path).peekable(); @@ -1188,11 +1215,13 @@ impl WorktreeHandle for ModelHandle { FileHandleState { path: entry.path().clone(), is_deleted: false, + mtime: UNIX_EPOCH, } } else { FileHandleState { path: path.into(), is_deleted: !tree.path_is_pending(path), + mtime: UNIX_EPOCH, } }; From c54a49e5d14331fcc4b047379be434b68554ddd4 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 11 May 2021 16:59:31 -0700 Subject: [PATCH 3/9] Use a different tab icon color for buffers with conflicts --- zed/src/editor/buffer/mod.rs | 7 +++++-- zed/src/editor/buffer_view.rs | 5 +++++ zed/src/workspace.rs | 8 ++++++++ zed/src/workspace/pane.rs | 27 +++++++++++++++++++-------- 4 files changed, 37 insertions(+), 10 deletions(-) diff --git a/zed/src/editor/buffer/mod.rs b/zed/src/editor/buffer/mod.rs index 82881afca3975f2259ea1b4a123c8cc7e13c198d..1951a764b7c368df37dabb39d41ef3ac6390d343 100644 --- a/zed/src/editor/buffer/mod.rs +++ b/zed/src/editor/buffer/mod.rs @@ -390,9 +390,11 @@ impl Buffer { move |this, history, ctx| { if let (Ok(history), true) = (history, this.version == version) { let task = this.set_text_via_diff(history.base_text, ctx); - ctx.spawn(task, |this, ops, _| { + ctx.spawn(task, move |this, ops, ctx| { if ops.is_some() { this.saved_version = this.version.clone(); + this.saved_mtime = file.mtime(); + ctx.emit(Event::Reloaded); } }) .detach(); @@ -586,7 +588,7 @@ impl Buffer { && self .file .as_ref() - .map_or(false, |f| f.mtime() != self.saved_mtime) + .map_or(false, |f| f.mtime() > self.saved_mtime) } pub fn version(&self) -> time::Global { @@ -1931,6 +1933,7 @@ pub enum Event { Dirtied, Saved, FileHandleChanged, + Reloaded, } impl Entity for Buffer { diff --git a/zed/src/editor/buffer_view.rs b/zed/src/editor/buffer_view.rs index a76f558b96784d5686a5a3b1d0c75e15285a4b24..c2aef72ff103fa48e7c07f201b366fdf31ab8137 100644 --- a/zed/src/editor/buffer_view.rs +++ b/zed/src/editor/buffer_view.rs @@ -2401,6 +2401,7 @@ impl BufferView { buffer::Event::Dirtied => ctx.emit(Event::Dirtied), buffer::Event::Saved => ctx.emit(Event::Saved), buffer::Event::FileHandleChanged => ctx.emit(Event::FileHandleChanged), + buffer::Event::Reloaded => ctx.emit(Event::FileHandleChanged), } } } @@ -2505,6 +2506,10 @@ impl workspace::ItemView for BufferView { fn is_dirty(&self, ctx: &AppContext) -> bool { self.buffer.read(ctx).is_dirty() } + + fn has_conflict(&self, ctx: &AppContext) -> bool { + self.buffer.read(ctx).has_conflict() + } } #[cfg(test)] diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index 7253e0a5af9777bf50e3cd0d1fc56ff9159b2745..b60cd63487df9d838ad4a8cf544cca29d2a586e0 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -119,6 +119,9 @@ pub trait ItemView: View { fn is_dirty(&self, _: &AppContext) -> bool { false } + fn has_conflict(&self, _: &AppContext) -> bool { + false + } fn save( &mut self, _: Option, @@ -157,6 +160,7 @@ pub trait ItemViewHandle: Send + Sync { fn id(&self) -> usize; fn to_any(&self) -> AnyViewHandle; fn is_dirty(&self, ctx: &AppContext) -> bool; + fn has_conflict(&self, ctx: &AppContext) -> bool; fn save( &self, file: Option, @@ -247,6 +251,10 @@ impl ItemViewHandle for ViewHandle { self.read(ctx).is_dirty(ctx) } + fn has_conflict(&self, ctx: &AppContext) -> bool { + self.read(ctx).has_conflict(ctx) + } + fn id(&self) -> usize { self.id() } diff --git a/zed/src/workspace/pane.rs b/zed/src/workspace/pane.rs index e7957ec9489d0db6bcb103ef3c1cb4197a75c45e..4108ac67a540b69ac204b9a3de43d258dac215b9 100644 --- a/zed/src/workspace/pane.rs +++ b/zed/src/workspace/pane.rs @@ -228,6 +228,7 @@ impl Pane { line_height - 2., mouse_state.hovered, item.is_dirty(ctx), + item.has_conflict(ctx), ctx, )) .right() @@ -296,15 +297,25 @@ impl Pane { item_id: usize, close_icon_size: f32, tab_hovered: bool, - is_modified: bool, + is_dirty: bool, + has_conflict: bool, ctx: &AppContext, ) -> ElementBox { enum TabCloseButton {} - let modified_color = ColorU::from_u32(0x556de8ff); - let mut clicked_color = modified_color; + let dirty_color = ColorU::from_u32(0x556de8ff); + let conflict_color = ColorU::from_u32(0xe45349ff); + let mut clicked_color = dirty_color; clicked_color.a = 180; + let current_color = if has_conflict { + Some(conflict_color) + } else if is_dirty { + Some(dirty_color) + } else { + None + }; + let icon = if tab_hovered { let mut icon = Svg::new("icons/x.svg"); @@ -314,13 +325,13 @@ impl Pane { .with_background_color(if mouse_state.clicked { clicked_color } else { - modified_color + dirty_color }) .with_corner_radius(close_icon_size / 2.) .boxed() } else { - if is_modified { - icon = icon.with_color(modified_color); + if let Some(current_color) = current_color { + icon = icon.with_color(current_color); } icon.boxed() } @@ -331,11 +342,11 @@ impl Pane { let diameter = 8.; ConstrainedBox::new( Canvas::new(move |bounds, ctx| { - if is_modified { + if let Some(current_color) = current_color { let square = RectF::new(bounds.origin(), vec2f(diameter, diameter)); ctx.scene.push_quad(Quad { bounds: square, - background: Some(modified_color), + background: Some(current_color), border: Default::default(), corner_radius: diameter / 2., }); From 62403343fa7eaf6ef080c169f95c770365bdadac Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 12 May 2021 11:54:48 +0200 Subject: [PATCH 4/9] Display prompt when trying to save a conflicting file --- gpui/src/app.rs | 35 +++++++++++++++++++++++++- gpui/src/lib.rs | 2 +- gpui/src/platform/mac/window.rs | 44 ++++++++++++++++++++++++++++++++- gpui/src/platform/mod.rs | 13 ++++++++++ gpui/src/platform/test.rs | 2 ++ zed/src/workspace.rs | 42 +++++++++++++++++++++++-------- 6 files changed, 125 insertions(+), 13 deletions(-) diff --git a/gpui/src/app.rs b/gpui/src/app.rs index 178b8f9b69f4b1e43065bfb145d4968b0d482d9a..db3455598bcb36e41acbaaacf66d3a93186e5111 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -2,7 +2,7 @@ use crate::{ elements::ElementBox, executor, keymap::{self, Keystroke}, - platform::{self, WindowOptions}, + platform::{self, PromptLevel, WindowOptions}, presenter::Presenter, util::{post_inc, timeout}, AssetCache, AssetSource, ClipboardItem, FontCache, PathPromptOptions, TextLayoutCache, @@ -578,6 +578,31 @@ impl MutableAppContext { self.platform.set_menus(menus); } + pub fn prompt( + &self, + window_id: usize, + level: PromptLevel, + msg: &str, + answers: &[&str], + done_fn: F, + ) where + F: 'static + FnOnce(usize, &mut MutableAppContext), + { + let app = self.weak_self.as_ref().unwrap().upgrade().unwrap(); + let foreground = self.foreground.clone(); + let (_, window) = &self.presenters_and_platform_windows[&window_id]; + window.prompt( + level, + msg, + answers, + Box::new(move |answer| { + foreground + .spawn(async move { (done_fn)(answer, &mut *app.borrow_mut()) }) + .detach(); + }), + ); + } + pub fn prompt_for_paths(&self, options: PathPromptOptions, done_fn: F) where F: 'static + FnOnce(Option>, &mut MutableAppContext), @@ -1766,6 +1791,14 @@ impl<'a, T: View> ViewContext<'a, T> { &self.app.ctx.background } + pub fn prompt(&self, level: PromptLevel, msg: &str, answers: &[&str], done_fn: F) + where + F: 'static + FnOnce(usize, &mut MutableAppContext), + { + self.app + .prompt(self.window_id, level, msg, answers, done_fn) + } + pub fn prompt_for_paths(&self, options: PathPromptOptions, done_fn: F) where F: 'static + FnOnce(Option>, &mut MutableAppContext), diff --git a/gpui/src/lib.rs b/gpui/src/lib.rs index edf14bc65c8ae4bc1332ee7016685d5bd3e389d7..72e3c5a2f5f698a78ebc298c3467d4bca07d03d9 100644 --- a/gpui/src/lib.rs +++ b/gpui/src/lib.rs @@ -24,7 +24,7 @@ pub mod color; pub mod json; pub mod keymap; mod platform; -pub use platform::{Event, PathPromptOptions}; +pub use platform::{Event, PathPromptOptions, PromptLevel}; pub use presenter::{ AfterLayoutContext, Axis, DebugContext, EventContext, LayoutContext, PaintContext, SizeConstraint, Vector2FExt, diff --git a/gpui/src/platform/mac/window.rs b/gpui/src/platform/mac/window.rs index 0c81ca623c324051d2c12568f49f1ddd53060e51..2c6df244e1d69633a34f672661794005b9514a95 100644 --- a/gpui/src/platform/mac/window.rs +++ b/gpui/src/platform/mac/window.rs @@ -5,6 +5,7 @@ use crate::{ platform::{self, Event, WindowContext}, Scene, }; +use block::ConcreteBlock; use cocoa::{ appkit::{ NSApplication, NSBackingStoreBuffered, NSScreen, NSView, NSViewHeightSizable, @@ -26,7 +27,8 @@ use objc::{ use pathfinder_geometry::vector::vec2f; use smol::Timer; use std::{ - cell::RefCell, + cell::{Cell, RefCell}, + convert::TryInto, ffi::c_void, mem, ptr, rc::{Rc, Weak}, @@ -272,6 +274,42 @@ impl platform::Window for Window { fn on_close(&mut self, callback: Box) { self.0.as_ref().borrow_mut().close_callback = Some(callback); } + + fn prompt( + &self, + level: platform::PromptLevel, + msg: &str, + answers: &[&str], + done_fn: Box, + ) { + unsafe { + let alert: id = msg_send![class!(NSAlert), alloc]; + let alert: id = msg_send![alert, init]; + let alert_style = match level { + platform::PromptLevel::Info => 1, + platform::PromptLevel::Warning => 0, + platform::PromptLevel::Critical => 2, + }; + let _: () = msg_send![alert, setAlertStyle: alert_style]; + let _: () = msg_send![alert, setMessageText: ns_string(msg)]; + for (ix, answer) in answers.into_iter().enumerate() { + let button: id = msg_send![alert, addButtonWithTitle: ns_string(answer)]; + let _: () = msg_send![button, setTag: ix as NSInteger]; + } + let done_fn = Cell::new(Some(done_fn)); + let block = ConcreteBlock::new(move |answer: NSInteger| { + if let Some(done_fn) = done_fn.take() { + (done_fn)(answer.try_into().unwrap()); + } + }); + let block = block.copy(); + let _: () = msg_send![ + alert, + beginSheetModalForWindow: self.0.borrow().native_window + completionHandler: block + ]; + } + } } impl platform::WindowContext for Window { @@ -515,3 +553,7 @@ async fn synthetic_drag( } } } + +unsafe fn ns_string(string: &str) -> id { + NSString::alloc(nil).init_str(string).autorelease() +} diff --git a/gpui/src/platform/mod.rs b/gpui/src/platform/mod.rs index 2ad5ce42026daedaba801546bfe4f83bb8b1a803..f75351744957483f1990758a6592cfed4e1a9f9d 100644 --- a/gpui/src/platform/mod.rs +++ b/gpui/src/platform/mod.rs @@ -71,6 +71,13 @@ pub trait Window: WindowContext { fn on_event(&mut self, callback: Box); fn on_resize(&mut self, callback: Box); fn on_close(&mut self, callback: Box); + fn prompt( + &self, + level: PromptLevel, + msg: &str, + answers: &[&str], + done_fn: Box, + ); } pub trait WindowContext { @@ -90,6 +97,12 @@ pub struct PathPromptOptions { pub multiple: bool, } +pub enum PromptLevel { + Info, + Warning, + Critical, +} + pub trait FontSystem: Send + Sync { fn load_family(&self, name: &str) -> anyhow::Result>; fn select_font( diff --git a/gpui/src/platform/test.rs b/gpui/src/platform/test.rs index 255484e2a6fe51899a7fd21851a30b3a0270559f..98c36b110f081d0cbb7f769cf8af16aa33a70da5 100644 --- a/gpui/src/platform/test.rs +++ b/gpui/src/platform/test.rs @@ -163,6 +163,8 @@ impl super::Window for Window { fn on_close(&mut self, callback: Box) { self.close_handlers.push(callback); } + + fn prompt(&self, _: crate::PromptLevel, _: &str, _: &[&str], _: Box) {} } pub(crate) fn platform() -> Platform { diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index b60cd63487df9d838ad4a8cf544cca29d2a586e0..cd299daf9cf9ad001262c32127b98ee7488b038b 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -9,8 +9,8 @@ use crate::{ use futures_core::{future::LocalBoxFuture, Future}; use gpui::{ color::rgbu, elements::*, json::to_string_pretty, keymap::Binding, AnyViewHandle, AppContext, - ClipboardItem, Entity, EntityTask, ModelHandle, MutableAppContext, PathPromptOptions, View, - ViewContext, ViewHandle, WeakModelHandle, + ClipboardItem, Entity, EntityTask, ModelHandle, MutableAppContext, PathPromptOptions, + PromptLevel, View, ViewContext, ViewHandle, WeakModelHandle, }; use log::error; pub use pane::*; @@ -573,15 +573,37 @@ impl Workspace { } }); return; - } + } else if item.has_conflict(ctx.as_ref()) { + const CONFLICT_MESSAGE: &'static str = "This file has changed on disk since you started editing it. Do you want to overwrite it?"; - let task = item.save(None, ctx.as_mut()); - ctx.spawn(task, |_, result, _| { - if let Err(e) = result { - error!("failed to save item: {:?}, ", e); - } - }) - .detach() + let handle = ctx.handle(); + ctx.prompt( + PromptLevel::Warning, + CONFLICT_MESSAGE, + &["Overwrite", "Cancel"], + move |answer, ctx| { + if answer == 0 { + handle.update(ctx, move |_, ctx| { + let task = item.save(None, ctx.as_mut()); + ctx.spawn(task, |_, result, _| { + if let Err(e) = result { + error!("failed to save item: {:?}, ", e); + } + }) + .detach(); + }); + } + }, + ); + } else { + let task = item.save(None, ctx.as_mut()); + ctx.spawn(task, |_, result, _| { + if let Err(e) = result { + error!("failed to save item: {:?}, ", e); + } + }) + .detach(); + } } } From 2eff93615449458931ed187f8bb74161de943993 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 12 May 2021 11:57:05 +0200 Subject: [PATCH 5/9] Make `MutableAppContext::prompt` private ...as we're supposed to call this method only via ViewContext. --- gpui/src/app.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gpui/src/app.rs b/gpui/src/app.rs index db3455598bcb36e41acbaaacf66d3a93186e5111..a8d3e13fdedd3aef18a0f165d65cb647e2770ecd 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -578,7 +578,7 @@ impl MutableAppContext { self.platform.set_menus(menus); } - pub fn prompt( + fn prompt( &self, window_id: usize, level: PromptLevel, From ac2168f1bd7e5aa07a928265b31a22d96930e037 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 12 May 2021 14:33:51 +0200 Subject: [PATCH 6/9] Update `mtime` and `is_deleted` when saving/loading a file --- zed/src/worktree.rs | 38 +++++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index a653501b517cd736dc5cc139a3840b3bcd06da52..62497ba168bf144581130525ed316ae734a2b076 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -180,11 +180,14 @@ impl Worktree { path: &Path, ctx: &AppContext, ) -> impl Future> { - let abs_path = self.absolutize(path); + let handles = self.handles.clone(); + let path = path.to_path_buf(); + let abs_path = self.absolutize(&path); ctx.background_executor().spawn(async move { - let mut file = std::fs::File::open(&abs_path)?; + let mut file = fs::File::open(&abs_path)?; let mut base_text = String::new(); file.read_to_string(&mut base_text)?; + Self::update_file_handle(&file, &path, &handles)?; Ok(History::new(Arc::from(base_text))) }) } @@ -200,20 +203,29 @@ impl Worktree { let abs_path = self.absolutize(&path); ctx.background_executor().spawn(async move { let buffer_size = content.text_summary().bytes.min(10 * 1024); - let file = std::fs::File::create(&abs_path)?; - let mut writer = std::io::BufWriter::with_capacity(buffer_size, file); + let file = fs::File::create(&abs_path)?; + let mut writer = io::BufWriter::with_capacity(buffer_size, &file); for chunk in content.fragments() { writer.write(chunk.as_bytes())?; } writer.flush()?; - - if let Some(handle) = handles.lock().get(path.as_path()).and_then(Weak::upgrade) { - handle.lock().is_deleted = false; - } - + Self::update_file_handle(&file, &path, &handles)?; Ok(()) }) } + + fn update_file_handle( + file: &fs::File, + path: &Path, + handles: &Mutex, Weak>>>, + ) -> Result<()> { + if let Some(handle) = handles.lock().get(path).and_then(Weak::upgrade) { + let mut handle = handle.lock(); + handle.mtime = file.metadata()?.modified()?; + handle.is_deleted = false; + } + Ok(()) + } } impl Entity for Worktree { @@ -1554,10 +1566,10 @@ mod tests { assert!(non_existent_file.is_deleted()); tree.flush_fs_events(&app).await; - std::fs::rename(dir.path().join("a/file3"), dir.path().join("b/c/file3")).unwrap(); - std::fs::remove_file(dir.path().join("b/c/file5")).unwrap(); - std::fs::rename(dir.path().join("b/c"), dir.path().join("d")).unwrap(); - std::fs::rename(dir.path().join("a/file2"), dir.path().join("a/file2.new")).unwrap(); + fs::rename(dir.path().join("a/file3"), dir.path().join("b/c/file3")).unwrap(); + fs::remove_file(dir.path().join("b/c/file5")).unwrap(); + fs::rename(dir.path().join("b/c"), dir.path().join("d")).unwrap(); + fs::rename(dir.path().join("a/file2"), dir.path().join("a/file2.new")).unwrap(); tree.update(&mut app, |tree, ctx| tree.next_scan_complete(ctx)) .await; From 048bbc9da01e8b6f61bfc6810260d6128e6bd5ad Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 12 May 2021 15:19:46 +0200 Subject: [PATCH 7/9] Test prompting when saving while there's a conflict --- gpui/src/app.rs | 17 ++++++++++ gpui/src/platform/mac/window.rs | 5 +++ gpui/src/platform/mod.rs | 1 + gpui/src/platform/test.rs | 10 +++++- zed/src/workspace.rs | 55 ++++++++++++++++++++++++++++++++- 5 files changed, 86 insertions(+), 2 deletions(-) diff --git a/gpui/src/app.rs b/gpui/src/app.rs index a8d3e13fdedd3aef18a0f165d65cb647e2770ecd..decc8096be084ceb6ff3017af8eba4c5a881fbff 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -343,6 +343,23 @@ impl TestAppContext { pub fn did_prompt_for_new_path(&self) -> bool { self.1.as_ref().did_prompt_for_new_path() } + + pub fn simulate_prompt_answer(&self, window_id: usize, answer: usize) { + let mut state = self.0.borrow_mut(); + let (_, window) = state + .presenters_and_platform_windows + .get_mut(&window_id) + .unwrap(); + let test_window = window + .as_any_mut() + .downcast_mut::() + .unwrap(); + let callback = test_window + .last_prompt + .take() + .expect("prompt was not called"); + (callback)(answer); + } } impl UpdateModel for TestAppContext { diff --git a/gpui/src/platform/mac/window.rs b/gpui/src/platform/mac/window.rs index 2c6df244e1d69633a34f672661794005b9514a95..b1d86acb12e7b358ce747a22f0d32e5c4ff0e270 100644 --- a/gpui/src/platform/mac/window.rs +++ b/gpui/src/platform/mac/window.rs @@ -27,6 +27,7 @@ use objc::{ use pathfinder_geometry::vector::vec2f; use smol::Timer; use std::{ + any::Any, cell::{Cell, RefCell}, convert::TryInto, ffi::c_void, @@ -263,6 +264,10 @@ impl Drop for Window { } impl platform::Window for Window { + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + fn on_event(&mut self, callback: Box) { self.0.as_ref().borrow_mut().event_callback = Some(callback); } diff --git a/gpui/src/platform/mod.rs b/gpui/src/platform/mod.rs index f75351744957483f1990758a6592cfed4e1a9f9d..fd62770c12c82b98bdc9ddad17bf8da44184f96d 100644 --- a/gpui/src/platform/mod.rs +++ b/gpui/src/platform/mod.rs @@ -68,6 +68,7 @@ pub trait Dispatcher: Send + Sync { } pub trait Window: WindowContext { + fn as_any_mut(&mut self) -> &mut dyn Any; fn on_event(&mut self, callback: Box); fn on_resize(&mut self, callback: Box); fn on_close(&mut self, callback: Box); diff --git a/gpui/src/platform/test.rs b/gpui/src/platform/test.rs index 98c36b110f081d0cbb7f769cf8af16aa33a70da5..d3a0c0d3f107b8205c775e65c665fa842611a658 100644 --- a/gpui/src/platform/test.rs +++ b/gpui/src/platform/test.rs @@ -24,6 +24,7 @@ pub struct Window { event_handlers: Vec>, resize_handlers: Vec>, close_handlers: Vec>, + pub(crate) last_prompt: RefCell>>, } impl Platform { @@ -123,6 +124,7 @@ impl Window { close_handlers: Vec::new(), scale_factor: 1.0, current_scene: None, + last_prompt: RefCell::new(None), } } } @@ -152,6 +154,10 @@ impl super::WindowContext for Window { } impl super::Window for Window { + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + fn on_event(&mut self, callback: Box) { self.event_handlers.push(callback); } @@ -164,7 +170,9 @@ impl super::Window for Window { self.close_handlers.push(callback); } - fn prompt(&self, _: crate::PromptLevel, _: &str, _: &[&str], _: Box) {} + fn prompt(&self, _: crate::PromptLevel, _: &str, _: &[&str], f: Box) { + self.last_prompt.replace(Some(f)); + } } pub(crate) fn platform() -> Platform { diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index cd299daf9cf9ad001262c32127b98ee7488b038b..b737b0e99d342981c61bc6b7c1ef8fecb74c6132 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -761,7 +761,7 @@ mod tests { use crate::{editor::BufferView, settings, test::temp_tree}; use gpui::App; use serde_json::json; - use std::collections::HashSet; + use std::{collections::HashSet, fs}; use tempdir::TempDir; #[test] @@ -1101,6 +1101,59 @@ mod tests { }); } + #[test] + fn test_save_conflicting_item() { + App::test_async((), |mut app| async move { + let dir = temp_tree(json!({ + "a.txt": "", + })); + + let settings = settings::channel(&app.font_cache()).unwrap().1; + let (window_id, workspace) = app.add_window(|ctx| { + let mut workspace = Workspace::new(0, settings, ctx); + workspace.add_worktree(dir.path(), ctx); + workspace + }); + let tree = app.read(|ctx| { + let mut trees = workspace.read(ctx).worktrees().iter(); + trees.next().unwrap().clone() + }); + tree.flush_fs_events(&app).await; + + // Open a file within an existing worktree. + app.update(|ctx| { + workspace.update(ctx, |view, ctx| { + view.open_paths(&[dir.path().join("a.txt")], ctx) + }) + }) + .await; + let editor = app.read(|ctx| { + let pane = workspace.read(ctx).active_pane().read(ctx); + let item = pane.active_item().unwrap(); + item.to_any().downcast::().unwrap() + }); + + app.update(|ctx| { + editor.update(ctx, |editor, ctx| editor.insert(&"x".to_string(), ctx)) + }); + fs::write(dir.path().join("a.txt"), "changed").unwrap(); + tree.flush_fs_events(&app).await; + app.read(|ctx| { + assert!(editor.is_dirty(ctx)); + assert!(editor.has_conflict(ctx)); + }); + + app.update(|ctx| workspace.update(ctx, |w, ctx| w.save_active_item(&(), ctx))); + app.simulate_prompt_answer(window_id, 0); + tree.update(&mut app, |tree, ctx| tree.next_scan_complete(ctx)) + .await; + app.read(|ctx| { + assert!(!editor.is_dirty(ctx)); + assert!(!editor.has_conflict(ctx)); + }); + }); + } + #[test] fn test_pane_actions() { App::test_async((), |mut app| async move { From c757b3f46ebe893cdef9ba94d96e2b1833ff4e8a Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 12 May 2021 16:20:22 -0700 Subject: [PATCH 8/9] Allow a longer timeout in buffer conflict test --- zed/src/editor/buffer/mod.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/zed/src/editor/buffer/mod.rs b/zed/src/editor/buffer/mod.rs index 190564a4b448809c7ae55c87b5373a3576a34882..bc672a1ffcccaa5a20c7287f817b8e2f3e5dc7b8 100644 --- a/zed/src/editor/buffer/mod.rs +++ b/zed/src/editor/buffer/mod.rs @@ -3263,7 +3263,9 @@ mod tests { // Becaues the buffer is modified, it doesn't reload from disk, but is // marked as having a conflict. buffer - .condition(&app, |buffer, _| buffer.has_conflict()) + .condition_with_duration(Duration::from_millis(500), &app, |buffer, _| { + buffer.has_conflict() + }) .await; } From 520cbfb9556c1a7a3cd7c99cf425893df91e2db0 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 12 May 2021 20:13:27 -0700 Subject: [PATCH 9/9] Read file's mtime in background when getting a FileHandle Co-Authored-By: Antonio Scandurra --- zed/src/editor/buffer/mod.rs | 14 ++++-- zed/src/workspace.rs | 23 +++++---- zed/src/worktree.rs | 93 ++++++++++++++++++++---------------- 3 files changed, 73 insertions(+), 57 deletions(-) diff --git a/zed/src/editor/buffer/mod.rs b/zed/src/editor/buffer/mod.rs index bc672a1ffcccaa5a20c7287f817b8e2f3e5dc7b8..b1f04f6786bc2dc746f54e8e2335690db3c995ef 100644 --- a/zed/src/editor/buffer/mod.rs +++ b/zed/src/editor/buffer/mod.rs @@ -376,7 +376,9 @@ impl Buffer { file: Option, ctx: &mut ModelContext, ) -> Self { + let saved_mtime; if let Some(file) = file.as_ref() { + saved_mtime = file.mtime(); file.observe_from_model(ctx, |this, file, ctx| { let version = this.version.clone(); if this.version == this.saved_version { @@ -408,6 +410,8 @@ impl Buffer { } ctx.emit(Event::FileHandleChanged); }); + } else { + saved_mtime = UNIX_EPOCH; } let mut insertion_splits = HashMap::default(); @@ -472,11 +476,11 @@ impl Buffer { insertion_splits, version: time::Global::new(), saved_version: time::Global::new(), - saved_mtime: UNIX_EPOCH, last_edit: time::Local::default(), undo_map: Default::default(), history, file, + saved_mtime, selections: HashMap::default(), selections_last_update: 0, deferred_ops: OperationQueue::new(), @@ -3073,7 +3077,7 @@ mod tests { tree.flush_fs_events(&app).await; app.read(|ctx| tree.read(ctx).scan_complete()).await; - let file1 = app.read(|ctx| tree.file("file1", ctx)); + let file1 = app.update(|ctx| tree.file("file1", ctx)).await; let buffer1 = app.add_model(|ctx| { Buffer::from_history(0, History::new("abc".into()), Some(file1), ctx) }); @@ -3133,7 +3137,7 @@ mod tests { // When a file is deleted, the buffer is considered dirty. let events = Rc::new(RefCell::new(Vec::new())); - let file2 = app.read(|ctx| tree.file("file2", ctx)); + let file2 = app.update(|ctx| tree.file("file2", ctx)).await; let buffer2 = app.add_model(|ctx: &mut ModelContext| { ctx.subscribe(&ctx.handle(), { let events = events.clone(); @@ -3154,7 +3158,7 @@ mod tests { // When a file is already dirty when deleted, we don't emit a Dirtied event. let events = Rc::new(RefCell::new(Vec::new())); - let file3 = app.read(|ctx| tree.file("file3", ctx)); + let file3 = app.update(|ctx| tree.file("file3", ctx)).await; let buffer3 = app.add_model(|ctx: &mut ModelContext| { ctx.subscribe(&ctx.handle(), { let events = events.clone(); @@ -3185,7 +3189,7 @@ mod tests { app.read(|ctx| tree.read(ctx).scan_complete()).await; let abs_path = dir.path().join("the-file"); - let file = app.read(|ctx| tree.file("the-file", ctx)); + let file = app.update(|ctx| tree.file("the-file", ctx)).await; let buffer = app.add_model(|ctx| { Buffer::from_history(0, History::new(initial_contents.into()), Some(file), ctx) }); diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index e82d7351e48187613a163d442375186c87a1e314..5c9dc16c92707d8875ad59cc4eb8f981b0f5162e 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -369,6 +369,7 @@ impl Workspace { .map(|(abs_path, file)| { let is_file = bg.spawn(async move { abs_path.is_file() }); ctx.spawn(|this, mut ctx| async move { + let file = file.await; let is_file = is_file.await; this.update(&mut ctx, |this, ctx| { if is_file { @@ -389,14 +390,14 @@ impl Workspace { } } - fn file_for_path(&mut self, abs_path: &Path, ctx: &mut ViewContext) -> FileHandle { + fn file_for_path(&mut self, abs_path: &Path, ctx: &mut ViewContext) -> Task { for tree in self.worktrees.iter() { if let Ok(relative_path) = abs_path.strip_prefix(tree.read(ctx).abs_path()) { - return tree.file(relative_path, ctx.as_ref()); + return tree.file(relative_path, ctx.as_mut()); } } let worktree = self.add_worktree(&abs_path, ctx); - worktree.file(Path::new(""), ctx.as_ref()) + worktree.file(Path::new(""), ctx.as_mut()) } pub fn add_worktree( @@ -497,18 +498,19 @@ impl Workspace { } }; - let file = worktree.file(path.clone(), ctx.as_ref()); + let file = worktree.file(path.clone(), ctx.as_mut()); if let Entry::Vacant(entry) = self.loading_items.entry(entry.clone()) { let (mut tx, rx) = postage::watch::channel(); entry.insert(rx); let replica_id = self.replica_id; - let history = ctx - .background_executor() - .spawn(file.load_history(ctx.as_ref())); ctx.as_mut() .spawn(|mut ctx| async move { - *tx.borrow_mut() = Some(match history.await { + let file = file.await; + let history = ctx.read(|ctx| file.load_history(ctx)); + let history = ctx.background_executor().spawn(history).await; + + *tx.borrow_mut() = Some(match history { Ok(history) => Ok(Box::new(ctx.add_model(|ctx| { Buffer::from_history(replica_id, history, Some(file), ctx) }))), @@ -564,8 +566,9 @@ impl Workspace { ctx.prompt_for_new_path(&start_path, move |path, ctx| { if let Some(path) = path { ctx.spawn(|mut ctx| async move { - let file = - handle.update(&mut ctx, |me, ctx| me.file_for_path(&path, ctx)); + let file = handle + .update(&mut ctx, |me, ctx| me.file_for_path(&path, ctx)) + .await; if let Err(error) = ctx.update(|ctx| item.save(Some(file), ctx)).await { error!("failed to save item: {:?}, ", error); } diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 62bb4ae0080e439dce51fdabd0cd48c5dd7fb7a0..59171e115df7dd01587d4ca6b969f1313aeae889 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -9,7 +9,7 @@ use crate::{ use ::ignore::gitignore::Gitignore; use anyhow::{Context, Result}; pub use fuzzy::{match_paths, PathMatch}; -use gpui::{scoped_pool, AppContext, Entity, ModelContext, ModelHandle, Task}; +use gpui::{scoped_pool, AppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task}; use lazy_static::lazy_static; use parking_lot::Mutex; use postage::{ @@ -202,14 +202,12 @@ impl Worktree { path: &Path, ctx: &AppContext, ) -> impl Future> { - let handles = self.handles.clone(); let path = path.to_path_buf(); let abs_path = self.absolutize(&path); ctx.background_executor().spawn(async move { let mut file = fs::File::open(&abs_path)?; let mut base_text = String::new(); file.read_to_string(&mut base_text)?; - Self::update_file_handle(&file, &path, &handles)?; Ok(History::new(Arc::from(base_text))) }) } @@ -1228,7 +1226,7 @@ struct UpdateIgnoreStatusJob { } pub trait WorktreeHandle { - fn file(&self, path: impl AsRef, app: &AppContext) -> FileHandle; + fn file(&self, path: impl AsRef, app: &mut MutableAppContext) -> Task; #[cfg(test)] fn flush_fs_events<'a>( @@ -1238,36 +1236,51 @@ pub trait WorktreeHandle { } impl WorktreeHandle for ModelHandle { - fn file(&self, path: impl AsRef, app: &AppContext) -> FileHandle { - let path = path.as_ref(); + fn file(&self, path: impl AsRef, app: &mut MutableAppContext) -> Task { + let path = Arc::from(path.as_ref()); + let handle = self.clone(); let tree = self.read(app); - let mut handles = tree.handles.lock(); - let state = if let Some(state) = handles.get(path).and_then(Weak::upgrade) { - state - } else { - let handle_state = if let Some(entry) = tree.entry_for_path(path) { - FileHandleState { - path: entry.path().clone(), - is_deleted: false, - mtime: UNIX_EPOCH, - } - } else { - FileHandleState { - path: path.into(), - is_deleted: !tree.path_is_pending(path), - mtime: UNIX_EPOCH, - } - }; - - let state = Arc::new(Mutex::new(handle_state.clone())); - handles.insert(handle_state.path, Arc::downgrade(&state)); - state - }; + let abs_path = tree.absolutize(&path); + app.spawn(|ctx| async move { + let mtime = ctx + .background_executor() + .spawn(async move { + if let Ok(metadata) = fs::metadata(&abs_path) { + metadata.modified().unwrap() + } else { + UNIX_EPOCH + } + }) + .await; + let state = handle.read_with(&ctx, |tree, _| { + let mut handles = tree.handles.lock(); + if let Some(state) = handles.get(&path).and_then(Weak::upgrade) { + state + } else { + let handle_state = if let Some(entry) = tree.entry_for_path(&path) { + FileHandleState { + path: entry.path().clone(), + is_deleted: false, + mtime, + } + } else { + FileHandleState { + path: path.clone(), + is_deleted: !tree.path_is_pending(path), + mtime, + } + }; - FileHandle { - worktree: self.clone(), - state, - } + let state = Arc::new(Mutex::new(handle_state.clone())); + handles.insert(handle_state.path, Arc::downgrade(&state)); + state + } + }); + FileHandle { + worktree: handle.clone(), + state, + } + }) } // When the worktree's FS event stream sometimes delivers "redundant" events for FS changes that @@ -1525,7 +1538,7 @@ mod tests { let buffer = app.add_model(|ctx| Buffer::new(1, "a line of text.\n".repeat(10 * 1024), ctx)); - let file = app.read(|ctx| tree.file("", ctx)); + let file = app.update(|ctx| tree.file("", ctx)).await; app.update(|ctx| { assert_eq!(file.path().file_name(), None); smol::block_on(file.save(buffer.read(ctx).snapshot(), ctx.as_ref())).unwrap(); @@ -1552,15 +1565,11 @@ mod tests { })); let tree = app.add_model(|ctx| Worktree::new(dir.path(), ctx)); - let (file2, file3, file4, file5, non_existent_file) = app.read(|ctx| { - ( - tree.file("a/file2", ctx), - tree.file("a/file3", ctx), - tree.file("b/c/file4", ctx), - tree.file("b/c/file5", ctx), - tree.file("a/filex", ctx), - ) - }); + let file2 = app.update(|ctx| tree.file("a/file2", ctx)).await; + let file3 = app.update(|ctx| tree.file("a/file3", ctx)).await; + let file4 = app.update(|ctx| tree.file("b/c/file4", ctx)).await; + let file5 = app.update(|ctx| tree.file("b/c/file5", ctx)).await; + let non_existent_file = app.update(|ctx| tree.file("a/file_x", ctx)).await; // The worktree hasn't scanned the directories containing these paths, // so it can't determine that the paths are deleted.