Merge branch 'master' into file-changed-on-disk

This commit is contained in:
Max Brunsfeld 2021-05-12 16:20:03 -07:00
commit 4910bc50c6
14 changed files with 3718 additions and 4119 deletions

9
Cargo.lock generated
View file

@ -1180,6 +1180,7 @@ dependencies = [
"etagere",
"font-kit",
"foreign-types",
"gpui_macros",
"log",
"metal",
"num_cpus",
@ -1205,6 +1206,14 @@ dependencies = [
"usvg",
]
[[package]]
name = "gpui_macros"
version = "0.1.0"
dependencies = [
"quote",
"syn",
]
[[package]]
name = "hashbrown"
version = "0.9.1"

View file

@ -1,5 +1,5 @@
[workspace]
members = ["zed", "gpui", "fsevent", "scoped_pool"]
members = ["zed", "gpui", "gpui_macros", "fsevent", "scoped_pool"]
[patch.crates-io]
async-task = {git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e"}

View file

@ -8,6 +8,7 @@ version = "0.1.0"
async-task = "4.0.3"
ctor = "0.1"
etagere = "0.2"
gpui_macros = {path = "../gpui_macros"}
log = "0.4"
num_cpus = "1.13"
ordered-float = "2.1.1"

File diff suppressed because it is too large Load diff

View file

@ -24,6 +24,7 @@ pub mod color;
pub mod json;
pub mod keymap;
mod platform;
pub use gpui_macros::test;
pub use platform::{Event, PathPromptOptions, PromptLevel};
pub use presenter::{
AfterLayoutContext, Axis, DebugContext, EventContext, LayoutContext, PaintContext,

11
gpui_macros/Cargo.toml Normal file
View file

@ -0,0 +1,11 @@
[package]
name = "gpui_macros"
version = "0.1.0"
edition = "2018"
[lib]
proc-macro = true
[dependencies]
syn = "1.0"
quote = "1.0"

57
gpui_macros/src/lib.rs Normal file
View file

@ -0,0 +1,57 @@
use std::mem;
use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::{parse_macro_input, parse_quote, AttributeArgs, ItemFn, Meta, NestedMeta};
#[proc_macro_attribute]
pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
let mut namespace = format_ident!("gpui");
let args = syn::parse_macro_input!(args as AttributeArgs);
for arg in args {
match arg {
NestedMeta::Meta(Meta::Path(name))
if name.get_ident().map_or(false, |n| n == "self") =>
{
namespace = format_ident!("crate");
}
other => {
return TokenStream::from(
syn::Error::new_spanned(other, "invalid argument").into_compile_error(),
)
}
}
}
let mut inner_fn = parse_macro_input!(function as ItemFn);
let inner_fn_attributes = mem::take(&mut inner_fn.attrs);
let inner_fn_name = format_ident!("_{}", inner_fn.sig.ident);
let outer_fn_name = mem::replace(&mut inner_fn.sig.ident, inner_fn_name.clone());
let mut outer_fn: ItemFn = if inner_fn.sig.asyncness.is_some() {
parse_quote! {
#[test]
fn #outer_fn_name() {
#inner_fn
#namespace::App::test_async((), move |ctx| async {
#inner_fn_name(ctx).await;
});
}
}
} else {
parse_quote! {
#[test]
fn #outer_fn_name() {
#inner_fn
#namespace::App::test((), |ctx| {
#inner_fn_name(ctx);
});
}
}
};
outer_fn.attrs.extend(inner_fn_attributes);
TokenStream::from(quote!(#outer_fn))
}

View file

@ -4,12 +4,10 @@ mod selection;
mod text;
pub use anchor::*;
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::*;
use crate::{
@ -20,7 +18,7 @@ use crate::{
worktree::FileHandle,
};
use anyhow::{anyhow, Result};
use gpui::{Entity, EntityTask, ModelContext};
use gpui::{Entity, ModelContext, Task};
use lazy_static::lazy_static;
use rand::prelude::*;
use std::{
@ -385,24 +383,28 @@ impl Buffer {
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, move |this, ops, ctx| {
if ops.is_some() {
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();
}
},
)
.detach()
}
}
ctx.emit(Event::FileHandleChanged);
});
@ -499,21 +501,22 @@ impl Buffer {
&mut self,
new_file: Option<FileHandle>,
ctx: &mut ModelContext<Self>,
) -> LocalBoxFuture<'static, Result<()>> {
) -> Task<Result<()>> {
let snapshot = self.snapshot();
let version = self.version.clone();
if let Some(file) = new_file.as_ref().or(self.file.as_ref()) {
let save_task = file.save(snapshot, ctx.as_ref());
ctx.spawn(save_task, |me, save_result, ctx| {
if save_result.is_ok() {
me.did_save(version, new_file, ctx);
let file = self.file.clone();
ctx.spawn(|handle, mut ctx| async move {
if let Some(file) = new_file.as_ref().or(file.as_ref()) {
let result = ctx.read(|ctx| file.save(snapshot, ctx.as_ref())).await;
if result.is_ok() {
handle.update(&mut ctx, |me, ctx| me.did_save(version, new_file, ctx));
}
save_result
})
.boxed_local()
result
} else {
async { Ok(()) }.boxed_local()
Ok(())
}
})
}
fn did_save(
@ -536,11 +539,13 @@ impl Buffer {
&mut self,
new_text: Arc<str>,
ctx: &mut ModelContext<Self>,
) -> EntityTask<Option<Vec<Operation>>> {
) -> Task<Option<Vec<Operation>>> {
let version = self.version.clone();
let old_text = self.text();
ctx.spawn(
ctx.background_executor().spawn({
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())
@ -548,8 +553,9 @@ impl Buffer {
.map(|c| (c.tag(), c.value().len()))
.collect::<Vec<_>>()
}
}),
move |this, diff, ctx| {
})
.await;
handle.update(&mut ctx, |this, ctx| {
if this.version == version {
this.start_transaction(None).unwrap();
let mut operations = Vec::new();
@ -575,8 +581,8 @@ impl Buffer {
} else {
None
}
},
)
})
})
}
pub fn is_dirty(&self) -> bool {
@ -2480,9 +2486,8 @@ mod tests {
sync::atomic::{self, AtomicUsize},
};
#[test]
fn test_edit() {
App::test((), |ctx| {
#[gpui::test]
fn test_edit(ctx: &mut gpui::MutableAppContext) {
ctx.add_model(|ctx| {
let mut buffer = Buffer::new(0, "abc", ctx);
assert_eq!(buffer.text(), "abc");
@ -2498,12 +2503,10 @@ mod tests {
assert_eq!(buffer.text(), "ghiamnoef");
buffer
});
})
}
#[test]
fn test_edit_events() {
App::test((), |app| {
#[gpui::test]
fn test_edit_events(app: &mut gpui::MutableAppContext) {
let mut now = Instant::now();
let buffer_1_events = Rc::new(RefCell::new(Vec::new()));
let buffer_2_events = Rc::new(RefCell::new(Vec::new()));
@ -2558,13 +2561,11 @@ mod tests {
let buffer_2_events = buffer_2_events.borrow();
assert_eq!(*buffer_2_events, vec![Event::Edited, Event::Dirtied]);
});
}
#[test]
fn test_random_edits() {
#[gpui::test]
fn test_random_edits(ctx: &mut gpui::MutableAppContext) {
for seed in 0..100 {
App::test((), |ctx| {
println!("{:?}", seed);
let mut rng = &mut StdRng::seed_from_u64(seed);
@ -2638,8 +2639,7 @@ mod tests {
let old_len = old_range.end - old_range.start;
let new_len = new_range.end - new_range.start;
let old_start = (old_range.start as isize + delta) as usize;
let new_text: String =
buffer.text_for_range(new_range).unwrap().collect();
let new_text: String = buffer.text_for_range(new_range).unwrap().collect();
old_buffer
.edit(Some(old_start..old_start + old_len), new_text, None)
.unwrap();
@ -2650,14 +2650,12 @@ mod tests {
}
buffer
})
});
}
}
#[test]
fn test_line_len() {
App::test((), |ctx| {
#[gpui::test]
fn test_line_len(ctx: &mut gpui::MutableAppContext) {
ctx.add_model(|ctx| {
let mut buffer = Buffer::new(0, "", ctx);
buffer.edit(vec![0..0], "abcd\nefg\nhij", None).unwrap();
@ -2674,12 +2672,10 @@ mod tests {
assert!(buffer.line_len(6).is_err());
buffer
});
});
}
#[test]
fn test_rightmost_point() {
App::test((), |ctx| {
#[gpui::test]
fn test_rightmost_point(ctx: &mut gpui::MutableAppContext) {
ctx.add_model(|ctx| {
let mut buffer = Buffer::new(0, "", ctx);
assert_eq!(buffer.rightmost_point().row, 0);
@ -2695,12 +2691,10 @@ mod tests {
assert_eq!(buffer.rightmost_point().row, 4);
buffer
});
});
}
#[test]
fn test_text_summary_for_range() {
App::test((), |ctx| {
#[gpui::test]
fn test_text_summary_for_range(ctx: &mut gpui::MutableAppContext) {
ctx.add_model(|ctx| {
let buffer = Buffer::new(0, "ab\nefg\nhklm\nnopqrs\ntuvwxyz", ctx);
let text = Text::from(buffer.text());
@ -2726,12 +2720,10 @@ mod tests {
);
buffer
});
});
}
#[test]
fn test_chars_at() {
App::test((), |ctx| {
#[gpui::test]
fn test_chars_at(ctx: &mut gpui::MutableAppContext) {
ctx.add_model(|ctx| {
let mut buffer = Buffer::new(0, "", ctx);
buffer.edit(vec![0..0], "abcd\nefgh\nij", None).unwrap();
@ -2764,7 +2756,6 @@ mod tests {
buffer
});
});
}
// #[test]
@ -2881,9 +2872,8 @@ mod tests {
}
}
#[test]
fn test_anchors() {
App::test((), |ctx| {
#[gpui::test]
fn test_anchors(ctx: &mut gpui::MutableAppContext) {
ctx.add_model(|ctx| {
let mut buffer = Buffer::new(0, "", ctx);
buffer.edit(vec![0..0], "abc", None).unwrap();
@ -3043,12 +3033,10 @@ mod tests {
);
buffer
});
});
}
#[test]
fn test_anchors_at_start_and_end() {
App::test((), |ctx| {
#[gpui::test]
fn test_anchors_at_start_and_end(ctx: &mut gpui::MutableAppContext) {
ctx.add_model(|ctx| {
let mut buffer = Buffer::new(0, "", ctx);
let before_start_anchor = buffer.anchor_before(0).unwrap();
@ -3071,7 +3059,6 @@ mod tests {
assert_eq!(after_end_anchor.to_offset(&buffer).unwrap(), 9);
buffer
});
});
}
#[test]
@ -3190,9 +3177,8 @@ mod tests {
});
}
#[test]
fn test_file_changes_on_disk() {
App::test_async((), |mut app| async move {
#[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));
@ -3279,12 +3265,10 @@ mod tests {
buffer
.condition(&app, |buffer, _| buffer.has_conflict())
.await;
});
}
#[test]
fn test_set_text_via_diff() {
App::test_async((), |mut app| async move {
#[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));
@ -3299,12 +3283,10 @@ mod tests {
.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| {
#[gpui::test]
fn test_undo_redo(app: &mut gpui::MutableAppContext) {
app.add_model(|ctx| {
let mut buffer = Buffer::new(0, "1234", ctx);
@ -3336,18 +3318,16 @@ mod tests {
buffer
});
});
}
#[test]
fn test_history() {
App::test((), |app| {
#[gpui::test]
fn test_history(app: &mut gpui::MutableAppContext) {
app.add_model(|ctx| {
let mut now = Instant::now();
let mut buffer = Buffer::new(0, "123456", ctx);
let (set_id, _) = buffer
.add_selection_set(buffer.selections_from_ranges(vec![4..4]).unwrap(), None);
let (set_id, _) =
buffer.add_selection_set(buffer.selections_from_ranges(vec![4..4]).unwrap(), None);
buffer.start_transaction_at(Some(set_id), now).unwrap();
buffer.edit(vec![2..4], "cd", None).unwrap();
buffer.end_transaction_at(Some(set_id), now, None).unwrap();
@ -3406,11 +3386,10 @@ mod tests {
buffer
});
});
}
#[test]
fn test_random_concurrent_edits() {
#[gpui::test]
fn test_random_concurrent_edits(ctx: &mut gpui::MutableAppContext) {
use crate::test::Network;
const PEERS: usize = 5;
@ -3419,7 +3398,6 @@ mod tests {
println!("{:?}", seed);
let mut rng = &mut StdRng::seed_from_u64(seed);
App::test((), |ctx| {
let base_text_len = rng.gen_range(0..10);
let base_text = RandomCharIter::new(&mut rng)
.take(base_text_len)
@ -3478,7 +3456,6 @@ mod tests {
.collect::<HashMap<_, _>>()
);
}
});
}
}

View file

@ -4,11 +4,10 @@ use super::{
};
use crate::{settings::Settings, util::post_inc, workspace, worktree::FileHandle};
use anyhow::Result;
use futures_core::future::LocalBoxFuture;
use gpui::{
fonts::Properties as FontProperties, geometry::vector::Vector2F, keymap::Binding, text_layout,
AppContext, ClipboardItem, Element, ElementBox, Entity, FontCache, ModelHandle,
MutableAppContext, TextLayoutCache, View, ViewContext, WeakViewHandle,
MutableAppContext, Task, TextLayoutCache, View, ViewContext, WeakViewHandle,
};
use parking_lot::Mutex;
use postage::watch;
@ -2348,13 +2347,12 @@ impl BufferView {
ctx.notify();
let epoch = self.next_blink_epoch();
ctx.spawn(
async move {
ctx.spawn(|this, mut ctx| async move {
Timer::after(CURSOR_BLINK_INTERVAL).await;
epoch
},
Self::resume_cursor_blinking,
)
this.update(&mut ctx, |this, ctx| {
this.resume_cursor_blinking(epoch, ctx);
})
})
.detach();
}
@ -2371,13 +2369,10 @@ impl BufferView {
ctx.notify();
let epoch = self.next_blink_epoch();
ctx.spawn(
async move {
ctx.spawn(|this, mut ctx| async move {
Timer::after(CURSOR_BLINK_INTERVAL).await;
epoch
},
Self::blink_cursors,
)
this.update(&mut ctx, |this, ctx| this.blink_cursors(epoch, ctx));
})
.detach();
}
}
@ -2499,7 +2494,7 @@ impl workspace::ItemView for BufferView {
&mut self,
new_file: Option<FileHandle>,
ctx: &mut ViewContext<Self>,
) -> LocalBoxFuture<'static, Result<()>> {
) -> Task<Result<()>> {
self.buffer.update(ctx, |b, ctx| b.save(new_file, ctx))
}
@ -2516,17 +2511,13 @@ impl workspace::ItemView for BufferView {
mod tests {
use super::*;
use crate::{editor::Point, settings, test::sample_text};
use gpui::App;
use unindent::Unindent;
#[test]
fn test_selection_with_mouse() {
App::test((), |app| {
let buffer =
app.add_model(|ctx| Buffer::new(0, "aaaaaa\nbbbbbb\ncccccc\ndddddd\n", ctx));
#[gpui::test]
fn test_selection_with_mouse(app: &mut gpui::MutableAppContext) {
let buffer = app.add_model(|ctx| Buffer::new(0, "aaaaaa\nbbbbbb\ncccccc\ndddddd\n", ctx));
let settings = settings::channel(&app.font_cache()).unwrap().1;
let (_, buffer_view) =
app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx));
let (_, buffer_view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx));
buffer_view.update(app, |view, ctx| {
view.begin_selection(DisplayPoint::new(2, 2), false, ctx);
@ -2628,14 +2619,11 @@ mod tests {
selections,
[DisplayPoint::new(3, 3)..DisplayPoint::new(0, 0)]
);
});
}
#[test]
fn test_canceling_pending_selection() {
App::test((), |app| {
let buffer =
app.add_model(|ctx| Buffer::new(0, "aaaaaa\nbbbbbb\ncccccc\ndddddd\n", ctx));
#[gpui::test]
fn test_canceling_pending_selection(app: &mut gpui::MutableAppContext) {
let buffer = app.add_model(|ctx| Buffer::new(0, "aaaaaa\nbbbbbb\ncccccc\ndddddd\n", ctx));
let settings = settings::channel(&app.font_cache()).unwrap().1;
let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx));
@ -2663,14 +2651,11 @@ mod tests {
view.read(app).selection_ranges(app.as_ref()),
[DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)]
);
});
}
#[test]
fn test_cancel() {
App::test((), |app| {
let buffer =
app.add_model(|ctx| Buffer::new(0, "aaaaaa\nbbbbbb\ncccccc\ndddddd\n", ctx));
#[gpui::test]
fn test_cancel(app: &mut gpui::MutableAppContext) {
let buffer = app.add_model(|ctx| Buffer::new(0, "aaaaaa\nbbbbbb\ncccccc\ndddddd\n", ctx));
let settings = settings::channel(&app.font_cache()).unwrap().1;
let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx));
@ -2702,32 +2687,27 @@ mod tests {
view.read(app).selection_ranges(app.as_ref()),
[DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1)]
);
});
}
#[test]
fn test_layout_line_numbers() {
App::test((), |app| {
#[gpui::test]
fn test_layout_line_numbers(app: &mut gpui::MutableAppContext) {
let layout_cache = TextLayoutCache::new(app.platform().fonts());
let font_cache = app.font_cache().clone();
let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(6, 6), ctx));
let settings = settings::channel(&font_cache).unwrap().1;
let (_, view) =
app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), settings, ctx));
let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), settings, ctx));
let layouts = view
.read(app)
.layout_line_numbers(1000.0, &font_cache, &layout_cache, app.as_ref())
.unwrap();
assert_eq!(layouts.len(), 6);
})
}
#[test]
fn test_fold() {
App::test((), |app| {
#[gpui::test]
fn test_fold(app: &mut gpui::MutableAppContext) {
let buffer = app.add_model(|ctx| {
Buffer::new(
0,
@ -2753,14 +2733,10 @@ mod tests {
)
});
let settings = settings::channel(&app.font_cache()).unwrap().1;
let (_, view) =
app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), settings, ctx));
let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), settings, ctx));
view.update(app, |view, ctx| {
view.select_display_ranges(
&[DisplayPoint::new(8, 0)..DisplayPoint::new(12, 0)],
ctx,
)
view.select_display_ranges(&[DisplayPoint::new(8, 0)..DisplayPoint::new(12, 0)], ctx)
.unwrap();
view.fold(&(), ctx);
assert_eq!(
@ -2817,16 +2793,13 @@ mod tests {
view.unfold(&(), ctx);
assert_eq!(view.text(ctx.as_ref()), buffer.read(ctx).text());
});
});
}
#[test]
fn test_move_cursor() {
App::test((), |app| {
#[gpui::test]
fn test_move_cursor(app: &mut gpui::MutableAppContext) {
let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(6, 6), ctx));
let settings = settings::channel(&app.font_cache()).unwrap().1;
let (_, view) =
app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), settings, ctx));
let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), settings, ctx));
buffer.update(app, |buffer, ctx| {
buffer
@ -2878,10 +2851,7 @@ mod tests {
&[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)]
);
view.select_display_ranges(
&[DisplayPoint::new(0, 1)..DisplayPoint::new(0, 2)],
ctx,
)
view.select_display_ranges(&[DisplayPoint::new(0, 1)..DisplayPoint::new(0, 2)], ctx)
.unwrap();
view.select_to_beginning(&(), ctx);
assert_eq!(
@ -2895,12 +2865,10 @@ mod tests {
&[DisplayPoint::new(0, 1)..DisplayPoint::new(5, 6)]
);
});
});
}
#[test]
fn test_beginning_end_of_line() {
App::test((), |app| {
#[gpui::test]
fn test_beginning_end_of_line(app: &mut gpui::MutableAppContext) {
let buffer = app.add_model(|ctx| Buffer::new(0, "abc\n def", ctx));
let settings = settings::channel(&app.font_cache()).unwrap().1;
let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx));
@ -3023,14 +2991,12 @@ mod tests {
DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0),
]
);
});
}
#[test]
fn test_prev_next_word_boundary() {
App::test((), |app| {
let buffer = app
.add_model(|ctx| Buffer::new(0, "use std::str::{foo, bar}\n\n {baz.qux()}", ctx));
#[gpui::test]
fn test_prev_next_word_boundary(app: &mut gpui::MutableAppContext) {
let buffer =
app.add_model(|ctx| Buffer::new(0, "use std::str::{foo, bar}\n\n {baz.qux()}", ctx));
let settings = settings::channel(&app.font_cache()).unwrap().1;
let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx));
view.update(app, |view, ctx| {
@ -3205,12 +3171,10 @@ mod tests {
DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2),
]
);
});
}
#[test]
fn test_backspace() {
App::test((), |app| {
#[gpui::test]
fn test_backspace(app: &mut gpui::MutableAppContext) {
let buffer = app.add_model(|ctx| {
Buffer::new(
0,
@ -3219,8 +3183,7 @@ mod tests {
)
});
let settings = settings::channel(&app.font_cache()).unwrap().1;
let (_, view) =
app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), settings, ctx));
let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), settings, ctx));
view.update(app, |view, ctx| {
view.select_display_ranges(
@ -3242,12 +3205,10 @@ mod tests {
buffer.read(app).text(),
"oe two three\nfou five six\nseven ten\n"
);
})
}
#[test]
fn test_delete() {
App::test((), |app| {
#[gpui::test]
fn test_delete(app: &mut gpui::MutableAppContext) {
let buffer = app.add_model(|ctx| {
Buffer::new(
0,
@ -3256,8 +3217,7 @@ mod tests {
)
});
let settings = settings::channel(&app.font_cache()).unwrap().1;
let (_, view) =
app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), settings, ctx));
let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), settings, ctx));
view.update(app, |view, ctx| {
view.select_display_ranges(
@ -3279,12 +3239,10 @@ mod tests {
buffer.read(app).text(),
"on two three\nfou five six\nseven ten\n"
);
})
}
#[test]
fn test_delete_line() {
App::test((), |app| {
#[gpui::test]
fn test_delete_line(app: &mut gpui::MutableAppContext) {
let settings = settings::channel(&app.font_cache()).unwrap().1;
let buffer = app.add_model(|ctx| Buffer::new(0, "abc\ndef\nghi\n", ctx));
let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx));
@ -3313,10 +3271,7 @@ mod tests {
let buffer = app.add_model(|ctx| Buffer::new(0, "abc\ndef\nghi\n", ctx));
let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx));
view.update(app, |view, ctx| {
view.select_display_ranges(
&[DisplayPoint::new(2, 0)..DisplayPoint::new(0, 1)],
ctx,
)
view.select_display_ranges(&[DisplayPoint::new(2, 0)..DisplayPoint::new(0, 1)], ctx)
.unwrap();
view.delete_line(&(), ctx);
});
@ -3325,12 +3280,10 @@ mod tests {
view.read(app).selection_ranges(app.as_ref()),
vec![DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1)]
);
});
}
#[test]
fn test_duplicate_line() {
App::test((), |app| {
#[gpui::test]
fn test_duplicate_line(app: &mut gpui::MutableAppContext) {
let settings = settings::channel(&app.font_cache()).unwrap().1;
let buffer = app.add_model(|ctx| Buffer::new(0, "abc\ndef\nghi\n", ctx));
let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx));
@ -3386,12 +3339,10 @@ mod tests {
DisplayPoint::new(4, 2)..DisplayPoint::new(5, 1),
]
);
});
}
#[test]
fn test_move_line_up_down() {
App::test((), |app| {
#[gpui::test]
fn test_move_line_up_down(app: &mut gpui::MutableAppContext) {
let settings = settings::channel(&app.font_cache()).unwrap().1;
let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(10, 5), ctx));
let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx));
@ -3479,12 +3430,10 @@ mod tests {
DisplayPoint::new(4, 0)..DisplayPoint::new(4, 2)
]
);
});
}
#[test]
fn test_clipboard() {
App::test((), |app| {
#[gpui::test]
fn test_clipboard(app: &mut gpui::MutableAppContext) {
let buffer = app.add_model(|ctx| Buffer::new(0, "one two three four five six ", ctx));
let settings = settings::channel(&app.font_cache()).unwrap().1;
let view = app
@ -3586,10 +3535,7 @@ mod tests {
// Copy with a single cursor only, which writes the whole line into the clipboard.
view.update(app, |view, ctx| {
view.select_display_ranges(
&[DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1)],
ctx,
)
view.select_display_ranges(&[DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1)], ctx)
.unwrap();
view.copy(&(), ctx);
});
@ -3620,12 +3566,10 @@ mod tests {
DisplayPoint::new(5, 1)..DisplayPoint::new(5, 1),
]
);
});
}
#[test]
fn test_select_all() {
App::test((), |app| {
#[gpui::test]
fn test_select_all(app: &mut gpui::MutableAppContext) {
let buffer = app.add_model(|ctx| Buffer::new(0, "abc\nde\nfgh", ctx));
let settings = settings::channel(&app.font_cache()).unwrap().1;
let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx));
@ -3634,12 +3578,10 @@ mod tests {
view.read(app).selection_ranges(app.as_ref()),
&[DisplayPoint::new(0, 0)..DisplayPoint::new(2, 3)]
);
});
}
#[test]
fn test_select_line() {
App::test((), |app| {
#[gpui::test]
fn test_select_line(app: &mut gpui::MutableAppContext) {
let settings = settings::channel(&app.font_cache()).unwrap().1;
let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(6, 5), ctx));
let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx));
@ -3678,12 +3620,10 @@ mod tests {
view.read(app).selection_ranges(app.as_ref()),
vec![DisplayPoint::new(0, 0)..DisplayPoint::new(5, 5)]
);
});
}
#[test]
fn test_split_selection_into_lines() {
App::test((), |app| {
#[gpui::test]
fn test_split_selection_into_lines(app: &mut gpui::MutableAppContext) {
let settings = settings::channel(&app.font_cache()).unwrap().1;
let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(9, 5), ctx));
let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx));
@ -3728,10 +3668,7 @@ mod tests {
);
view.update(app, |view, ctx| {
view.select_display_ranges(
&[DisplayPoint::new(4, 0)..DisplayPoint::new(0, 1)],
ctx,
)
view.select_display_ranges(&[DisplayPoint::new(4, 0)..DisplayPoint::new(0, 1)], ctx)
.unwrap();
view.split_selection_into_lines(&(), ctx);
});
@ -3752,21 +3689,16 @@ mod tests {
DisplayPoint::new(7, 0)..DisplayPoint::new(7, 0)
]
);
});
}
#[test]
fn test_add_selection_above_below() {
App::test((), |app| {
#[gpui::test]
fn test_add_selection_above_below(app: &mut gpui::MutableAppContext) {
let settings = settings::channel(&app.font_cache()).unwrap().1;
let buffer = app.add_model(|ctx| Buffer::new(0, "abc\ndefghi\n\njk\nlmno\n", ctx));
let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx));
view.update(app, |view, ctx| {
view.select_display_ranges(
&[DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)],
ctx,
)
view.select_display_ranges(&[DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)], ctx)
.unwrap();
});
view.update(app, |view, ctx| view.add_selection_above(&(), ctx));
@ -3812,10 +3744,7 @@ mod tests {
);
view.update(app, |view, ctx| {
view.select_display_ranges(
&[DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3)],
ctx,
)
view.select_display_ranges(&[DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3)], ctx)
.unwrap();
});
view.update(app, |view, ctx| view.add_selection_below(&(), ctx));
@ -3849,10 +3778,7 @@ mod tests {
);
view.update(app, |view, ctx| {
view.select_display_ranges(
&[DisplayPoint::new(0, 1)..DisplayPoint::new(1, 4)],
ctx,
)
view.select_display_ranges(&[DisplayPoint::new(0, 1)..DisplayPoint::new(1, 4)], ctx)
.unwrap();
});
view.update(app, |view, ctx| view.add_selection_below(&(), ctx));
@ -3887,10 +3813,7 @@ mod tests {
);
view.update(app, |view, ctx| {
view.select_display_ranges(
&[DisplayPoint::new(4, 3)..DisplayPoint::new(1, 1)],
ctx,
)
view.select_display_ranges(&[DisplayPoint::new(4, 3)..DisplayPoint::new(1, 1)], ctx)
.unwrap();
});
view.update(app, |view, ctx| view.add_selection_above(&(), ctx));
@ -3913,7 +3836,6 @@ mod tests {
DisplayPoint::new(4, 3)..DisplayPoint::new(4, 1),
]
);
});
}
impl BufferView {

View file

@ -671,11 +671,9 @@ mod tests {
use super::*;
use crate::test::sample_text;
use buffer::ToPoint;
use gpui::App;
#[test]
fn test_basic_folds() {
App::test((), |app| {
#[gpui::test]
fn test_basic_folds(app: &mut gpui::MutableAppContext) {
let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(5, 6), ctx));
let mut map = FoldMap::new(buffer.clone(), app.as_ref());
@ -715,12 +713,10 @@ mod tests {
map.unfold(Some(Point::new(0, 4)..Point::new(0, 5)), app.as_ref())
.unwrap();
assert_eq!(map.text(app.as_ref()), "123aaaaa\nbbbbbb\nccc123456eee");
});
}
#[test]
fn test_adjacent_folds() {
App::test((), |app| {
#[gpui::test]
fn test_adjacent_folds(app: &mut gpui::MutableAppContext) {
let buffer = app.add_model(|ctx| Buffer::new(0, "abcdefghijkl", ctx));
{
@ -758,12 +754,10 @@ mod tests {
map.check_invariants(app.as_ref());
assert_eq!(map.text(app.as_ref()), "12345…fghijkl");
}
});
}
#[test]
fn test_overlapping_folds() {
App::test((), |app| {
#[gpui::test]
fn test_overlapping_folds(app: &mut gpui::MutableAppContext) {
let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(5, 6), ctx));
let mut map = FoldMap::new(buffer.clone(), app.as_ref());
map.fold(
@ -777,12 +771,10 @@ mod tests {
)
.unwrap();
assert_eq!(map.text(app.as_ref()), "aa…eeeee");
})
}
#[test]
fn test_merging_folds_via_edit() {
App::test((), |app| {
#[gpui::test]
fn test_merging_folds_via_edit(app: &mut gpui::MutableAppContext) {
let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(5, 6), ctx));
let mut map = FoldMap::new(buffer.clone(), app.as_ref());
@ -802,12 +794,10 @@ mod tests {
.unwrap();
});
assert_eq!(map.text(app.as_ref()), "aa…eeeee");
});
}
#[test]
fn test_folds_in_range() {
App::test((), |app| {
#[gpui::test]
fn test_folds_in_range(app: &mut gpui::MutableAppContext) {
let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(5, 6), ctx));
let mut map = FoldMap::new(buffer.clone(), app.as_ref());
let buffer = buffer.read(app);
@ -825,9 +815,7 @@ mod tests {
let fold_ranges = map
.folds_in_range(Point::new(1, 0)..Point::new(1, 3), app.as_ref())
.unwrap()
.map(|fold| {
fold.start.to_point(buffer).unwrap()..fold.end.to_point(buffer).unwrap()
})
.map(|fold| fold.start.to_point(buffer).unwrap()..fold.end.to_point(buffer).unwrap())
.collect::<Vec<_>>();
assert_eq!(
fold_ranges,
@ -836,11 +824,10 @@ mod tests {
Point::new(1, 2)..Point::new(3, 2)
]
);
});
}
#[test]
fn test_random_folds() {
#[gpui::test]
fn test_random_folds(app: &mut gpui::MutableAppContext) {
use crate::editor::ToPoint;
use crate::util::RandomCharIter;
use rand::prelude::*;
@ -863,7 +850,6 @@ mod tests {
dbg!(seed);
let mut rng = StdRng::seed_from_u64(seed);
App::test((), |app| {
let buffer = app.add_model(|ctx| {
let len = rng.gen_range(0..10);
let text = RandomCharIter::new(&mut rng).take(len).collect::<String>();
@ -991,10 +977,8 @@ mod tests {
}
for fold_range in map.merged_fold_ranges(app.as_ref()) {
let display_point = map.to_display_point(
fold_range.start.to_point(buffer).unwrap(),
app.as_ref(),
);
let display_point = map
.to_display_point(fold_range.start.to_point(buffer).unwrap(), app.as_ref());
assert!(map.is_line_folded(display_point.row(), app.as_ref()));
}
@ -1023,13 +1007,11 @@ mod tests {
);
}
}
});
}
}
#[test]
fn test_buffer_rows() {
App::test((), |app| {
#[gpui::test]
fn test_buffer_rows(app: &mut gpui::MutableAppContext) {
let text = sample_text(6, 6) + "\n";
let buffer = app.add_model(|ctx| Buffer::new(0, text, ctx));
@ -1059,7 +1041,6 @@ mod tests {
.collect::<Vec<_>>(),
vec![6]
);
});
}
impl FoldMap {

View file

@ -339,11 +339,9 @@ pub fn collapse_tabs(
mod tests {
use super::*;
use crate::test::*;
use gpui::App;
#[test]
fn test_chars_at() {
App::test((), |app| {
#[gpui::test]
fn test_chars_at(app: &mut gpui::MutableAppContext) {
let text = sample_text(6, 6);
let buffer = app.add_model(|ctx| Buffer::new(0, text, ctx));
let map = DisplayMap::new(buffer.clone(), 4, app.as_ref());
@ -385,7 +383,6 @@ mod tests {
.collect::<String>(),
" bbbbb\nc c"
);
});
}
#[test]
@ -411,12 +408,10 @@ mod tests {
assert_eq!(collapse_tabs("\ta".chars(), 5, Bias::Right, 4), (2, 0));
}
#[test]
fn test_max_point() {
App::test((), |app| {
#[gpui::test]
fn test_max_point(app: &mut gpui::MutableAppContext) {
let buffer = app.add_model(|ctx| Buffer::new(0, "aaa\n\t\tbbb", ctx));
let map = DisplayMap::new(buffer.clone(), 4, app.as_ref());
assert_eq!(map.max_point(app.as_ref()), DisplayPoint::new(1, 11))
});
}
}

View file

@ -399,7 +399,7 @@ impl FileFinder {
self.cancel_flag.store(true, atomic::Ordering::Relaxed);
self.cancel_flag = Arc::new(AtomicBool::new(false));
let cancel_flag = self.cancel_flag.clone();
let task = ctx.background_executor().spawn(async move {
let background_task = ctx.background_executor().spawn(async move {
let include_root_name = snapshots.len() > 1;
let matches = match_paths(
snapshots.iter(),
@ -415,7 +415,11 @@ impl FileFinder {
(search_id, did_cancel, query, matches)
});
ctx.spawn(task, Self::update_matches).detach();
ctx.spawn(|this, mut ctx| async move {
let matches = background_task.await;
this.update(&mut ctx, |this, ctx| this.update_matches(matches, ctx));
})
.detach();
Some(())
}
@ -453,14 +457,12 @@ impl FileFinder {
mod tests {
use super::*;
use crate::{editor, settings, test::temp_tree, workspace::Workspace};
use gpui::App;
use serde_json::json;
use std::fs;
use tempdir::TempDir;
#[test]
fn test_matching_paths() {
App::test_async((), |mut app| async move {
#[gpui::test]
async fn test_matching_paths(mut app: gpui::TestAppContext) {
let tmp_dir = TempDir::new("example").unwrap();
fs::create_dir(tmp_dir.path().join("a")).unwrap();
fs::write(tmp_dir.path().join("a/banana"), "banana").unwrap();
@ -524,12 +526,10 @@ mod tests {
let active_item = active_pane.read(ctx).active_item().unwrap();
assert_eq!(active_item.title(ctx), "bandana");
});
});
}
#[test]
fn test_matching_cancellation() {
App::test_async((), |mut app| async move {
#[gpui::test]
async fn test_matching_cancellation(mut app: gpui::TestAppContext) {
let tmp_dir = temp_tree(json!({
"hello": "",
"goodbye": "",
@ -547,8 +547,7 @@ mod tests {
});
app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx))
.await;
let (_, finder) =
app.add_window(|ctx| FileFinder::new(settings, workspace.clone(), ctx));
let (_, finder) = app.add_window(|ctx| FileFinder::new(settings, workspace.clone(), ctx));
let query = "hi".to_string();
finder.update(&mut app, |f, ctx| f.spawn_search(query.clone(), ctx));
@ -584,12 +583,10 @@ mod tests {
assert_eq!(finder.matches, matches[0..4])
});
});
}
#[test]
fn test_single_file_worktrees() {
App::test_async((), |mut app| async move {
#[gpui::test]
async fn test_single_file_worktrees(mut app: gpui::TestAppContext) {
let temp_dir = TempDir::new("test-single-file-worktrees").unwrap();
let dir_path = temp_dir.path().join("the-parent-dir");
let file_path = dir_path.join("the-file");
@ -604,8 +601,7 @@ mod tests {
});
app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx))
.await;
let (_, finder) =
app.add_window(|ctx| FileFinder::new(settings, workspace.clone(), ctx));
let (_, finder) = app.add_window(|ctx| FileFinder::new(settings, workspace.clone(), ctx));
// Even though there is only one worktree, that worktree's filename
// is included in the matching, because the worktree is a single file.
@ -627,12 +623,10 @@ mod tests {
// not match anything.
finder.update(&mut app, |f, ctx| f.spawn_search("thf/".into(), ctx));
finder.condition(&app, |f, _| f.matches.len() == 0).await;
});
}
#[test]
fn test_multiple_matches_with_same_relative_path() {
App::test_async((), |mut app| async move {
#[gpui::test]
async fn test_multiple_matches_with_same_relative_path(mut app: gpui::TestAppContext) {
let tmp_dir = temp_tree(json!({
"dir1": { "a.txt": "" },
"dir2": { "a.txt": "" }
@ -652,8 +646,7 @@ mod tests {
app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx))
.await;
let (_, finder) =
app.add_window(|ctx| FileFinder::new(settings, workspace.clone(), ctx));
let (_, finder) = app.add_window(|ctx| FileFinder::new(settings, workspace.clone(), ctx));
// Run a search that matches two files with the same relative path.
finder.update(&mut app, |f, ctx| f.spawn_search("a.t".into(), ctx));
@ -667,6 +660,5 @@ mod tests {
f.select_prev(&(), ctx);
assert_eq!(f.selected_index(), 0);
});
});
}
}

View file

@ -6,11 +6,11 @@ use crate::{
time::ReplicaId,
worktree::{FileHandle, Worktree, WorktreeHandle},
};
use futures_core::{future::LocalBoxFuture, Future};
use futures_core::Future;
use gpui::{
color::rgbu, elements::*, json::to_string_pretty, keymap::Binding, AnyViewHandle, AppContext,
ClipboardItem, Entity, EntityTask, ModelHandle, MutableAppContext, PathPromptOptions,
PromptLevel, View, ViewContext, ViewHandle, WeakModelHandle,
ClipboardItem, Entity, ModelHandle, MutableAppContext, PathPromptOptions, PromptLevel, Task,
View, ViewContext, ViewHandle, WeakModelHandle,
};
use log::error;
pub use pane::*;
@ -126,7 +126,7 @@ pub trait ItemView: View {
&mut self,
_: Option<FileHandle>,
_: &mut ViewContext<Self>,
) -> LocalBoxFuture<'static, anyhow::Result<()>>;
) -> Task<anyhow::Result<()>>;
fn should_activate_item_on_event(_: &Self::Event) -> bool {
false
}
@ -165,7 +165,7 @@ pub trait ItemViewHandle: Send + Sync {
&self,
file: Option<FileHandle>,
ctx: &mut MutableAppContext,
) -> LocalBoxFuture<'static, anyhow::Result<()>>;
) -> Task<anyhow::Result<()>>;
}
impl<T: Item> ItemHandle for ModelHandle<T> {
@ -243,7 +243,7 @@ impl<T: ItemView> ItemViewHandle for ViewHandle<T> {
&self,
file: Option<FileHandle>,
ctx: &mut MutableAppContext,
) -> LocalBoxFuture<'static, anyhow::Result<()>> {
) -> Task<anyhow::Result<()>> {
self.update(ctx, |item, ctx| item.save(file, ctx))
}
@ -367,16 +367,17 @@ impl Workspace {
.cloned()
.zip(entries.into_iter())
.map(|(abs_path, file)| {
ctx.spawn(
bg.spawn(async move { abs_path.is_file() }),
move |me, is_file, ctx| {
let is_file = bg.spawn(async move { abs_path.is_file() });
ctx.spawn(|this, mut ctx| async move {
let is_file = is_file.await;
this.update(&mut ctx, |this, ctx| {
if is_file {
me.open_entry(file.entry_id(), ctx)
this.open_entry(file.entry_id(), ctx)
} else {
None
}
},
)
})
})
})
.collect::<Vec<_>>();
async move {
@ -450,7 +451,7 @@ impl Workspace {
&mut self,
entry: (usize, Arc<Path>),
ctx: &mut ViewContext<Self>,
) -> Option<EntityTask<()>> {
) -> Option<Task<()>> {
// If the active pane contains a view for this file, then activate
// that item view.
if self
@ -504,44 +505,46 @@ impl Workspace {
let history = ctx
.background_executor()
.spawn(file.load_history(ctx.as_ref()));
ctx.spawn(history, move |_, history, ctx| {
*tx.borrow_mut() = Some(match history {
ctx.as_mut()
.spawn(|mut ctx| async move {
*tx.borrow_mut() = Some(match history.await {
Ok(history) => Ok(Box::new(ctx.add_model(|ctx| {
Buffer::from_history(replica_id, history, Some(file), ctx)
}))),
Err(error) => Err(Arc::new(error)),
})
})
.detach()
.detach();
}
let mut watch = self.loading_items.get(&entry).unwrap().clone();
Some(ctx.spawn(
async move {
loop {
Some(ctx.spawn(|this, mut ctx| async move {
let load_result = loop {
if let Some(load_result) = watch.borrow().as_ref() {
return load_result.clone();
break load_result.clone();
}
watch.next().await;
}
},
move |me, load_result, ctx| {
me.loading_items.remove(&entry);
};
this.update(&mut ctx, |this, ctx| {
this.loading_items.remove(&entry);
match load_result {
Ok(item) => {
let weak_item = item.downgrade();
let view = weak_item
.add_view(ctx.window_id(), settings, ctx.as_mut())
.unwrap();
me.items.push(weak_item);
me.add_item_view(view, ctx);
this.items.push(weak_item);
this.add_item_view(view, ctx);
}
Err(error) => {
log::error!("error opening item: {}", error);
}
}
},
))
})
}))
}
pub fn active_item(&self, ctx: &ViewContext<Self>) -> Option<Box<dyn ItemViewHandle>> {
@ -550,8 +553,8 @@ impl Workspace {
pub fn save_active_item(&mut self, _: &(), ctx: &mut ViewContext<Self>) {
if let Some(item) = self.active_item(ctx) {
if item.entry_id(ctx.as_ref()).is_none() {
let handle = ctx.handle();
if item.entry_id(ctx.as_ref()).is_none() {
let start_path = self
.worktrees
.iter()
@ -560,46 +563,39 @@ impl Workspace {
.to_path_buf();
ctx.prompt_for_new_path(&start_path, move |path, ctx| {
if let Some(path) = path {
handle.update(ctx, move |this, ctx| {
let file = this.file_for_path(&path, ctx);
let task = item.save(Some(file), ctx.as_mut());
ctx.spawn(task, move |_, result, _| {
if let Err(e) = result {
error!("failed to save item: {:?}, ", e);
ctx.spawn(|mut ctx| async move {
let file =
handle.update(&mut ctx, |me, ctx| me.file_for_path(&path, ctx));
if let Err(error) = ctx.update(|ctx| item.save(Some(file), ctx)).await {
error!("failed to save item: {:?}, ", error);
}
})
.detach()
})
}
});
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 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);
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 {
let task = item.save(None, ctx.as_mut());
ctx.spawn(task, |_, result, _| {
if let Err(e) = result {
error!("failed to save item: {:?}, ", e);
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();
@ -759,14 +755,12 @@ impl WorkspaceHandle for ViewHandle<Workspace> {
mod tests {
use super::*;
use crate::{editor::BufferView, settings, test::temp_tree};
use gpui::App;
use serde_json::json;
use std::{collections::HashSet, fs};
use tempdir::TempDir;
#[test]
fn test_open_paths_action() {
App::test((), |app| {
#[gpui::test]
fn test_open_paths_action(app: &mut gpui::MutableAppContext) {
let settings = settings::channel(&app.font_cache()).unwrap().1;
init(app);
@ -822,12 +816,10 @@ mod tests {
},
);
assert_eq!(app.window_ids().count(), 2);
});
}
#[test]
fn test_open_entry() {
App::test_async((), |mut app| async move {
#[gpui::test]
async fn test_open_entry(mut app: gpui::TestAppContext) {
let dir = temp_tree(json!({
"a": {
"file1": "contents 1",
@ -929,12 +921,10 @@ mod tests {
.collect::<Vec<_>>();
assert_eq!(pane_entries, &[file1, file2, file3.clone(), file3]);
});
});
}
#[test]
fn test_open_paths() {
App::test_async((), |mut app| async move {
#[gpui::test]
async fn test_open_paths(mut app: gpui::TestAppContext) {
let dir1 = temp_tree(json!({
"a.txt": "",
}));
@ -1002,12 +992,59 @@ mod tests {
"b.txt"
);
});
}
#[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));
});
}
#[test]
fn test_open_and_save_new_file() {
App::test_async((), |mut app| async move {
#[gpui::test]
async fn test_open_and_save_new_file(mut app: gpui::TestAppContext) {
let dir = TempDir::new("test-new-file").unwrap();
let settings = settings::channel(&app.font_cache()).unwrap().1;
let (_, workspace) = app.add_window(|ctx| {
@ -1098,65 +1135,10 @@ mod tests {
app.read(|ctx| {
assert_eq!(editor2.read(ctx).buffer(), editor.read(ctx).buffer());
})
});
}
#[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::<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));
});
});
}
#[test]
fn test_pane_actions() {
App::test_async((), |mut app| async move {
#[gpui::test]
async fn test_pane_actions(mut app: gpui::TestAppContext) {
app.update(|ctx| pane::init(ctx));
let dir = temp_tree(json!({
@ -1204,6 +1186,5 @@ mod tests {
assert_eq!(workspace_view.panes.len(), 1);
assert_eq!(workspace_view.active_pane(), &pane_1);
});
});
}
}

View file

@ -16,7 +16,7 @@ use postage::{
prelude::{Sink, Stream},
watch,
};
use smol::{channel::Sender, Timer};
use smol::channel::Sender;
use std::{
cmp,
collections::{HashMap, HashSet},
@ -99,7 +99,26 @@ impl Worktree {
scanner.run(event_stream)
});
ctx.spawn_stream(scan_state_rx, Self::observe_scan_state, |_, _| {})
ctx.spawn(|this, mut ctx| {
let this = this.downgrade();
async move {
while let Ok(scan_state) = scan_state_rx.recv().await {
let alive = ctx.update(|ctx| {
if let Some(handle) = this.upgrade(&ctx) {
handle
.update(ctx, |this, ctx| this.observe_scan_state(scan_state, ctx));
true
} else {
false
}
});
if !alive {
break;
}
}
}
})
.detach();
tree
@ -117,15 +136,16 @@ impl Worktree {
pub fn next_scan_complete(&self, ctx: &mut ModelContext<Self>) -> impl Future<Output = ()> {
let scan_id = self.snapshot.scan_id;
ctx.spawn_stream(
self.scan_state.1.clone(),
move |this, scan_state, ctx| {
if matches!(scan_state, ScanState::Idle) && this.snapshot.scan_id > scan_id {
ctx.halt_stream();
let mut scan_state = self.scan_state.1.clone();
ctx.spawn(|this, ctx| async move {
while let Some(scan_state) = scan_state.recv().await {
if this.read_with(&ctx, |this, _| {
matches!(scan_state, ScanState::Idle) && this.snapshot.scan_id > scan_id
}) {
break;
}
},
|_, _| {},
)
}
})
}
fn observe_scan_state(&mut self, scan_state: ScanState, ctx: &mut ModelContext<Self>) {
@ -138,10 +158,12 @@ impl Worktree {
ctx.notify();
if self.is_scanning() && !self.poll_scheduled {
ctx.spawn(Timer::after(Duration::from_millis(100)), |this, _, ctx| {
ctx.spawn(|this, mut ctx| async move {
this.update(&mut ctx, |this, ctx| {
this.poll_scheduled = false;
this.poll_entries(ctx);
})
})
.detach();
self.poll_scheduled = true;
}
@ -1394,7 +1416,6 @@ mod tests {
use crate::editor::Buffer;
use crate::test::*;
use anyhow::Result;
use gpui::App;
use rand::prelude::*;
use serde_json::json;
use std::env;
@ -1402,9 +1423,8 @@ mod tests {
use std::os::unix;
use std::time::{SystemTime, UNIX_EPOCH};
#[test]
fn test_populate_and_search() {
App::test_async((), |mut app| async move {
#[gpui::test]
async fn test_populate_and_search(mut app: gpui::TestAppContext) {
let dir = temp_tree(json!({
"root": {
"apple": "",
@ -1461,12 +1481,10 @@ mod tests {
]
);
})
});
}
#[test]
fn test_save_file() {
App::test_async((), |mut app| async move {
#[gpui::test]
async fn test_save_file(mut app: gpui::TestAppContext) {
let dir = temp_tree(json!({
"file1": "the old contents",
}));
@ -1481,8 +1499,7 @@ mod tests {
let path = tree.update(&mut app, |tree, ctx| {
let path = tree.files(0).next().unwrap().path().clone();
assert_eq!(path.file_name().unwrap(), "file1");
smol::block_on(tree.save(&path, buffer.read(ctx).snapshot(), ctx.as_ref()))
.unwrap();
smol::block_on(tree.save(&path, buffer.read(ctx).snapshot(), ctx.as_ref())).unwrap();
path
});
@ -1493,12 +1510,10 @@ mod tests {
app.read(|ctx| {
assert_eq!(history.base_text.as_ref(), buffer.read(ctx).text());
});
});
}
#[test]
fn test_save_in_single_file_worktree() {
App::test_async((), |mut app| async move {
#[gpui::test]
async fn test_save_in_single_file_worktree(mut app: gpui::TestAppContext) {
let dir = temp_tree(json!({
"file1": "the old contents",
}));
@ -1518,12 +1533,10 @@ mod tests {
let history = app.read(|ctx| file.load_history(ctx)).await.unwrap();
app.read(|ctx| assert_eq!(history.base_text.as_ref(), buffer.read(ctx).text()));
});
}
#[test]
fn test_rescan_simple() {
App::test_async((), |mut app| async move {
#[gpui::test]
async fn test_rescan_simple(mut app: gpui::TestAppContext) {
let dir = temp_tree(json!({
"a": {
"file1": "",
@ -1566,10 +1579,10 @@ mod tests {
assert!(non_existent_file.is_deleted());
tree.flush_fs_events(&app).await;
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();
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();
tree.update(&mut app, |tree, ctx| tree.next_scan_complete(ctx))
.await;
@ -1603,12 +1616,10 @@ mod tests {
assert_eq!(file3.path().as_ref(), Path::new("a/file3"));
assert!(file3.is_deleted());
});
});
}
#[test]
fn test_rescan_with_gitignore() {
App::test_async((), |mut app| async move {
#[gpui::test]
async fn test_rescan_with_gitignore(mut app: gpui::TestAppContext) {
let dir = temp_tree(json!({
".git": {},
".gitignore": "ignored-dir\n",
@ -1644,7 +1655,6 @@ mod tests {
assert_eq!(ignored.is_ignored(), true);
assert_eq!(dot_git.is_ignored(), true);
});
});
}
#[test]