mirror of
https://github.com/zed-industries/zed.git
synced 2025-01-12 21:32:40 +00:00
Merge pull request #54 from zed-industries/file-changed-on-disk
Handle buffers' files changing on disk from outside of Zed
This commit is contained in:
commit
0187b6da2c
12 changed files with 595 additions and 114 deletions
7
Cargo.lock
generated
7
Cargo.lock
generated
|
@ -2451,6 +2451,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"
|
||||
|
@ -2976,6 +2982,7 @@ dependencies = [
|
|||
"seahash",
|
||||
"serde 1.0.125",
|
||||
"serde_json 1.0.64",
|
||||
"similar",
|
||||
"simplelog",
|
||||
"smallvec",
|
||||
"smol",
|
||||
|
|
|
@ -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,
|
||||
|
@ -361,6 +361,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::<platform::test::Window>()
|
||||
.unwrap();
|
||||
let callback = test_window
|
||||
.last_prompt
|
||||
.take()
|
||||
.expect("prompt was not called");
|
||||
(callback)(answer);
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncAppContext {
|
||||
|
@ -383,6 +400,10 @@ impl AsyncAppContext {
|
|||
{
|
||||
self.update(|ctx| ctx.add_model(build_model))
|
||||
}
|
||||
|
||||
pub fn background_executor(&self) -> Arc<executor::Background> {
|
||||
self.0.borrow().ctx.background.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl UpdateModel for AsyncAppContext {
|
||||
|
@ -688,6 +709,31 @@ impl MutableAppContext {
|
|||
self.platform.set_menus(menus);
|
||||
}
|
||||
|
||||
fn prompt<F>(
|
||||
&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<F>(&self, options: PathPromptOptions, done_fn: F)
|
||||
where
|
||||
F: 'static + FnOnce(Option<Vec<PathBuf>>, &mut MutableAppContext),
|
||||
|
@ -1731,6 +1777,14 @@ impl<'a, T: View> ViewContext<'a, T> {
|
|||
&self.app.ctx.background
|
||||
}
|
||||
|
||||
pub fn prompt<F>(&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<F>(&self, options: PathPromptOptions, done_fn: F)
|
||||
where
|
||||
F: 'static + FnOnce(Option<Vec<PathBuf>>, &mut MutableAppContext),
|
||||
|
|
|
@ -25,7 +25,7 @@ pub mod json;
|
|||
pub mod keymap;
|
||||
mod platform;
|
||||
pub use gpui_macros::test;
|
||||
pub use platform::{Event, PathPromptOptions};
|
||||
pub use platform::{Event, PathPromptOptions, PromptLevel};
|
||||
pub use presenter::{
|
||||
AfterLayoutContext, Axis, DebugContext, EventContext, LayoutContext, PaintContext,
|
||||
SizeConstraint, Vector2FExt,
|
||||
|
|
|
@ -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,9 @@ use objc::{
|
|||
use pathfinder_geometry::vector::vec2f;
|
||||
use smol::Timer;
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
any::Any,
|
||||
cell::{Cell, RefCell},
|
||||
convert::TryInto,
|
||||
ffi::c_void,
|
||||
mem, ptr,
|
||||
rc::{Rc, Weak},
|
||||
|
@ -261,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<dyn FnMut(Event)>) {
|
||||
self.0.as_ref().borrow_mut().event_callback = Some(callback);
|
||||
}
|
||||
|
@ -272,6 +279,42 @@ impl platform::Window for Window {
|
|||
fn on_close(&mut self, callback: Box<dyn FnOnce()>) {
|
||||
self.0.as_ref().borrow_mut().close_callback = Some(callback);
|
||||
}
|
||||
|
||||
fn prompt(
|
||||
&self,
|
||||
level: platform::PromptLevel,
|
||||
msg: &str,
|
||||
answers: &[&str],
|
||||
done_fn: Box<dyn FnOnce(usize)>,
|
||||
) {
|
||||
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 +558,7 @@ async fn synthetic_drag(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn ns_string(string: &str) -> id {
|
||||
NSString::alloc(nil).init_str(string).autorelease()
|
||||
}
|
||||
|
|
|
@ -68,9 +68,17 @@ 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<dyn FnMut(Event)>);
|
||||
fn on_resize(&mut self, callback: Box<dyn FnMut(&mut dyn WindowContext)>);
|
||||
fn on_close(&mut self, callback: Box<dyn FnOnce()>);
|
||||
fn prompt(
|
||||
&self,
|
||||
level: PromptLevel,
|
||||
msg: &str,
|
||||
answers: &[&str],
|
||||
done_fn: Box<dyn FnOnce(usize)>,
|
||||
);
|
||||
}
|
||||
|
||||
pub trait WindowContext {
|
||||
|
@ -90,6 +98,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<Vec<FontId>>;
|
||||
fn select_font(
|
||||
|
|
|
@ -24,6 +24,7 @@ pub struct Window {
|
|||
event_handlers: Vec<Box<dyn FnMut(super::Event)>>,
|
||||
resize_handlers: Vec<Box<dyn FnMut(&mut dyn super::WindowContext)>>,
|
||||
close_handlers: Vec<Box<dyn FnOnce()>>,
|
||||
pub(crate) last_prompt: RefCell<Option<Box<dyn FnOnce(usize)>>>,
|
||||
}
|
||||
|
||||
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<dyn FnMut(crate::Event)>) {
|
||||
self.event_handlers.push(callback);
|
||||
}
|
||||
|
@ -163,6 +169,10 @@ impl super::Window for Window {
|
|||
fn on_close(&mut self, callback: Box<dyn FnOnce()>) {
|
||||
self.close_handlers.push(callback);
|
||||
}
|
||||
|
||||
fn prompt(&self, _: crate::PromptLevel, _: &str, _: &[&str], f: Box<dyn FnOnce(usize)>) {
|
||||
self.last_prompt.replace(Some(f));
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn platform() -> Platform {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -7,6 +7,7 @@ pub use anchor::*;
|
|||
pub use point::*;
|
||||
use seahash::SeaHasher;
|
||||
pub use selection::*;
|
||||
use similar::{ChangeTag, TextDiff};
|
||||
pub use text::*;
|
||||
|
||||
use crate::{
|
||||
|
@ -27,7 +28,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);
|
||||
|
@ -60,6 +61,7 @@ pub struct Buffer {
|
|||
insertion_splits: HashMap<time::Local, SumTree<InsertionSplit>>,
|
||||
pub version: time::Global,
|
||||
saved_version: time::Global,
|
||||
saved_mtime: SystemTime,
|
||||
last_edit: time::Local,
|
||||
undo_map: UndoMap,
|
||||
history: History,
|
||||
|
@ -374,13 +376,42 @@ impl Buffer {
|
|||
file: Option<FileHandle>,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
let saved_mtime;
|
||||
if let Some(file) = file.as_ref() {
|
||||
saved_mtime = file.mtime();
|
||||
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(|handle, mut ctx| async move {
|
||||
let (current_version, history) = handle.read_with(&ctx, |this, ctx| {
|
||||
(this.version.clone(), file.load_history(ctx.as_ref()))
|
||||
});
|
||||
if let (Ok(history), true) = (history.await, current_version == version)
|
||||
{
|
||||
let operations = handle
|
||||
.update(&mut ctx, |this, ctx| {
|
||||
this.set_text_via_diff(history.base_text, ctx)
|
||||
})
|
||||
.await;
|
||||
if operations.is_some() {
|
||||
handle.update(&mut ctx, |this, ctx| {
|
||||
this.saved_version = this.version.clone();
|
||||
this.saved_mtime = file.mtime();
|
||||
ctx.emit(Event::Reloaded);
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
ctx.emit(Event::FileHandleChanged);
|
||||
});
|
||||
} else {
|
||||
saved_mtime = UNIX_EPOCH;
|
||||
}
|
||||
|
||||
let mut insertion_splits = HashMap::default();
|
||||
|
@ -449,6 +480,7 @@ impl Buffer {
|
|||
undo_map: Default::default(),
|
||||
history,
|
||||
file,
|
||||
saved_mtime,
|
||||
selections: HashMap::default(),
|
||||
selections_last_update: 0,
|
||||
deferred_ops: OperationQueue::new(),
|
||||
|
@ -500,14 +532,75 @@ 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<str>,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) -> Task<Option<Vec<Operation>>> {
|
||||
let version = self.version.clone();
|
||||
let old_text = self.text();
|
||||
ctx.spawn(|handle, mut ctx| async move {
|
||||
let diff = 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::<Vec<_>>()
|
||||
}
|
||||
})
|
||||
.await;
|
||||
handle.update(&mut ctx, |this, 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 {
|
||||
self.version > self.saved_version
|
||||
&& self
|
||||
.file
|
||||
.as_ref()
|
||||
.map_or(false, |f| f.mtime() > self.saved_mtime)
|
||||
}
|
||||
|
||||
pub fn version(&self) -> time::Global {
|
||||
self.version.clone()
|
||||
}
|
||||
|
@ -1818,6 +1911,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(),
|
||||
|
@ -1849,6 +1943,7 @@ pub enum Event {
|
|||
Dirtied,
|
||||
Saved,
|
||||
FileHandleChanged,
|
||||
Reloaded,
|
||||
}
|
||||
|
||||
impl Entity for Buffer {
|
||||
|
@ -2380,7 +2475,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;
|
||||
|
@ -2969,8 +3067,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": "",
|
||||
|
@ -2978,9 +3074,10 @@ 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));
|
||||
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)
|
||||
});
|
||||
|
@ -3040,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<Buffer>| {
|
||||
ctx.subscribe(&ctx.handle(), {
|
||||
let events = events.clone();
|
||||
|
@ -3050,7 +3147,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;
|
||||
|
@ -3062,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<Buffer>| {
|
||||
ctx.subscribe(&ctx.handle(), {
|
||||
let events = events.clone();
|
||||
|
@ -3085,6 +3181,116 @@ mod tests {
|
|||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_file_changes_on_disk(mut app: gpui::TestAppContext) {
|
||||
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;
|
||||
|
||||
let abs_path = dir.path().join("the-file");
|
||||
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)
|
||||
});
|
||||
|
||||
// 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::Right)
|
||||
.unwrap();
|
||||
Selection {
|
||||
id: row as usize,
|
||||
start: anchor.clone(),
|
||||
end: anchor,
|
||||
reversed: false,
|
||||
goal: SelectionGoal::None,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
Some(ctx),
|
||||
)
|
||||
});
|
||||
|
||||
// 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 = "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_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()
|
||||
.map(|selection| {
|
||||
assert_eq!(selection.start, selection.end);
|
||||
selection.start.to_point(&buffer).unwrap()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
cursor_positions,
|
||||
&[Point::new(1, 0), Point::new(3, 0), Point::new(4, 0),]
|
||||
);
|
||||
});
|
||||
|
||||
// Modify the buffer
|
||||
buffer.update(&mut app, |buffer, ctx| {
|
||||
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\nAAAA\naaa\nBB\nbbbbb\n").unwrap();
|
||||
|
||||
// Becaues the buffer is modified, it doesn't reload from disk, but is
|
||||
// marked as having a conflict.
|
||||
buffer
|
||||
.condition_with_duration(Duration::from_millis(500), &app, |buffer, _| {
|
||||
buffer.has_conflict()
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_set_text_via_diff(mut app: gpui::TestAppContext) {
|
||||
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));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_undo_redo(app: &mut gpui::MutableAppContext) {
|
||||
app.add_model(|ctx| {
|
||||
|
|
|
@ -2396,6 +2396,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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2500,6 +2501,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)]
|
||||
|
|
|
@ -9,8 +9,8 @@ use crate::{
|
|||
use futures_core::Future;
|
||||
use gpui::{
|
||||
color::rgbu, elements::*, json::to_string_pretty, keymap::Binding, AnyViewHandle, AppContext,
|
||||
ClipboardItem, Entity, ModelHandle, MutableAppContext, PathPromptOptions, Task, View,
|
||||
ViewContext, ViewHandle, WeakModelHandle,
|
||||
ClipboardItem, Entity, ModelHandle, MutableAppContext, PathPromptOptions, PromptLevel, Task,
|
||||
View, ViewContext, ViewHandle, WeakModelHandle,
|
||||
};
|
||||
use log::error;
|
||||
pub use pane::*;
|
||||
|
@ -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<FileHandle>,
|
||||
|
@ -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<FileHandle>,
|
||||
|
@ -247,6 +251,10 @@ impl<T: ItemView> ItemViewHandle for ViewHandle<T> {
|
|||
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()
|
||||
}
|
||||
|
@ -361,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 {
|
||||
|
@ -381,14 +390,14 @@ impl Workspace {
|
|||
}
|
||||
}
|
||||
|
||||
fn file_for_path(&mut self, abs_path: &Path, ctx: &mut ViewContext<Self>) -> FileHandle {
|
||||
fn file_for_path(&mut self, abs_path: &Path, ctx: &mut ViewContext<Self>) -> Task<FileHandle> {
|
||||
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(
|
||||
|
@ -489,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)
|
||||
}))),
|
||||
|
@ -545,8 +555,8 @@ impl Workspace {
|
|||
|
||||
pub fn save_active_item(&mut self, _: &(), ctx: &mut ViewContext<Self>) {
|
||||
if let Some(item) = self.active_item(ctx) {
|
||||
let handle = ctx.handle();
|
||||
if item.entry_id(ctx.as_ref()).is_none() {
|
||||
let handle = ctx.handle();
|
||||
let start_path = self
|
||||
.worktrees
|
||||
.iter()
|
||||
|
@ -556,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);
|
||||
}
|
||||
|
@ -566,16 +577,32 @@ 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 save = item.save(None, ctx.as_mut());
|
||||
ctx.foreground()
|
||||
.spawn(async move {
|
||||
if let Err(e) = save.await {
|
||||
error!("failed to save item: {:?}, ", e);
|
||||
ctx.prompt(
|
||||
PromptLevel::Warning,
|
||||
CONFLICT_MESSAGE,
|
||||
&["Overwrite", "Cancel"],
|
||||
move |answer, ctx| {
|
||||
if answer == 0 {
|
||||
ctx.spawn(|mut ctx| async move {
|
||||
if let Err(error) = ctx.update(|ctx| item.save(None, ctx)).await {
|
||||
error!("failed to save item: {:?}, ", error);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
},
|
||||
);
|
||||
} else {
|
||||
ctx.spawn(|_, mut ctx| async move {
|
||||
if let Err(error) = ctx.update(|ctx| item.save(None, ctx)).await {
|
||||
error!("failed to save item: {:?}, ", error);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -732,7 +759,7 @@ mod tests {
|
|||
use super::*;
|
||||
use crate::{editor::BufferView, settings, test::temp_tree};
|
||||
use serde_json::json;
|
||||
use std::collections::HashSet;
|
||||
use std::{collections::HashSet, fs};
|
||||
use tempdir::TempDir;
|
||||
|
||||
#[gpui::test]
|
||||
|
@ -970,6 +997,55 @@ mod tests {
|
|||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_save_conflicting_item(mut app: gpui::TestAppContext) {
|
||||
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::<BufferView>().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));
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_open_and_save_new_file(mut app: gpui::TestAppContext) {
|
||||
let dir = TempDir::new("test-new-file").unwrap();
|
||||
|
|
|
@ -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.,
|
||||
});
|
||||
|
|
|
@ -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::{
|
||||
|
@ -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<Path>,
|
||||
is_deleted: bool,
|
||||
mtime: SystemTime,
|
||||
}
|
||||
|
||||
impl Worktree {
|
||||
|
@ -201,9 +202,10 @@ impl Worktree {
|
|||
path: &Path,
|
||||
ctx: &AppContext,
|
||||
) -> impl Future<Output = Result<History>> {
|
||||
let abs_path = self.absolutize(path);
|
||||
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)?;
|
||||
Ok(History::new(Arc::from(base_text)))
|
||||
|
@ -221,20 +223,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<HashMap<Arc<Path>, Weak<Mutex<FileHandleState>>>>,
|
||||
) -> 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 {
|
||||
|
@ -457,6 +468,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()
|
||||
}
|
||||
|
@ -927,41 +942,63 @@ impl BackgroundScanner {
|
|||
};
|
||||
|
||||
let mut renamed_paths: HashMap<u64, PathBuf> = 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<Path> =
|
||||
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<Path> =
|
||||
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();
|
||||
|
@ -1189,7 +1226,7 @@ struct UpdateIgnoreStatusJob {
|
|||
}
|
||||
|
||||
pub trait WorktreeHandle {
|
||||
fn file(&self, path: impl AsRef<Path>, app: &AppContext) -> FileHandle;
|
||||
fn file(&self, path: impl AsRef<Path>, app: &mut MutableAppContext) -> Task<FileHandle>;
|
||||
|
||||
#[cfg(test)]
|
||||
fn flush_fs_events<'a>(
|
||||
|
@ -1199,34 +1236,51 @@ pub trait WorktreeHandle {
|
|||
}
|
||||
|
||||
impl WorktreeHandle for ModelHandle<Worktree> {
|
||||
fn file(&self, path: impl AsRef<Path>, app: &AppContext) -> FileHandle {
|
||||
let path = path.as_ref();
|
||||
fn file(&self, path: impl AsRef<Path>, app: &mut MutableAppContext) -> Task<FileHandle> {
|
||||
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,
|
||||
}
|
||||
} else {
|
||||
FileHandleState {
|
||||
path: path.into(),
|
||||
is_deleted: !tree.path_is_pending(path),
|
||||
}
|
||||
};
|
||||
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,
|
||||
}
|
||||
};
|
||||
|
||||
let state = Arc::new(Mutex::new(handle_state.clone()));
|
||||
handles.insert(handle_state.path, Arc::downgrade(&state));
|
||||
state
|
||||
};
|
||||
|
||||
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
|
||||
|
@ -1484,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();
|
||||
|
@ -1511,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.
|
||||
|
|
Loading…
Reference in a new issue