From 040189db3c543f937a4f0dceaabb9b81311d4476 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 11 May 2021 14:48:57 -0700 Subject: [PATCH] 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 1fba36fd12..06ce5843d4 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 d146387752..2302fc6509 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 2d578c2c0b..82881afca3 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 be895533f1..a653501b51 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, } };