Merge pull request #55 from zed-industries/test-macros
Introduce a `gpui::test` macro for less ceremony when writing tests
This commit is contained in:
Generated
+9
@@ -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"
|
||||
|
||||
+1
-1
@@ -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"}
|
||||
|
||||
@@ -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"
|
||||
|
||||
+397
-443
File diff suppressed because it is too large
Load Diff
@@ -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};
|
||||
pub use presenter::{
|
||||
AfterLayoutContext, Axis, DebugContext, EventContext, LayoutContext, PaintContext,
|
||||
|
||||
@@ -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"
|
||||
@@ -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))
|
||||
}
|
||||
+520
-545
File diff suppressed because it is too large
Load Diff
+1206
-1279
File diff suppressed because it is too large
Load Diff
@@ -671,176 +671,163 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::test::sample_text;
|
||||
use buffer::ToPoint;
|
||||
use gpui::App;
|
||||
|
||||
#[test]
|
||||
fn test_basic_folds() {
|
||||
App::test((), |app| {
|
||||
let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(5, 6), ctx));
|
||||
let mut map = FoldMap::new(buffer.clone(), app.as_ref());
|
||||
#[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());
|
||||
|
||||
map.fold(
|
||||
vec![
|
||||
Point::new(0, 2)..Point::new(2, 2),
|
||||
Point::new(2, 4)..Point::new(4, 1),
|
||||
],
|
||||
app.as_ref(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(map.text(app.as_ref()), "aa…cc…eeeee");
|
||||
map.fold(
|
||||
vec![
|
||||
Point::new(0, 2)..Point::new(2, 2),
|
||||
Point::new(2, 4)..Point::new(4, 1),
|
||||
],
|
||||
app.as_ref(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(map.text(app.as_ref()), "aa…cc…eeeee");
|
||||
|
||||
buffer.update(app, |buffer, ctx| {
|
||||
buffer
|
||||
.edit(
|
||||
vec![
|
||||
Point::new(0, 0)..Point::new(0, 1),
|
||||
Point::new(2, 3)..Point::new(2, 3),
|
||||
],
|
||||
"123",
|
||||
Some(ctx),
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
assert_eq!(map.text(app.as_ref()), "123a…c123c…eeeee");
|
||||
|
||||
buffer.update(app, |buffer, ctx| {
|
||||
let start_version = buffer.version.clone();
|
||||
buffer
|
||||
.edit(Some(Point::new(2, 6)..Point::new(4, 3)), "456", Some(ctx))
|
||||
.unwrap();
|
||||
buffer.edits_since(start_version).collect::<Vec<_>>()
|
||||
});
|
||||
assert_eq!(map.text(app.as_ref()), "123a…c123456eee");
|
||||
|
||||
map.unfold(Some(Point::new(0, 4)..Point::new(0, 5)), app.as_ref())
|
||||
buffer.update(app, |buffer, ctx| {
|
||||
buffer
|
||||
.edit(
|
||||
vec![
|
||||
Point::new(0, 0)..Point::new(0, 1),
|
||||
Point::new(2, 3)..Point::new(2, 3),
|
||||
],
|
||||
"123",
|
||||
Some(ctx),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(map.text(app.as_ref()), "123aaaaa\nbbbbbb\nccc123456eee");
|
||||
});
|
||||
}
|
||||
assert_eq!(map.text(app.as_ref()), "123a…c123c…eeeee");
|
||||
|
||||
#[test]
|
||||
fn test_adjacent_folds() {
|
||||
App::test((), |app| {
|
||||
let buffer = app.add_model(|ctx| Buffer::new(0, "abcdefghijkl", ctx));
|
||||
|
||||
{
|
||||
let mut map = FoldMap::new(buffer.clone(), app.as_ref());
|
||||
|
||||
map.fold(vec![5..8], app.as_ref()).unwrap();
|
||||
map.check_invariants(app.as_ref());
|
||||
assert_eq!(map.text(app.as_ref()), "abcde…ijkl");
|
||||
|
||||
// Create an fold adjacent to the start of the first fold.
|
||||
map.fold(vec![0..1, 2..5], app.as_ref()).unwrap();
|
||||
map.check_invariants(app.as_ref());
|
||||
assert_eq!(map.text(app.as_ref()), "…b…ijkl");
|
||||
|
||||
// Create an fold adjacent to the end of the first fold.
|
||||
map.fold(vec![11..11, 8..10], app.as_ref()).unwrap();
|
||||
map.check_invariants(app.as_ref());
|
||||
assert_eq!(map.text(app.as_ref()), "…b…kl");
|
||||
}
|
||||
|
||||
{
|
||||
let mut map = FoldMap::new(buffer.clone(), app.as_ref());
|
||||
|
||||
// Create two adjacent folds.
|
||||
map.fold(vec![0..2, 2..5], app.as_ref()).unwrap();
|
||||
map.check_invariants(app.as_ref());
|
||||
assert_eq!(map.text(app.as_ref()), "…fghijkl");
|
||||
|
||||
// Edit within one of the folds.
|
||||
buffer.update(app, |buffer, ctx| {
|
||||
let version = buffer.version();
|
||||
buffer.edit(vec![0..1], "12345", Some(ctx)).unwrap();
|
||||
buffer.edits_since(version).collect::<Vec<_>>()
|
||||
});
|
||||
map.check_invariants(app.as_ref());
|
||||
assert_eq!(map.text(app.as_ref()), "12345…fghijkl");
|
||||
}
|
||||
buffer.update(app, |buffer, ctx| {
|
||||
let start_version = buffer.version.clone();
|
||||
buffer
|
||||
.edit(Some(Point::new(2, 6)..Point::new(4, 3)), "456", Some(ctx))
|
||||
.unwrap();
|
||||
buffer.edits_since(start_version).collect::<Vec<_>>()
|
||||
});
|
||||
}
|
||||
assert_eq!(map.text(app.as_ref()), "123a…c123456eee");
|
||||
|
||||
#[test]
|
||||
fn test_overlapping_folds() {
|
||||
App::test((), |app| {
|
||||
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(
|
||||
vec![
|
||||
Point::new(0, 2)..Point::new(2, 2),
|
||||
Point::new(0, 4)..Point::new(1, 0),
|
||||
Point::new(1, 2)..Point::new(3, 2),
|
||||
Point::new(3, 1)..Point::new(4, 1),
|
||||
],
|
||||
app.as_ref(),
|
||||
)
|
||||
map.unfold(Some(Point::new(0, 4)..Point::new(0, 5)), app.as_ref())
|
||||
.unwrap();
|
||||
assert_eq!(map.text(app.as_ref()), "aa…eeeee");
|
||||
})
|
||||
assert_eq!(map.text(app.as_ref()), "123aaaaa\nbbbbbb\nccc123456eee");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_merging_folds_via_edit() {
|
||||
App::test((), |app| {
|
||||
let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(5, 6), ctx));
|
||||
#[gpui::test]
|
||||
fn test_adjacent_folds(app: &mut gpui::MutableAppContext) {
|
||||
let buffer = app.add_model(|ctx| Buffer::new(0, "abcdefghijkl", ctx));
|
||||
|
||||
{
|
||||
let mut map = FoldMap::new(buffer.clone(), app.as_ref());
|
||||
|
||||
map.fold(
|
||||
vec![
|
||||
Point::new(0, 2)..Point::new(2, 2),
|
||||
Point::new(3, 1)..Point::new(4, 1),
|
||||
],
|
||||
app.as_ref(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(map.text(app.as_ref()), "aa…cccc\nd…eeeee");
|
||||
map.fold(vec![5..8], app.as_ref()).unwrap();
|
||||
map.check_invariants(app.as_ref());
|
||||
assert_eq!(map.text(app.as_ref()), "abcde…ijkl");
|
||||
|
||||
// Create an fold adjacent to the start of the first fold.
|
||||
map.fold(vec![0..1, 2..5], app.as_ref()).unwrap();
|
||||
map.check_invariants(app.as_ref());
|
||||
assert_eq!(map.text(app.as_ref()), "…b…ijkl");
|
||||
|
||||
// Create an fold adjacent to the end of the first fold.
|
||||
map.fold(vec![11..11, 8..10], app.as_ref()).unwrap();
|
||||
map.check_invariants(app.as_ref());
|
||||
assert_eq!(map.text(app.as_ref()), "…b…kl");
|
||||
}
|
||||
|
||||
{
|
||||
let mut map = FoldMap::new(buffer.clone(), app.as_ref());
|
||||
|
||||
// Create two adjacent folds.
|
||||
map.fold(vec![0..2, 2..5], app.as_ref()).unwrap();
|
||||
map.check_invariants(app.as_ref());
|
||||
assert_eq!(map.text(app.as_ref()), "…fghijkl");
|
||||
|
||||
// Edit within one of the folds.
|
||||
buffer.update(app, |buffer, ctx| {
|
||||
buffer
|
||||
.edit(Some(Point::new(2, 2)..Point::new(3, 1)), "", Some(ctx))
|
||||
.unwrap();
|
||||
let version = buffer.version();
|
||||
buffer.edit(vec![0..1], "12345", Some(ctx)).unwrap();
|
||||
buffer.edits_since(version).collect::<Vec<_>>()
|
||||
});
|
||||
assert_eq!(map.text(app.as_ref()), "aa…eeeee");
|
||||
});
|
||||
map.check_invariants(app.as_ref());
|
||||
assert_eq!(map.text(app.as_ref()), "12345…fghijkl");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_folds_in_range() {
|
||||
App::test((), |app| {
|
||||
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);
|
||||
|
||||
map.fold(
|
||||
vec![
|
||||
Point::new(0, 2)..Point::new(2, 2),
|
||||
Point::new(0, 4)..Point::new(1, 0),
|
||||
Point::new(1, 2)..Point::new(3, 2),
|
||||
Point::new(3, 1)..Point::new(4, 1),
|
||||
],
|
||||
app.as_ref(),
|
||||
)
|
||||
.unwrap();
|
||||
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()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
fold_ranges,
|
||||
vec![
|
||||
Point::new(0, 2)..Point::new(2, 2),
|
||||
Point::new(1, 2)..Point::new(3, 2)
|
||||
]
|
||||
);
|
||||
});
|
||||
#[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(
|
||||
vec![
|
||||
Point::new(0, 2)..Point::new(2, 2),
|
||||
Point::new(0, 4)..Point::new(1, 0),
|
||||
Point::new(1, 2)..Point::new(3, 2),
|
||||
Point::new(3, 1)..Point::new(4, 1),
|
||||
],
|
||||
app.as_ref(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(map.text(app.as_ref()), "aa…eeeee");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_random_folds() {
|
||||
#[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());
|
||||
|
||||
map.fold(
|
||||
vec![
|
||||
Point::new(0, 2)..Point::new(2, 2),
|
||||
Point::new(3, 1)..Point::new(4, 1),
|
||||
],
|
||||
app.as_ref(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(map.text(app.as_ref()), "aa…cccc\nd…eeeee");
|
||||
|
||||
buffer.update(app, |buffer, ctx| {
|
||||
buffer
|
||||
.edit(Some(Point::new(2, 2)..Point::new(3, 1)), "", Some(ctx))
|
||||
.unwrap();
|
||||
});
|
||||
assert_eq!(map.text(app.as_ref()), "aa…eeeee");
|
||||
}
|
||||
|
||||
#[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);
|
||||
|
||||
map.fold(
|
||||
vec![
|
||||
Point::new(0, 2)..Point::new(2, 2),
|
||||
Point::new(0, 4)..Point::new(1, 0),
|
||||
Point::new(1, 2)..Point::new(3, 2),
|
||||
Point::new(3, 1)..Point::new(4, 1),
|
||||
],
|
||||
app.as_ref(),
|
||||
)
|
||||
.unwrap();
|
||||
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())
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
fold_ranges,
|
||||
vec![
|
||||
Point::new(0, 2)..Point::new(2, 2),
|
||||
Point::new(1, 2)..Point::new(3, 2)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_random_folds(app: &mut gpui::MutableAppContext) {
|
||||
use crate::editor::ToPoint;
|
||||
use crate::util::RandomCharIter;
|
||||
use rand::prelude::*;
|
||||
@@ -863,203 +850,197 @@ 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>();
|
||||
Buffer::new(0, text, ctx)
|
||||
});
|
||||
let mut map = FoldMap::new(buffer.clone(), app.as_ref());
|
||||
let buffer = app.add_model(|ctx| {
|
||||
let len = rng.gen_range(0..10);
|
||||
let text = RandomCharIter::new(&mut rng).take(len).collect::<String>();
|
||||
Buffer::new(0, text, ctx)
|
||||
});
|
||||
let mut map = FoldMap::new(buffer.clone(), app.as_ref());
|
||||
|
||||
for _ in 0..operations {
|
||||
log::info!("text: {:?}", buffer.read(app).text());
|
||||
match rng.gen_range(0..=100) {
|
||||
0..=34 => {
|
||||
let buffer = buffer.read(app);
|
||||
let mut to_fold = Vec::new();
|
||||
for _ in 0..rng.gen_range(1..=5) {
|
||||
let end = rng.gen_range(0..=buffer.len());
|
||||
let start = rng.gen_range(0..=end);
|
||||
to_fold.push(start..end);
|
||||
}
|
||||
log::info!("folding {:?}", to_fold);
|
||||
map.fold(to_fold, app.as_ref()).unwrap();
|
||||
for _ in 0..operations {
|
||||
log::info!("text: {:?}", buffer.read(app).text());
|
||||
match rng.gen_range(0..=100) {
|
||||
0..=34 => {
|
||||
let buffer = buffer.read(app);
|
||||
let mut to_fold = Vec::new();
|
||||
for _ in 0..rng.gen_range(1..=5) {
|
||||
let end = rng.gen_range(0..=buffer.len());
|
||||
let start = rng.gen_range(0..=end);
|
||||
to_fold.push(start..end);
|
||||
}
|
||||
35..=59 if !map.folds.is_empty() => {
|
||||
let buffer = buffer.read(app);
|
||||
let mut to_unfold = Vec::new();
|
||||
for _ in 0..rng.gen_range(1..=3) {
|
||||
let end = rng.gen_range(0..=buffer.len());
|
||||
let start = rng.gen_range(0..=end);
|
||||
to_unfold.push(start..end);
|
||||
}
|
||||
log::info!("unfolding {:?}", to_unfold);
|
||||
map.unfold(to_unfold, app.as_ref()).unwrap();
|
||||
}
|
||||
_ => {
|
||||
let edits = buffer.update(app, |buffer, ctx| {
|
||||
let start_version = buffer.version.clone();
|
||||
let edit_count = rng.gen_range(1..=5);
|
||||
buffer.randomly_edit(&mut rng, edit_count, Some(ctx));
|
||||
buffer.edits_since(start_version).collect::<Vec<_>>()
|
||||
});
|
||||
log::info!("editing {:?}", edits);
|
||||
log::info!("folding {:?}", to_fold);
|
||||
map.fold(to_fold, app.as_ref()).unwrap();
|
||||
}
|
||||
35..=59 if !map.folds.is_empty() => {
|
||||
let buffer = buffer.read(app);
|
||||
let mut to_unfold = Vec::new();
|
||||
for _ in 0..rng.gen_range(1..=3) {
|
||||
let end = rng.gen_range(0..=buffer.len());
|
||||
let start = rng.gen_range(0..=end);
|
||||
to_unfold.push(start..end);
|
||||
}
|
||||
log::info!("unfolding {:?}", to_unfold);
|
||||
map.unfold(to_unfold, app.as_ref()).unwrap();
|
||||
}
|
||||
map.check_invariants(app.as_ref());
|
||||
|
||||
let buffer = map.buffer.read(app);
|
||||
let mut expected_text = buffer.text();
|
||||
let mut expected_buffer_rows = Vec::new();
|
||||
let mut next_row = buffer.max_point().row;
|
||||
for fold_range in map.merged_fold_ranges(app.as_ref()).into_iter().rev() {
|
||||
let fold_start = buffer.point_for_offset(fold_range.start).unwrap();
|
||||
let fold_end = buffer.point_for_offset(fold_range.end).unwrap();
|
||||
expected_buffer_rows.extend((fold_end.row + 1..=next_row).rev());
|
||||
next_row = fold_start.row;
|
||||
|
||||
expected_text.replace_range(fold_range.start..fold_range.end, "…");
|
||||
}
|
||||
expected_buffer_rows.extend((0..=next_row).rev());
|
||||
expected_buffer_rows.reverse();
|
||||
|
||||
assert_eq!(map.text(app.as_ref()), expected_text);
|
||||
|
||||
for (display_row, line) in expected_text.lines().enumerate() {
|
||||
let line_len = map.line_len(display_row as u32, app.as_ref()).unwrap();
|
||||
assert_eq!(line_len, line.chars().count() as u32);
|
||||
}
|
||||
|
||||
let mut display_point = DisplayPoint::new(0, 0);
|
||||
let mut display_offset = DisplayOffset(0);
|
||||
for c in expected_text.chars() {
|
||||
let buffer_point = map.to_buffer_point(display_point, app.as_ref());
|
||||
let buffer_offset = buffer_point.to_offset(buffer).unwrap();
|
||||
assert_eq!(
|
||||
map.to_display_point(buffer_point, app.as_ref()),
|
||||
display_point
|
||||
);
|
||||
assert_eq!(
|
||||
map.to_buffer_offset(display_point, app.as_ref()).unwrap(),
|
||||
buffer_offset
|
||||
);
|
||||
assert_eq!(
|
||||
map.to_display_offset(display_point, app.as_ref()).unwrap(),
|
||||
display_offset
|
||||
);
|
||||
|
||||
if c == '\n' {
|
||||
*display_point.row_mut() += 1;
|
||||
*display_point.column_mut() = 0;
|
||||
} else {
|
||||
*display_point.column_mut() += 1;
|
||||
}
|
||||
display_offset.0 += 1;
|
||||
}
|
||||
|
||||
for _ in 0..5 {
|
||||
let row = rng.gen_range(0..=map.max_point(app.as_ref()).row());
|
||||
let column = rng.gen_range(0..=map.line_len(row, app.as_ref()).unwrap());
|
||||
let point = DisplayPoint::new(row, column);
|
||||
let offset = map.to_display_offset(point, app.as_ref()).unwrap().0;
|
||||
let len = rng.gen_range(0..=map.len(app.as_ref()) - offset);
|
||||
assert_eq!(
|
||||
map.snapshot(app.as_ref())
|
||||
.chars_at(point, app.as_ref())
|
||||
.unwrap()
|
||||
.take(len)
|
||||
.collect::<String>(),
|
||||
expected_text
|
||||
.chars()
|
||||
.skip(offset)
|
||||
.take(len)
|
||||
.collect::<String>()
|
||||
);
|
||||
}
|
||||
|
||||
for (idx, buffer_row) in expected_buffer_rows.iter().enumerate() {
|
||||
let display_row = map
|
||||
.to_display_point(Point::new(*buffer_row, 0), app.as_ref())
|
||||
.row();
|
||||
assert_eq!(
|
||||
map.snapshot(app.as_ref())
|
||||
.buffer_rows(display_row)
|
||||
.unwrap()
|
||||
.collect::<Vec<_>>(),
|
||||
expected_buffer_rows[idx..],
|
||||
);
|
||||
}
|
||||
|
||||
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(),
|
||||
);
|
||||
assert!(map.is_line_folded(display_point.row(), app.as_ref()));
|
||||
}
|
||||
|
||||
for _ in 0..5 {
|
||||
let end = rng.gen_range(0..=buffer.len());
|
||||
let start = rng.gen_range(0..=end);
|
||||
let expected_folds = map
|
||||
.folds
|
||||
.items()
|
||||
.into_iter()
|
||||
.filter(|fold| {
|
||||
let start = buffer.anchor_before(start).unwrap();
|
||||
let end = buffer.anchor_after(end).unwrap();
|
||||
start.cmp(&fold.0.end, buffer).unwrap() == Ordering::Less
|
||||
&& end.cmp(&fold.0.start, buffer).unwrap() == Ordering::Greater
|
||||
})
|
||||
.map(|fold| fold.0)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(
|
||||
map.folds_in_range(start..end, app.as_ref())
|
||||
.unwrap()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>(),
|
||||
expected_folds
|
||||
);
|
||||
_ => {
|
||||
let edits = buffer.update(app, |buffer, ctx| {
|
||||
let start_version = buffer.version.clone();
|
||||
let edit_count = rng.gen_range(1..=5);
|
||||
buffer.randomly_edit(&mut rng, edit_count, Some(ctx));
|
||||
buffer.edits_since(start_version).collect::<Vec<_>>()
|
||||
});
|
||||
log::info!("editing {:?}", edits);
|
||||
}
|
||||
}
|
||||
});
|
||||
map.check_invariants(app.as_ref());
|
||||
|
||||
let buffer = map.buffer.read(app);
|
||||
let mut expected_text = buffer.text();
|
||||
let mut expected_buffer_rows = Vec::new();
|
||||
let mut next_row = buffer.max_point().row;
|
||||
for fold_range in map.merged_fold_ranges(app.as_ref()).into_iter().rev() {
|
||||
let fold_start = buffer.point_for_offset(fold_range.start).unwrap();
|
||||
let fold_end = buffer.point_for_offset(fold_range.end).unwrap();
|
||||
expected_buffer_rows.extend((fold_end.row + 1..=next_row).rev());
|
||||
next_row = fold_start.row;
|
||||
|
||||
expected_text.replace_range(fold_range.start..fold_range.end, "…");
|
||||
}
|
||||
expected_buffer_rows.extend((0..=next_row).rev());
|
||||
expected_buffer_rows.reverse();
|
||||
|
||||
assert_eq!(map.text(app.as_ref()), expected_text);
|
||||
|
||||
for (display_row, line) in expected_text.lines().enumerate() {
|
||||
let line_len = map.line_len(display_row as u32, app.as_ref()).unwrap();
|
||||
assert_eq!(line_len, line.chars().count() as u32);
|
||||
}
|
||||
|
||||
let mut display_point = DisplayPoint::new(0, 0);
|
||||
let mut display_offset = DisplayOffset(0);
|
||||
for c in expected_text.chars() {
|
||||
let buffer_point = map.to_buffer_point(display_point, app.as_ref());
|
||||
let buffer_offset = buffer_point.to_offset(buffer).unwrap();
|
||||
assert_eq!(
|
||||
map.to_display_point(buffer_point, app.as_ref()),
|
||||
display_point
|
||||
);
|
||||
assert_eq!(
|
||||
map.to_buffer_offset(display_point, app.as_ref()).unwrap(),
|
||||
buffer_offset
|
||||
);
|
||||
assert_eq!(
|
||||
map.to_display_offset(display_point, app.as_ref()).unwrap(),
|
||||
display_offset
|
||||
);
|
||||
|
||||
if c == '\n' {
|
||||
*display_point.row_mut() += 1;
|
||||
*display_point.column_mut() = 0;
|
||||
} else {
|
||||
*display_point.column_mut() += 1;
|
||||
}
|
||||
display_offset.0 += 1;
|
||||
}
|
||||
|
||||
for _ in 0..5 {
|
||||
let row = rng.gen_range(0..=map.max_point(app.as_ref()).row());
|
||||
let column = rng.gen_range(0..=map.line_len(row, app.as_ref()).unwrap());
|
||||
let point = DisplayPoint::new(row, column);
|
||||
let offset = map.to_display_offset(point, app.as_ref()).unwrap().0;
|
||||
let len = rng.gen_range(0..=map.len(app.as_ref()) - offset);
|
||||
assert_eq!(
|
||||
map.snapshot(app.as_ref())
|
||||
.chars_at(point, app.as_ref())
|
||||
.unwrap()
|
||||
.take(len)
|
||||
.collect::<String>(),
|
||||
expected_text
|
||||
.chars()
|
||||
.skip(offset)
|
||||
.take(len)
|
||||
.collect::<String>()
|
||||
);
|
||||
}
|
||||
|
||||
for (idx, buffer_row) in expected_buffer_rows.iter().enumerate() {
|
||||
let display_row = map
|
||||
.to_display_point(Point::new(*buffer_row, 0), app.as_ref())
|
||||
.row();
|
||||
assert_eq!(
|
||||
map.snapshot(app.as_ref())
|
||||
.buffer_rows(display_row)
|
||||
.unwrap()
|
||||
.collect::<Vec<_>>(),
|
||||
expected_buffer_rows[idx..],
|
||||
);
|
||||
}
|
||||
|
||||
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());
|
||||
assert!(map.is_line_folded(display_point.row(), app.as_ref()));
|
||||
}
|
||||
|
||||
for _ in 0..5 {
|
||||
let end = rng.gen_range(0..=buffer.len());
|
||||
let start = rng.gen_range(0..=end);
|
||||
let expected_folds = map
|
||||
.folds
|
||||
.items()
|
||||
.into_iter()
|
||||
.filter(|fold| {
|
||||
let start = buffer.anchor_before(start).unwrap();
|
||||
let end = buffer.anchor_after(end).unwrap();
|
||||
start.cmp(&fold.0.end, buffer).unwrap() == Ordering::Less
|
||||
&& end.cmp(&fold.0.start, buffer).unwrap() == Ordering::Greater
|
||||
})
|
||||
.map(|fold| fold.0)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(
|
||||
map.folds_in_range(start..end, app.as_ref())
|
||||
.unwrap()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>(),
|
||||
expected_folds
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_buffer_rows() {
|
||||
App::test((), |app| {
|
||||
let text = sample_text(6, 6) + "\n";
|
||||
let buffer = app.add_model(|ctx| Buffer::new(0, text, ctx));
|
||||
#[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));
|
||||
|
||||
let mut map = FoldMap::new(buffer.clone(), app.as_ref());
|
||||
let mut map = FoldMap::new(buffer.clone(), app.as_ref());
|
||||
|
||||
map.fold(
|
||||
vec![
|
||||
Point::new(0, 2)..Point::new(2, 2),
|
||||
Point::new(3, 1)..Point::new(4, 1),
|
||||
],
|
||||
app.as_ref(),
|
||||
)
|
||||
.unwrap();
|
||||
map.fold(
|
||||
vec![
|
||||
Point::new(0, 2)..Point::new(2, 2),
|
||||
Point::new(3, 1)..Point::new(4, 1),
|
||||
],
|
||||
app.as_ref(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(map.text(app.as_ref()), "aa…cccc\nd…eeeee\nffffff\n");
|
||||
assert_eq!(
|
||||
map.snapshot(app.as_ref())
|
||||
.buffer_rows(0)
|
||||
.unwrap()
|
||||
.collect::<Vec<_>>(),
|
||||
vec![0, 3, 5, 6]
|
||||
);
|
||||
assert_eq!(
|
||||
map.snapshot(app.as_ref())
|
||||
.buffer_rows(3)
|
||||
.unwrap()
|
||||
.collect::<Vec<_>>(),
|
||||
vec![6]
|
||||
);
|
||||
});
|
||||
assert_eq!(map.text(app.as_ref()), "aa…cccc\nd…eeeee\nffffff\n");
|
||||
assert_eq!(
|
||||
map.snapshot(app.as_ref())
|
||||
.buffer_rows(0)
|
||||
.unwrap()
|
||||
.collect::<Vec<_>>(),
|
||||
vec![0, 3, 5, 6]
|
||||
);
|
||||
assert_eq!(
|
||||
map.snapshot(app.as_ref())
|
||||
.buffer_rows(3)
|
||||
.unwrap()
|
||||
.collect::<Vec<_>>(),
|
||||
vec![6]
|
||||
);
|
||||
}
|
||||
|
||||
impl FoldMap {
|
||||
|
||||
@@ -339,53 +339,50 @@ pub fn collapse_tabs(
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test::*;
|
||||
use gpui::App;
|
||||
|
||||
#[test]
|
||||
fn test_chars_at() {
|
||||
App::test((), |app| {
|
||||
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());
|
||||
buffer
|
||||
.update(app, |buffer, ctx| {
|
||||
buffer.edit(
|
||||
vec![
|
||||
Point::new(1, 0)..Point::new(1, 0),
|
||||
Point::new(1, 1)..Point::new(1, 1),
|
||||
Point::new(2, 1)..Point::new(2, 1),
|
||||
],
|
||||
"\t",
|
||||
Some(ctx),
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
#[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());
|
||||
buffer
|
||||
.update(app, |buffer, ctx| {
|
||||
buffer.edit(
|
||||
vec![
|
||||
Point::new(1, 0)..Point::new(1, 0),
|
||||
Point::new(1, 1)..Point::new(1, 1),
|
||||
Point::new(2, 1)..Point::new(2, 1),
|
||||
],
|
||||
"\t",
|
||||
Some(ctx),
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
map.snapshot(app.as_ref())
|
||||
.chars_at(DisplayPoint::new(1, 0), app.as_ref())
|
||||
.unwrap()
|
||||
.take(10)
|
||||
.collect::<String>(),
|
||||
" b bb"
|
||||
);
|
||||
assert_eq!(
|
||||
map.snapshot(app.as_ref())
|
||||
.chars_at(DisplayPoint::new(1, 2), app.as_ref())
|
||||
.unwrap()
|
||||
.take(10)
|
||||
.collect::<String>(),
|
||||
" b bbbb"
|
||||
);
|
||||
assert_eq!(
|
||||
map.snapshot(app.as_ref())
|
||||
.chars_at(DisplayPoint::new(1, 6), app.as_ref())
|
||||
.unwrap()
|
||||
.take(13)
|
||||
.collect::<String>(),
|
||||
" bbbbb\nc c"
|
||||
);
|
||||
});
|
||||
assert_eq!(
|
||||
map.snapshot(app.as_ref())
|
||||
.chars_at(DisplayPoint::new(1, 0), app.as_ref())
|
||||
.unwrap()
|
||||
.take(10)
|
||||
.collect::<String>(),
|
||||
" b bb"
|
||||
);
|
||||
assert_eq!(
|
||||
map.snapshot(app.as_ref())
|
||||
.chars_at(DisplayPoint::new(1, 2), app.as_ref())
|
||||
.unwrap()
|
||||
.take(10)
|
||||
.collect::<String>(),
|
||||
" b bbbb"
|
||||
);
|
||||
assert_eq!(
|
||||
map.snapshot(app.as_ref())
|
||||
.chars_at(DisplayPoint::new(1, 6), app.as_ref())
|
||||
.unwrap()
|
||||
.take(13)
|
||||
.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| {
|
||||
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))
|
||||
});
|
||||
#[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))
|
||||
}
|
||||
}
|
||||
|
||||
+192
-204
@@ -453,220 +453,208 @@ 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 {
|
||||
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();
|
||||
fs::write(tmp_dir.path().join("a/bandana"), "bandana").unwrap();
|
||||
app.update(|ctx| {
|
||||
super::init(ctx);
|
||||
editor::init(ctx);
|
||||
});
|
||||
|
||||
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(tmp_dir.path(), ctx);
|
||||
workspace
|
||||
});
|
||||
app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx))
|
||||
.await;
|
||||
app.dispatch_action(
|
||||
window_id,
|
||||
vec![workspace.id()],
|
||||
"file_finder:toggle".into(),
|
||||
(),
|
||||
);
|
||||
|
||||
let finder = app.read(|ctx| {
|
||||
workspace
|
||||
.read(ctx)
|
||||
.modal()
|
||||
.cloned()
|
||||
.unwrap()
|
||||
.downcast::<FileFinder>()
|
||||
.unwrap()
|
||||
});
|
||||
let query_buffer = app.read(|ctx| finder.read(ctx).query_buffer.clone());
|
||||
|
||||
let chain = vec![finder.id(), query_buffer.id()];
|
||||
app.dispatch_action(window_id, chain.clone(), "buffer:insert", "b".to_string());
|
||||
app.dispatch_action(window_id, chain.clone(), "buffer:insert", "n".to_string());
|
||||
app.dispatch_action(window_id, chain.clone(), "buffer:insert", "a".to_string());
|
||||
finder
|
||||
.condition(&app, |finder, _| finder.matches.len() == 2)
|
||||
.await;
|
||||
|
||||
let active_pane = app.read(|ctx| workspace.read(ctx).active_pane().clone());
|
||||
app.dispatch_action(
|
||||
window_id,
|
||||
vec![workspace.id(), finder.id()],
|
||||
"menu:select_next",
|
||||
(),
|
||||
);
|
||||
app.dispatch_action(
|
||||
window_id,
|
||||
vec![workspace.id(), finder.id()],
|
||||
"file_finder:confirm",
|
||||
(),
|
||||
);
|
||||
active_pane
|
||||
.condition(&app, |pane, _| pane.active_item().is_some())
|
||||
.await;
|
||||
app.read(|ctx| {
|
||||
let active_item = active_pane.read(ctx).active_item().unwrap();
|
||||
assert_eq!(active_item.title(ctx), "bandana");
|
||||
});
|
||||
#[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();
|
||||
fs::write(tmp_dir.path().join("a/bandana"), "bandana").unwrap();
|
||||
app.update(|ctx| {
|
||||
super::init(ctx);
|
||||
editor::init(ctx);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_matching_cancellation() {
|
||||
App::test_async((), |mut app| async move {
|
||||
let tmp_dir = temp_tree(json!({
|
||||
"hello": "",
|
||||
"goodbye": "",
|
||||
"halogen-light": "",
|
||||
"happiness": "",
|
||||
"height": "",
|
||||
"hi": "",
|
||||
"hiccup": "",
|
||||
}));
|
||||
let settings = settings::channel(&app.font_cache()).unwrap().1;
|
||||
let (_, workspace) = app.add_window(|ctx| {
|
||||
let mut workspace = Workspace::new(0, settings.clone(), ctx);
|
||||
workspace.add_worktree(tmp_dir.path(), ctx);
|
||||
workspace
|
||||
});
|
||||
app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx))
|
||||
.await;
|
||||
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));
|
||||
finder.condition(&app, |f, _| f.matches.len() == 5).await;
|
||||
|
||||
finder.update(&mut app, |finder, ctx| {
|
||||
let matches = finder.matches.clone();
|
||||
|
||||
// Simulate a search being cancelled after the time limit,
|
||||
// returning only a subset of the matches that would have been found.
|
||||
finder.spawn_search(query.clone(), ctx);
|
||||
finder.update_matches(
|
||||
(
|
||||
finder.latest_search_id,
|
||||
true, // did-cancel
|
||||
query.clone(),
|
||||
vec![matches[1].clone(), matches[3].clone()],
|
||||
),
|
||||
ctx,
|
||||
);
|
||||
|
||||
// Simulate another cancellation.
|
||||
finder.spawn_search(query.clone(), ctx);
|
||||
finder.update_matches(
|
||||
(
|
||||
finder.latest_search_id,
|
||||
true, // did-cancel
|
||||
query.clone(),
|
||||
vec![matches[0].clone(), matches[2].clone(), matches[3].clone()],
|
||||
),
|
||||
ctx,
|
||||
);
|
||||
|
||||
assert_eq!(finder.matches, matches[0..4])
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_single_file_worktrees() {
|
||||
App::test_async((), |mut app| async move {
|
||||
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");
|
||||
fs::create_dir(&dir_path).unwrap();
|
||||
fs::write(&file_path, "").unwrap();
|
||||
|
||||
let settings = settings::channel(&app.font_cache()).unwrap().1;
|
||||
let (_, workspace) = app.add_window(|ctx| {
|
||||
let mut workspace = Workspace::new(0, settings.clone(), ctx);
|
||||
workspace.add_worktree(&file_path, ctx);
|
||||
workspace
|
||||
});
|
||||
app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx))
|
||||
.await;
|
||||
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.
|
||||
finder.update(&mut app, |f, ctx| f.spawn_search("thf".into(), ctx));
|
||||
finder.condition(&app, |f, _| f.matches.len() == 1).await;
|
||||
|
||||
app.read(|ctx| {
|
||||
let finder = finder.read(ctx);
|
||||
let (file_name, file_name_positions, full_path, full_path_positions) =
|
||||
finder.labels_for_match(&finder.matches[0], ctx).unwrap();
|
||||
|
||||
assert_eq!(file_name, "the-file");
|
||||
assert_eq!(file_name_positions, &[0, 1, 4]);
|
||||
assert_eq!(full_path, "the-file");
|
||||
assert_eq!(full_path_positions, &[0, 1, 4]);
|
||||
});
|
||||
|
||||
// Since the worktree root is a file, searching for its name followed by a slash does
|
||||
// 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 {
|
||||
let tmp_dir = temp_tree(json!({
|
||||
"dir1": { "a.txt": "" },
|
||||
"dir2": { "a.txt": "" }
|
||||
}));
|
||||
let settings = settings::channel(&app.font_cache()).unwrap().1;
|
||||
|
||||
let (_, workspace) = app.add_window(|ctx| Workspace::new(0, settings.clone(), ctx));
|
||||
|
||||
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(tmp_dir.path(), ctx);
|
||||
workspace
|
||||
.update(&mut app, |workspace, ctx| {
|
||||
workspace.open_paths(
|
||||
&[tmp_dir.path().join("dir1"), tmp_dir.path().join("dir2")],
|
||||
ctx,
|
||||
)
|
||||
})
|
||||
.await;
|
||||
app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx))
|
||||
.await;
|
||||
});
|
||||
app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx))
|
||||
.await;
|
||||
app.dispatch_action(
|
||||
window_id,
|
||||
vec![workspace.id()],
|
||||
"file_finder:toggle".into(),
|
||||
(),
|
||||
);
|
||||
|
||||
let (_, finder) =
|
||||
app.add_window(|ctx| FileFinder::new(settings, workspace.clone(), ctx));
|
||||
let finder = app.read(|ctx| {
|
||||
workspace
|
||||
.read(ctx)
|
||||
.modal()
|
||||
.cloned()
|
||||
.unwrap()
|
||||
.downcast::<FileFinder>()
|
||||
.unwrap()
|
||||
});
|
||||
let query_buffer = app.read(|ctx| finder.read(ctx).query_buffer.clone());
|
||||
|
||||
// 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));
|
||||
finder.condition(&app, |f, _| f.matches.len() == 2).await;
|
||||
let chain = vec![finder.id(), query_buffer.id()];
|
||||
app.dispatch_action(window_id, chain.clone(), "buffer:insert", "b".to_string());
|
||||
app.dispatch_action(window_id, chain.clone(), "buffer:insert", "n".to_string());
|
||||
app.dispatch_action(window_id, chain.clone(), "buffer:insert", "a".to_string());
|
||||
finder
|
||||
.condition(&app, |finder, _| finder.matches.len() == 2)
|
||||
.await;
|
||||
|
||||
// Can switch between different matches with the same relative path.
|
||||
finder.update(&mut app, |f, ctx| {
|
||||
assert_eq!(f.selected_index(), 0);
|
||||
f.select_next(&(), ctx);
|
||||
assert_eq!(f.selected_index(), 1);
|
||||
f.select_prev(&(), ctx);
|
||||
assert_eq!(f.selected_index(), 0);
|
||||
});
|
||||
let active_pane = app.read(|ctx| workspace.read(ctx).active_pane().clone());
|
||||
app.dispatch_action(
|
||||
window_id,
|
||||
vec![workspace.id(), finder.id()],
|
||||
"menu:select_next",
|
||||
(),
|
||||
);
|
||||
app.dispatch_action(
|
||||
window_id,
|
||||
vec![workspace.id(), finder.id()],
|
||||
"file_finder:confirm",
|
||||
(),
|
||||
);
|
||||
active_pane
|
||||
.condition(&app, |pane, _| pane.active_item().is_some())
|
||||
.await;
|
||||
app.read(|ctx| {
|
||||
let active_item = active_pane.read(ctx).active_item().unwrap();
|
||||
assert_eq!(active_item.title(ctx), "bandana");
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_matching_cancellation(mut app: gpui::TestAppContext) {
|
||||
let tmp_dir = temp_tree(json!({
|
||||
"hello": "",
|
||||
"goodbye": "",
|
||||
"halogen-light": "",
|
||||
"happiness": "",
|
||||
"height": "",
|
||||
"hi": "",
|
||||
"hiccup": "",
|
||||
}));
|
||||
let settings = settings::channel(&app.font_cache()).unwrap().1;
|
||||
let (_, workspace) = app.add_window(|ctx| {
|
||||
let mut workspace = Workspace::new(0, settings.clone(), ctx);
|
||||
workspace.add_worktree(tmp_dir.path(), ctx);
|
||||
workspace
|
||||
});
|
||||
app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx))
|
||||
.await;
|
||||
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));
|
||||
finder.condition(&app, |f, _| f.matches.len() == 5).await;
|
||||
|
||||
finder.update(&mut app, |finder, ctx| {
|
||||
let matches = finder.matches.clone();
|
||||
|
||||
// Simulate a search being cancelled after the time limit,
|
||||
// returning only a subset of the matches that would have been found.
|
||||
finder.spawn_search(query.clone(), ctx);
|
||||
finder.update_matches(
|
||||
(
|
||||
finder.latest_search_id,
|
||||
true, // did-cancel
|
||||
query.clone(),
|
||||
vec![matches[1].clone(), matches[3].clone()],
|
||||
),
|
||||
ctx,
|
||||
);
|
||||
|
||||
// Simulate another cancellation.
|
||||
finder.spawn_search(query.clone(), ctx);
|
||||
finder.update_matches(
|
||||
(
|
||||
finder.latest_search_id,
|
||||
true, // did-cancel
|
||||
query.clone(),
|
||||
vec![matches[0].clone(), matches[2].clone(), matches[3].clone()],
|
||||
),
|
||||
ctx,
|
||||
);
|
||||
|
||||
assert_eq!(finder.matches, matches[0..4])
|
||||
});
|
||||
}
|
||||
|
||||
#[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");
|
||||
fs::create_dir(&dir_path).unwrap();
|
||||
fs::write(&file_path, "").unwrap();
|
||||
|
||||
let settings = settings::channel(&app.font_cache()).unwrap().1;
|
||||
let (_, workspace) = app.add_window(|ctx| {
|
||||
let mut workspace = Workspace::new(0, settings.clone(), ctx);
|
||||
workspace.add_worktree(&file_path, ctx);
|
||||
workspace
|
||||
});
|
||||
app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx))
|
||||
.await;
|
||||
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.
|
||||
finder.update(&mut app, |f, ctx| f.spawn_search("thf".into(), ctx));
|
||||
finder.condition(&app, |f, _| f.matches.len() == 1).await;
|
||||
|
||||
app.read(|ctx| {
|
||||
let finder = finder.read(ctx);
|
||||
let (file_name, file_name_positions, full_path, full_path_positions) =
|
||||
finder.labels_for_match(&finder.matches[0], ctx).unwrap();
|
||||
|
||||
assert_eq!(file_name, "the-file");
|
||||
assert_eq!(file_name_positions, &[0, 1, 4]);
|
||||
assert_eq!(full_path, "the-file");
|
||||
assert_eq!(full_path_positions, &[0, 1, 4]);
|
||||
});
|
||||
|
||||
// Since the worktree root is a file, searching for its name followed by a slash does
|
||||
// not match anything.
|
||||
finder.update(&mut app, |f, ctx| f.spawn_search("thf/".into(), ctx));
|
||||
finder.condition(&app, |f, _| f.matches.len() == 0).await;
|
||||
}
|
||||
|
||||
#[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": "" }
|
||||
}));
|
||||
let settings = settings::channel(&app.font_cache()).unwrap().1;
|
||||
|
||||
let (_, workspace) = app.add_window(|ctx| Workspace::new(0, settings.clone(), ctx));
|
||||
|
||||
workspace
|
||||
.update(&mut app, |workspace, ctx| {
|
||||
workspace.open_paths(
|
||||
&[tmp_dir.path().join("dir1"), tmp_dir.path().join("dir2")],
|
||||
ctx,
|
||||
)
|
||||
})
|
||||
.await;
|
||||
app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx))
|
||||
.await;
|
||||
|
||||
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));
|
||||
finder.condition(&app, |f, _| f.matches.len() == 2).await;
|
||||
|
||||
// Can switch between different matches with the same relative path.
|
||||
finder.update(&mut app, |f, ctx| {
|
||||
assert_eq!(f.selected_index(), 0);
|
||||
f.select_next(&(), ctx);
|
||||
assert_eq!(f.selected_index(), 1);
|
||||
f.select_prev(&(), ctx);
|
||||
assert_eq!(f.selected_index(), 0);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+345
-356
@@ -729,398 +729,387 @@ 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;
|
||||
use tempdir::TempDir;
|
||||
|
||||
#[test]
|
||||
fn test_open_paths_action() {
|
||||
App::test((), |app| {
|
||||
let settings = settings::channel(&app.font_cache()).unwrap().1;
|
||||
#[gpui::test]
|
||||
fn test_open_paths_action(app: &mut gpui::MutableAppContext) {
|
||||
let settings = settings::channel(&app.font_cache()).unwrap().1;
|
||||
|
||||
init(app);
|
||||
init(app);
|
||||
|
||||
let dir = temp_tree(json!({
|
||||
"a": {
|
||||
"aa": null,
|
||||
"ab": null,
|
||||
},
|
||||
"b": {
|
||||
"ba": null,
|
||||
"bb": null,
|
||||
},
|
||||
"c": {
|
||||
"ca": null,
|
||||
"cb": null,
|
||||
},
|
||||
}));
|
||||
let dir = temp_tree(json!({
|
||||
"a": {
|
||||
"aa": null,
|
||||
"ab": null,
|
||||
},
|
||||
"b": {
|
||||
"ba": null,
|
||||
"bb": null,
|
||||
},
|
||||
"c": {
|
||||
"ca": null,
|
||||
"cb": null,
|
||||
},
|
||||
}));
|
||||
|
||||
app.dispatch_global_action(
|
||||
"workspace:open_paths",
|
||||
OpenParams {
|
||||
paths: vec![
|
||||
dir.path().join("a").to_path_buf(),
|
||||
dir.path().join("b").to_path_buf(),
|
||||
],
|
||||
settings: settings.clone(),
|
||||
},
|
||||
);
|
||||
assert_eq!(app.window_ids().count(), 1);
|
||||
app.dispatch_global_action(
|
||||
"workspace:open_paths",
|
||||
OpenParams {
|
||||
paths: vec![
|
||||
dir.path().join("a").to_path_buf(),
|
||||
dir.path().join("b").to_path_buf(),
|
||||
],
|
||||
settings: settings.clone(),
|
||||
},
|
||||
);
|
||||
assert_eq!(app.window_ids().count(), 1);
|
||||
|
||||
app.dispatch_global_action(
|
||||
"workspace:open_paths",
|
||||
OpenParams {
|
||||
paths: vec![dir.path().join("a").to_path_buf()],
|
||||
settings: settings.clone(),
|
||||
},
|
||||
);
|
||||
assert_eq!(app.window_ids().count(), 1);
|
||||
let workspace_view_1 = app
|
||||
.root_view::<Workspace>(app.window_ids().next().unwrap())
|
||||
.unwrap();
|
||||
assert_eq!(workspace_view_1.read(app).worktrees().len(), 2);
|
||||
app.dispatch_global_action(
|
||||
"workspace:open_paths",
|
||||
OpenParams {
|
||||
paths: vec![dir.path().join("a").to_path_buf()],
|
||||
settings: settings.clone(),
|
||||
},
|
||||
);
|
||||
assert_eq!(app.window_ids().count(), 1);
|
||||
let workspace_view_1 = app
|
||||
.root_view::<Workspace>(app.window_ids().next().unwrap())
|
||||
.unwrap();
|
||||
assert_eq!(workspace_view_1.read(app).worktrees().len(), 2);
|
||||
|
||||
app.dispatch_global_action(
|
||||
"workspace:open_paths",
|
||||
OpenParams {
|
||||
paths: vec![
|
||||
dir.path().join("b").to_path_buf(),
|
||||
dir.path().join("c").to_path_buf(),
|
||||
],
|
||||
settings: settings.clone(),
|
||||
},
|
||||
);
|
||||
assert_eq!(app.window_ids().count(), 2);
|
||||
});
|
||||
app.dispatch_global_action(
|
||||
"workspace:open_paths",
|
||||
OpenParams {
|
||||
paths: vec![
|
||||
dir.path().join("b").to_path_buf(),
|
||||
dir.path().join("c").to_path_buf(),
|
||||
],
|
||||
settings: settings.clone(),
|
||||
},
|
||||
);
|
||||
assert_eq!(app.window_ids().count(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_open_entry() {
|
||||
App::test_async((), |mut app| async move {
|
||||
let dir = temp_tree(json!({
|
||||
"a": {
|
||||
"file1": "contents 1",
|
||||
"file2": "contents 2",
|
||||
"file3": "contents 3",
|
||||
},
|
||||
}));
|
||||
#[gpui::test]
|
||||
async fn test_open_entry(mut app: gpui::TestAppContext) {
|
||||
let dir = temp_tree(json!({
|
||||
"a": {
|
||||
"file1": "contents 1",
|
||||
"file2": "contents 2",
|
||||
"file3": "contents 3",
|
||||
},
|
||||
}));
|
||||
|
||||
let settings = settings::channel(&app.font_cache()).unwrap().1;
|
||||
let settings = settings::channel(&app.font_cache()).unwrap().1;
|
||||
|
||||
let (_, workspace) = app.add_window(|ctx| {
|
||||
let mut workspace = Workspace::new(0, settings, ctx);
|
||||
workspace.add_worktree(dir.path(), ctx);
|
||||
workspace
|
||||
});
|
||||
|
||||
app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx))
|
||||
.await;
|
||||
let entries = app.read(|ctx| workspace.file_entries(ctx));
|
||||
let file1 = entries[0].clone();
|
||||
let file2 = entries[1].clone();
|
||||
let file3 = entries[2].clone();
|
||||
|
||||
// Open the first entry
|
||||
let (_, workspace) = app.add_window(|ctx| {
|
||||
let mut workspace = Workspace::new(0, settings, ctx);
|
||||
workspace.add_worktree(dir.path(), ctx);
|
||||
workspace
|
||||
.update(&mut app, |w, ctx| w.open_entry(file1.clone(), ctx))
|
||||
.unwrap()
|
||||
.await;
|
||||
app.read(|ctx| {
|
||||
let pane = workspace.read(ctx).active_pane().read(ctx);
|
||||
assert_eq!(
|
||||
pane.active_item().unwrap().entry_id(ctx),
|
||||
Some(file1.clone())
|
||||
);
|
||||
assert_eq!(pane.items().len(), 1);
|
||||
});
|
||||
|
||||
// Open the second entry
|
||||
workspace
|
||||
.update(&mut app, |w, ctx| w.open_entry(file2.clone(), ctx))
|
||||
.unwrap()
|
||||
.await;
|
||||
app.read(|ctx| {
|
||||
let pane = workspace.read(ctx).active_pane().read(ctx);
|
||||
assert_eq!(
|
||||
pane.active_item().unwrap().entry_id(ctx),
|
||||
Some(file2.clone())
|
||||
);
|
||||
assert_eq!(pane.items().len(), 2);
|
||||
});
|
||||
|
||||
// Open the first entry again. The existing pane item is activated.
|
||||
workspace.update(&mut app, |w, ctx| {
|
||||
assert!(w.open_entry(file1.clone(), ctx).is_none())
|
||||
});
|
||||
app.read(|ctx| {
|
||||
let pane = workspace.read(ctx).active_pane().read(ctx);
|
||||
assert_eq!(
|
||||
pane.active_item().unwrap().entry_id(ctx),
|
||||
Some(file1.clone())
|
||||
);
|
||||
assert_eq!(pane.items().len(), 2);
|
||||
});
|
||||
|
||||
// Split the pane with the first entry, then open the second entry again.
|
||||
workspace.update(&mut app, |w, ctx| {
|
||||
w.split_pane(w.active_pane().clone(), SplitDirection::Right, ctx);
|
||||
assert!(w.open_entry(file2.clone(), ctx).is_none());
|
||||
assert_eq!(
|
||||
w.active_pane()
|
||||
.read(ctx)
|
||||
.active_item()
|
||||
.unwrap()
|
||||
.entry_id(ctx.as_ref()),
|
||||
Some(file2.clone())
|
||||
);
|
||||
});
|
||||
|
||||
// Open the third entry twice concurrently. Two pane items
|
||||
// are added.
|
||||
let (t1, t2) = workspace.update(&mut app, |w, ctx| {
|
||||
(
|
||||
w.open_entry(file3.clone(), ctx).unwrap(),
|
||||
w.open_entry(file3.clone(), ctx).unwrap(),
|
||||
)
|
||||
});
|
||||
t1.await;
|
||||
t2.await;
|
||||
app.read(|ctx| {
|
||||
let pane = workspace.read(ctx).active_pane().read(ctx);
|
||||
assert_eq!(
|
||||
pane.active_item().unwrap().entry_id(ctx),
|
||||
Some(file3.clone())
|
||||
);
|
||||
let pane_entries = pane
|
||||
.items()
|
||||
.iter()
|
||||
.map(|i| i.entry_id(ctx).unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(pane_entries, &[file1, file2, file3.clone(), file3]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_open_paths() {
|
||||
App::test_async((), |mut app| async move {
|
||||
let dir1 = temp_tree(json!({
|
||||
"a.txt": "",
|
||||
}));
|
||||
let dir2 = temp_tree(json!({
|
||||
"b.txt": "",
|
||||
}));
|
||||
|
||||
let settings = settings::channel(&app.font_cache()).unwrap().1;
|
||||
let (_, workspace) = app.add_window(|ctx| {
|
||||
let mut workspace = Workspace::new(0, settings, ctx);
|
||||
workspace.add_worktree(dir1.path(), ctx);
|
||||
workspace
|
||||
});
|
||||
app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx))
|
||||
.await;
|
||||
|
||||
// Open a file within an existing worktree.
|
||||
app.update(|ctx| {
|
||||
workspace.update(ctx, |view, ctx| {
|
||||
view.open_paths(&[dir1.path().join("a.txt")], ctx)
|
||||
})
|
||||
})
|
||||
app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx))
|
||||
.await;
|
||||
app.read(|ctx| {
|
||||
assert_eq!(
|
||||
workspace
|
||||
.read(ctx)
|
||||
.active_pane()
|
||||
.read(ctx)
|
||||
.active_item()
|
||||
.unwrap()
|
||||
.title(ctx),
|
||||
"a.txt"
|
||||
);
|
||||
});
|
||||
let entries = app.read(|ctx| workspace.file_entries(ctx));
|
||||
let file1 = entries[0].clone();
|
||||
let file2 = entries[1].clone();
|
||||
let file3 = entries[2].clone();
|
||||
|
||||
// Open a file outside of any existing worktree.
|
||||
app.update(|ctx| {
|
||||
workspace.update(ctx, |view, ctx| {
|
||||
view.open_paths(&[dir2.path().join("b.txt")], ctx)
|
||||
})
|
||||
})
|
||||
// Open the first entry
|
||||
workspace
|
||||
.update(&mut app, |w, ctx| w.open_entry(file1.clone(), ctx))
|
||||
.unwrap()
|
||||
.await;
|
||||
app.read(|ctx| {
|
||||
let worktree_roots = workspace
|
||||
app.read(|ctx| {
|
||||
let pane = workspace.read(ctx).active_pane().read(ctx);
|
||||
assert_eq!(
|
||||
pane.active_item().unwrap().entry_id(ctx),
|
||||
Some(file1.clone())
|
||||
);
|
||||
assert_eq!(pane.items().len(), 1);
|
||||
});
|
||||
|
||||
// Open the second entry
|
||||
workspace
|
||||
.update(&mut app, |w, ctx| w.open_entry(file2.clone(), ctx))
|
||||
.unwrap()
|
||||
.await;
|
||||
app.read(|ctx| {
|
||||
let pane = workspace.read(ctx).active_pane().read(ctx);
|
||||
assert_eq!(
|
||||
pane.active_item().unwrap().entry_id(ctx),
|
||||
Some(file2.clone())
|
||||
);
|
||||
assert_eq!(pane.items().len(), 2);
|
||||
});
|
||||
|
||||
// Open the first entry again. The existing pane item is activated.
|
||||
workspace.update(&mut app, |w, ctx| {
|
||||
assert!(w.open_entry(file1.clone(), ctx).is_none())
|
||||
});
|
||||
app.read(|ctx| {
|
||||
let pane = workspace.read(ctx).active_pane().read(ctx);
|
||||
assert_eq!(
|
||||
pane.active_item().unwrap().entry_id(ctx),
|
||||
Some(file1.clone())
|
||||
);
|
||||
assert_eq!(pane.items().len(), 2);
|
||||
});
|
||||
|
||||
// Split the pane with the first entry, then open the second entry again.
|
||||
workspace.update(&mut app, |w, ctx| {
|
||||
w.split_pane(w.active_pane().clone(), SplitDirection::Right, ctx);
|
||||
assert!(w.open_entry(file2.clone(), ctx).is_none());
|
||||
assert_eq!(
|
||||
w.active_pane()
|
||||
.read(ctx)
|
||||
.worktrees()
|
||||
.iter()
|
||||
.map(|w| w.read(ctx).abs_path())
|
||||
.collect::<HashSet<_>>();
|
||||
assert_eq!(
|
||||
worktree_roots,
|
||||
vec![dir1.path(), &dir2.path().join("b.txt")]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
);
|
||||
assert_eq!(
|
||||
workspace
|
||||
.read(ctx)
|
||||
.active_pane()
|
||||
.read(ctx)
|
||||
.active_item()
|
||||
.unwrap()
|
||||
.title(ctx),
|
||||
"b.txt"
|
||||
);
|
||||
});
|
||||
.active_item()
|
||||
.unwrap()
|
||||
.entry_id(ctx.as_ref()),
|
||||
Some(file2.clone())
|
||||
);
|
||||
});
|
||||
|
||||
// Open the third entry twice concurrently. Two pane items
|
||||
// are added.
|
||||
let (t1, t2) = workspace.update(&mut app, |w, ctx| {
|
||||
(
|
||||
w.open_entry(file3.clone(), ctx).unwrap(),
|
||||
w.open_entry(file3.clone(), ctx).unwrap(),
|
||||
)
|
||||
});
|
||||
t1.await;
|
||||
t2.await;
|
||||
app.read(|ctx| {
|
||||
let pane = workspace.read(ctx).active_pane().read(ctx);
|
||||
assert_eq!(
|
||||
pane.active_item().unwrap().entry_id(ctx),
|
||||
Some(file3.clone())
|
||||
);
|
||||
let pane_entries = pane
|
||||
.items()
|
||||
.iter()
|
||||
.map(|i| i.entry_id(ctx).unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(pane_entries, &[file1, file2, file3.clone(), file3]);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_open_and_save_new_file() {
|
||||
App::test_async((), |mut app| async move {
|
||||
let dir = TempDir::new("test-new-file").unwrap();
|
||||
let settings = settings::channel(&app.font_cache()).unwrap().1;
|
||||
let (_, 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| {
|
||||
#[gpui::test]
|
||||
async fn test_open_paths(mut app: gpui::TestAppContext) {
|
||||
let dir1 = temp_tree(json!({
|
||||
"a.txt": "",
|
||||
}));
|
||||
let dir2 = temp_tree(json!({
|
||||
"b.txt": "",
|
||||
}));
|
||||
|
||||
let settings = settings::channel(&app.font_cache()).unwrap().1;
|
||||
let (_, workspace) = app.add_window(|ctx| {
|
||||
let mut workspace = Workspace::new(0, settings, ctx);
|
||||
workspace.add_worktree(dir1.path(), ctx);
|
||||
workspace
|
||||
});
|
||||
app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx))
|
||||
.await;
|
||||
|
||||
// Open a file within an existing worktree.
|
||||
app.update(|ctx| {
|
||||
workspace.update(ctx, |view, ctx| {
|
||||
view.open_paths(&[dir1.path().join("a.txt")], ctx)
|
||||
})
|
||||
})
|
||||
.await;
|
||||
app.read(|ctx| {
|
||||
assert_eq!(
|
||||
workspace
|
||||
.read(ctx)
|
||||
.worktrees()
|
||||
.iter()
|
||||
.next()
|
||||
.active_pane()
|
||||
.read(ctx)
|
||||
.active_item()
|
||||
.unwrap()
|
||||
.clone()
|
||||
});
|
||||
tree.flush_fs_events(&app).await;
|
||||
.title(ctx),
|
||||
"a.txt"
|
||||
);
|
||||
});
|
||||
|
||||
// Create a new untitled buffer
|
||||
let editor = workspace.update(&mut app, |workspace, ctx| {
|
||||
workspace.open_new_file(&(), ctx);
|
||||
workspace
|
||||
.active_item(ctx)
|
||||
.unwrap()
|
||||
.to_any()
|
||||
.downcast::<BufferView>()
|
||||
.unwrap()
|
||||
});
|
||||
editor.update(&mut app, |editor, ctx| {
|
||||
assert!(!editor.is_dirty(ctx.as_ref()));
|
||||
assert_eq!(editor.title(ctx.as_ref()), "untitled");
|
||||
editor.insert(&"hi".to_string(), ctx);
|
||||
assert!(editor.is_dirty(ctx.as_ref()));
|
||||
});
|
||||
|
||||
// Save the buffer. This prompts for a filename.
|
||||
workspace.update(&mut app, |workspace, ctx| {
|
||||
workspace.save_active_item(&(), ctx)
|
||||
});
|
||||
app.simulate_new_path_selection(|parent_dir| {
|
||||
assert_eq!(parent_dir, dir.path());
|
||||
Some(parent_dir.join("the-new-name"))
|
||||
});
|
||||
app.read(|ctx| {
|
||||
assert!(editor.is_dirty(ctx));
|
||||
assert_eq!(editor.title(ctx), "untitled");
|
||||
});
|
||||
|
||||
// When the save completes, the buffer's title is updated.
|
||||
tree.update(&mut app, |tree, ctx| tree.next_scan_complete(ctx))
|
||||
.await;
|
||||
app.read(|ctx| {
|
||||
assert!(!editor.is_dirty(ctx));
|
||||
assert_eq!(editor.title(ctx), "the-new-name");
|
||||
});
|
||||
|
||||
// Edit the file and save it again. This time, there is no filename prompt.
|
||||
editor.update(&mut app, |editor, ctx| {
|
||||
editor.insert(&" there".to_string(), ctx);
|
||||
assert_eq!(editor.is_dirty(ctx.as_ref()), true);
|
||||
});
|
||||
workspace.update(&mut app, |workspace, ctx| {
|
||||
workspace.save_active_item(&(), ctx)
|
||||
});
|
||||
assert!(!app.did_prompt_for_new_path());
|
||||
editor
|
||||
.condition(&app, |editor, ctx| !editor.is_dirty(ctx))
|
||||
.await;
|
||||
app.read(|ctx| assert_eq!(editor.title(ctx), "the-new-name"));
|
||||
|
||||
// Open the same newly-created file in another pane item. The new editor should reuse
|
||||
// the same buffer.
|
||||
workspace.update(&mut app, |workspace, ctx| {
|
||||
workspace.open_new_file(&(), ctx);
|
||||
workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, ctx);
|
||||
assert!(workspace
|
||||
.open_entry((tree.id(), Path::new("the-new-name").into()), ctx)
|
||||
.is_none());
|
||||
});
|
||||
let editor2 = workspace.update(&mut app, |workspace, ctx| {
|
||||
workspace
|
||||
.active_item(ctx)
|
||||
.unwrap()
|
||||
.to_any()
|
||||
.downcast::<BufferView>()
|
||||
.unwrap()
|
||||
});
|
||||
app.read(|ctx| {
|
||||
assert_eq!(editor2.read(ctx).buffer(), editor.read(ctx).buffer());
|
||||
// Open a file outside of any existing worktree.
|
||||
app.update(|ctx| {
|
||||
workspace.update(ctx, |view, ctx| {
|
||||
view.open_paths(&[dir2.path().join("b.txt")], ctx)
|
||||
})
|
||||
})
|
||||
.await;
|
||||
app.read(|ctx| {
|
||||
let worktree_roots = workspace
|
||||
.read(ctx)
|
||||
.worktrees()
|
||||
.iter()
|
||||
.map(|w| w.read(ctx).abs_path())
|
||||
.collect::<HashSet<_>>();
|
||||
assert_eq!(
|
||||
worktree_roots,
|
||||
vec![dir1.path(), &dir2.path().join("b.txt")]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
);
|
||||
assert_eq!(
|
||||
workspace
|
||||
.read(ctx)
|
||||
.active_pane()
|
||||
.read(ctx)
|
||||
.active_item()
|
||||
.unwrap()
|
||||
.title(ctx),
|
||||
"b.txt"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pane_actions() {
|
||||
App::test_async((), |mut app| async move {
|
||||
app.update(|ctx| pane::init(ctx));
|
||||
|
||||
let dir = temp_tree(json!({
|
||||
"a": {
|
||||
"file1": "contents 1",
|
||||
"file2": "contents 2",
|
||||
"file3": "contents 3",
|
||||
},
|
||||
}));
|
||||
|
||||
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
|
||||
});
|
||||
app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx))
|
||||
.await;
|
||||
let entries = app.read(|ctx| workspace.file_entries(ctx));
|
||||
let file1 = entries[0].clone();
|
||||
|
||||
let pane_1 = app.read(|ctx| workspace.read(ctx).active_pane().clone());
|
||||
|
||||
#[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| {
|
||||
let mut workspace = Workspace::new(0, settings, ctx);
|
||||
workspace.add_worktree(dir.path(), ctx);
|
||||
workspace
|
||||
.update(&mut app, |w, ctx| w.open_entry(file1.clone(), ctx))
|
||||
});
|
||||
let tree = app.read(|ctx| {
|
||||
workspace
|
||||
.read(ctx)
|
||||
.worktrees()
|
||||
.iter()
|
||||
.next()
|
||||
.unwrap()
|
||||
.await;
|
||||
app.read(|ctx| {
|
||||
assert_eq!(
|
||||
pane_1.read(ctx).active_item().unwrap().entry_id(ctx),
|
||||
Some(file1.clone())
|
||||
);
|
||||
});
|
||||
.clone()
|
||||
});
|
||||
tree.flush_fs_events(&app).await;
|
||||
|
||||
app.dispatch_action(window_id, vec![pane_1.id()], "pane:split_right", ());
|
||||
app.update(|ctx| {
|
||||
let pane_2 = workspace.read(ctx).active_pane().clone();
|
||||
assert_ne!(pane_1, pane_2);
|
||||
// Create a new untitled buffer
|
||||
let editor = workspace.update(&mut app, |workspace, ctx| {
|
||||
workspace.open_new_file(&(), ctx);
|
||||
workspace
|
||||
.active_item(ctx)
|
||||
.unwrap()
|
||||
.to_any()
|
||||
.downcast::<BufferView>()
|
||||
.unwrap()
|
||||
});
|
||||
editor.update(&mut app, |editor, ctx| {
|
||||
assert!(!editor.is_dirty(ctx.as_ref()));
|
||||
assert_eq!(editor.title(ctx.as_ref()), "untitled");
|
||||
editor.insert(&"hi".to_string(), ctx);
|
||||
assert!(editor.is_dirty(ctx.as_ref()));
|
||||
});
|
||||
|
||||
let pane2_item = pane_2.read(ctx).active_item().unwrap();
|
||||
assert_eq!(pane2_item.entry_id(ctx.as_ref()), Some(file1.clone()));
|
||||
// Save the buffer. This prompts for a filename.
|
||||
workspace.update(&mut app, |workspace, ctx| {
|
||||
workspace.save_active_item(&(), ctx)
|
||||
});
|
||||
app.simulate_new_path_selection(|parent_dir| {
|
||||
assert_eq!(parent_dir, dir.path());
|
||||
Some(parent_dir.join("the-new-name"))
|
||||
});
|
||||
app.read(|ctx| {
|
||||
assert!(editor.is_dirty(ctx));
|
||||
assert_eq!(editor.title(ctx), "untitled");
|
||||
});
|
||||
|
||||
ctx.dispatch_action(window_id, vec![pane_2.id()], "pane:close_active_item", ());
|
||||
let workspace_view = workspace.read(ctx);
|
||||
assert_eq!(workspace_view.panes.len(), 1);
|
||||
assert_eq!(workspace_view.active_pane(), &pane_1);
|
||||
});
|
||||
// When the save completes, the buffer's title is updated.
|
||||
tree.update(&mut app, |tree, ctx| tree.next_scan_complete(ctx))
|
||||
.await;
|
||||
app.read(|ctx| {
|
||||
assert!(!editor.is_dirty(ctx));
|
||||
assert_eq!(editor.title(ctx), "the-new-name");
|
||||
});
|
||||
|
||||
// Edit the file and save it again. This time, there is no filename prompt.
|
||||
editor.update(&mut app, |editor, ctx| {
|
||||
editor.insert(&" there".to_string(), ctx);
|
||||
assert_eq!(editor.is_dirty(ctx.as_ref()), true);
|
||||
});
|
||||
workspace.update(&mut app, |workspace, ctx| {
|
||||
workspace.save_active_item(&(), ctx)
|
||||
});
|
||||
assert!(!app.did_prompt_for_new_path());
|
||||
editor
|
||||
.condition(&app, |editor, ctx| !editor.is_dirty(ctx))
|
||||
.await;
|
||||
app.read(|ctx| assert_eq!(editor.title(ctx), "the-new-name"));
|
||||
|
||||
// Open the same newly-created file in another pane item. The new editor should reuse
|
||||
// the same buffer.
|
||||
workspace.update(&mut app, |workspace, ctx| {
|
||||
workspace.open_new_file(&(), ctx);
|
||||
workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, ctx);
|
||||
assert!(workspace
|
||||
.open_entry((tree.id(), Path::new("the-new-name").into()), ctx)
|
||||
.is_none());
|
||||
});
|
||||
let editor2 = workspace.update(&mut app, |workspace, ctx| {
|
||||
workspace
|
||||
.active_item(ctx)
|
||||
.unwrap()
|
||||
.to_any()
|
||||
.downcast::<BufferView>()
|
||||
.unwrap()
|
||||
});
|
||||
app.read(|ctx| {
|
||||
assert_eq!(editor2.read(ctx).buffer(), editor.read(ctx).buffer());
|
||||
})
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_pane_actions(mut app: gpui::TestAppContext) {
|
||||
app.update(|ctx| pane::init(ctx));
|
||||
|
||||
let dir = temp_tree(json!({
|
||||
"a": {
|
||||
"file1": "contents 1",
|
||||
"file2": "contents 2",
|
||||
"file3": "contents 3",
|
||||
},
|
||||
}));
|
||||
|
||||
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
|
||||
});
|
||||
app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx))
|
||||
.await;
|
||||
let entries = app.read(|ctx| workspace.file_entries(ctx));
|
||||
let file1 = entries[0].clone();
|
||||
|
||||
let pane_1 = app.read(|ctx| workspace.read(ctx).active_pane().clone());
|
||||
|
||||
workspace
|
||||
.update(&mut app, |w, ctx| w.open_entry(file1.clone(), ctx))
|
||||
.unwrap()
|
||||
.await;
|
||||
app.read(|ctx| {
|
||||
assert_eq!(
|
||||
pane_1.read(ctx).active_item().unwrap().entry_id(ctx),
|
||||
Some(file1.clone())
|
||||
);
|
||||
});
|
||||
|
||||
app.dispatch_action(window_id, vec![pane_1.id()], "pane:split_right", ());
|
||||
app.update(|ctx| {
|
||||
let pane_2 = workspace.read(ctx).active_pane().clone();
|
||||
assert_ne!(pane_1, pane_2);
|
||||
|
||||
let pane2_item = pane_2.read(ctx).active_item().unwrap();
|
||||
assert_eq!(pane2_item.entry_id(ctx.as_ref()), Some(file1.clone()));
|
||||
|
||||
ctx.dispatch_action(window_id, vec![pane_2.id()], "pane:close_active_item", ());
|
||||
let workspace_view = workspace.read(ctx);
|
||||
assert_eq!(workspace_view.panes.len(), 1);
|
||||
assert_eq!(workspace_view.active_pane(), &pane_1);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+206
-218
@@ -1353,7 +1353,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;
|
||||
@@ -1361,248 +1360,237 @@ 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 {
|
||||
let dir = temp_tree(json!({
|
||||
"root": {
|
||||
"apple": "",
|
||||
"banana": {
|
||||
"carrot": {
|
||||
"date": "",
|
||||
"endive": "",
|
||||
}
|
||||
},
|
||||
"fennel": {
|
||||
"grape": "",
|
||||
#[gpui::test]
|
||||
async fn test_populate_and_search(mut app: gpui::TestAppContext) {
|
||||
let dir = temp_tree(json!({
|
||||
"root": {
|
||||
"apple": "",
|
||||
"banana": {
|
||||
"carrot": {
|
||||
"date": "",
|
||||
"endive": "",
|
||||
}
|
||||
},
|
||||
"fennel": {
|
||||
"grape": "",
|
||||
}
|
||||
}));
|
||||
}
|
||||
}));
|
||||
|
||||
let root_link_path = dir.path().join("root_link");
|
||||
unix::fs::symlink(&dir.path().join("root"), &root_link_path).unwrap();
|
||||
unix::fs::symlink(
|
||||
&dir.path().join("root/fennel"),
|
||||
&dir.path().join("root/finnochio"),
|
||||
let root_link_path = dir.path().join("root_link");
|
||||
unix::fs::symlink(&dir.path().join("root"), &root_link_path).unwrap();
|
||||
unix::fs::symlink(
|
||||
&dir.path().join("root/fennel"),
|
||||
&dir.path().join("root/finnochio"),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let tree = app.add_model(|ctx| Worktree::new(root_link_path, ctx));
|
||||
|
||||
app.read(|ctx| tree.read(ctx).scan_complete()).await;
|
||||
app.read(|ctx| {
|
||||
let tree = tree.read(ctx);
|
||||
assert_eq!(tree.file_count(), 5);
|
||||
|
||||
assert_eq!(
|
||||
tree.inode_for_path("fennel/grape"),
|
||||
tree.inode_for_path("finnochio/grape")
|
||||
);
|
||||
|
||||
let results = match_paths(
|
||||
Some(tree.snapshot()).iter(),
|
||||
"bna",
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
10,
|
||||
Default::default(),
|
||||
ctx.thread_pool().clone(),
|
||||
)
|
||||
.into_iter()
|
||||
.map(|result| result.path)
|
||||
.collect::<Vec<Arc<Path>>>();
|
||||
assert_eq!(
|
||||
results,
|
||||
vec![
|
||||
PathBuf::from("banana/carrot/date").into(),
|
||||
PathBuf::from("banana/carrot/endive").into(),
|
||||
]
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_save_file(mut app: gpui::TestAppContext) {
|
||||
let dir = temp_tree(json!({
|
||||
"file1": "the old contents",
|
||||
}));
|
||||
|
||||
let tree = app.add_model(|ctx| Worktree::new(dir.path(), ctx));
|
||||
app.read(|ctx| tree.read(ctx).scan_complete()).await;
|
||||
app.read(|ctx| assert_eq!(tree.read(ctx).file_count(), 1));
|
||||
|
||||
let buffer =
|
||||
app.add_model(|ctx| Buffer::new(1, "a line of text.\n".repeat(10 * 1024), ctx));
|
||||
|
||||
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();
|
||||
path
|
||||
});
|
||||
|
||||
let history = app
|
||||
.read(|ctx| tree.read(ctx).load_history(&path, ctx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let tree = app.add_model(|ctx| Worktree::new(root_link_path, ctx));
|
||||
|
||||
app.read(|ctx| tree.read(ctx).scan_complete()).await;
|
||||
app.read(|ctx| {
|
||||
let tree = tree.read(ctx);
|
||||
assert_eq!(tree.file_count(), 5);
|
||||
|
||||
assert_eq!(
|
||||
tree.inode_for_path("fennel/grape"),
|
||||
tree.inode_for_path("finnochio/grape")
|
||||
);
|
||||
|
||||
let results = match_paths(
|
||||
Some(tree.snapshot()).iter(),
|
||||
"bna",
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
10,
|
||||
Default::default(),
|
||||
ctx.thread_pool().clone(),
|
||||
)
|
||||
.into_iter()
|
||||
.map(|result| result.path)
|
||||
.collect::<Vec<Arc<Path>>>();
|
||||
assert_eq!(
|
||||
results,
|
||||
vec![
|
||||
PathBuf::from("banana/carrot/date").into(),
|
||||
PathBuf::from("banana/carrot/endive").into(),
|
||||
]
|
||||
);
|
||||
})
|
||||
app.read(|ctx| {
|
||||
assert_eq!(history.base_text.as_ref(), buffer.read(ctx).text());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_save_file() {
|
||||
App::test_async((), |mut app| async move {
|
||||
let dir = temp_tree(json!({
|
||||
"file1": "the old contents",
|
||||
}));
|
||||
#[gpui::test]
|
||||
async fn test_save_in_single_file_worktree(mut app: gpui::TestAppContext) {
|
||||
let dir = temp_tree(json!({
|
||||
"file1": "the old contents",
|
||||
}));
|
||||
|
||||
let tree = app.add_model(|ctx| Worktree::new(dir.path(), ctx));
|
||||
app.read(|ctx| tree.read(ctx).scan_complete()).await;
|
||||
app.read(|ctx| assert_eq!(tree.read(ctx).file_count(), 1));
|
||||
let tree = app.add_model(|ctx| Worktree::new(dir.path().join("file1"), ctx));
|
||||
app.read(|ctx| tree.read(ctx).scan_complete()).await;
|
||||
app.read(|ctx| assert_eq!(tree.read(ctx).file_count(), 1));
|
||||
|
||||
let buffer =
|
||||
app.add_model(|ctx| Buffer::new(1, "a line of text.\n".repeat(10 * 1024), ctx));
|
||||
let buffer =
|
||||
app.add_model(|ctx| Buffer::new(1, "a line of text.\n".repeat(10 * 1024), ctx));
|
||||
|
||||
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();
|
||||
path
|
||||
});
|
||||
|
||||
let history = app
|
||||
.read(|ctx| tree.read(ctx).load_history(&path, ctx))
|
||||
.await
|
||||
.unwrap();
|
||||
app.read(|ctx| {
|
||||
assert_eq!(history.base_text.as_ref(), buffer.read(ctx).text());
|
||||
});
|
||||
let file = app.read(|ctx| tree.file("", ctx));
|
||||
app.update(|ctx| {
|
||||
assert_eq!(file.path().file_name(), None);
|
||||
smol::block_on(file.save(buffer.read(ctx).snapshot(), ctx.as_ref())).unwrap();
|
||||
});
|
||||
|
||||
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_save_in_single_file_worktree() {
|
||||
App::test_async((), |mut app| async move {
|
||||
let dir = temp_tree(json!({
|
||||
"file1": "the old contents",
|
||||
}));
|
||||
|
||||
let tree = app.add_model(|ctx| Worktree::new(dir.path().join("file1"), ctx));
|
||||
app.read(|ctx| tree.read(ctx).scan_complete()).await;
|
||||
app.read(|ctx| assert_eq!(tree.read(ctx).file_count(), 1));
|
||||
|
||||
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));
|
||||
app.update(|ctx| {
|
||||
assert_eq!(file.path().file_name(), None);
|
||||
smol::block_on(file.save(buffer.read(ctx).snapshot(), ctx.as_ref())).unwrap();
|
||||
});
|
||||
|
||||
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 {
|
||||
let dir = temp_tree(json!({
|
||||
"a": {
|
||||
"file1": "",
|
||||
"file2": "",
|
||||
"file3": "",
|
||||
},
|
||||
"b": {
|
||||
"c": {
|
||||
"file4": "",
|
||||
"file5": "",
|
||||
}
|
||||
#[gpui::test]
|
||||
async fn test_rescan_simple(mut app: gpui::TestAppContext) {
|
||||
let dir = temp_tree(json!({
|
||||
"a": {
|
||||
"file1": "",
|
||||
"file2": "",
|
||||
"file3": "",
|
||||
},
|
||||
"b": {
|
||||
"c": {
|
||||
"file4": "",
|
||||
"file5": "",
|
||||
}
|
||||
}));
|
||||
}
|
||||
}));
|
||||
|
||||
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 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),
|
||||
)
|
||||
});
|
||||
|
||||
// The worktree hasn't scanned the directories containing these paths,
|
||||
// so it can't determine that the paths are deleted.
|
||||
// The worktree hasn't scanned the directories containing these paths,
|
||||
// so it can't determine that the paths are deleted.
|
||||
assert!(!file2.is_deleted());
|
||||
assert!(!file3.is_deleted());
|
||||
assert!(!file4.is_deleted());
|
||||
assert!(!file5.is_deleted());
|
||||
assert!(!non_existent_file.is_deleted());
|
||||
|
||||
// After scanning, the worktree knows which files exist and which don't.
|
||||
app.read(|ctx| tree.read(ctx).scan_complete()).await;
|
||||
assert!(!file2.is_deleted());
|
||||
assert!(!file3.is_deleted());
|
||||
assert!(!file4.is_deleted());
|
||||
assert!(!file5.is_deleted());
|
||||
assert!(non_existent_file.is_deleted());
|
||||
|
||||
tree.flush_fs_events(&app).await;
|
||||
std::fs::rename(dir.path().join("a/file3"), dir.path().join("b/c/file3")).unwrap();
|
||||
std::fs::remove_file(dir.path().join("b/c/file5")).unwrap();
|
||||
std::fs::rename(dir.path().join("b/c"), dir.path().join("d")).unwrap();
|
||||
std::fs::rename(dir.path().join("a/file2"), dir.path().join("a/file2.new")).unwrap();
|
||||
tree.update(&mut app, |tree, ctx| tree.next_scan_complete(ctx))
|
||||
.await;
|
||||
|
||||
app.read(|ctx| {
|
||||
assert_eq!(
|
||||
tree.read(ctx)
|
||||
.paths()
|
||||
.map(|p| p.to_str().unwrap())
|
||||
.collect::<Vec<_>>(),
|
||||
vec![
|
||||
"a",
|
||||
"a/file1",
|
||||
"a/file2.new",
|
||||
"b",
|
||||
"d",
|
||||
"d/file3",
|
||||
"d/file4"
|
||||
]
|
||||
);
|
||||
|
||||
assert_eq!(file2.path().to_str().unwrap(), "a/file2.new");
|
||||
assert_eq!(file4.path().as_ref(), Path::new("d/file4"));
|
||||
assert_eq!(file5.path().as_ref(), Path::new("d/file5"));
|
||||
assert!(!file2.is_deleted());
|
||||
assert!(!file3.is_deleted());
|
||||
assert!(!file4.is_deleted());
|
||||
assert!(!file5.is_deleted());
|
||||
assert!(!non_existent_file.is_deleted());
|
||||
assert!(file5.is_deleted());
|
||||
|
||||
// After scanning, the worktree knows which files exist and which don't.
|
||||
app.read(|ctx| tree.read(ctx).scan_complete()).await;
|
||||
assert!(!file2.is_deleted());
|
||||
assert!(!file3.is_deleted());
|
||||
assert!(!file4.is_deleted());
|
||||
assert!(!file5.is_deleted());
|
||||
assert!(non_existent_file.is_deleted());
|
||||
|
||||
tree.flush_fs_events(&app).await;
|
||||
std::fs::rename(dir.path().join("a/file3"), dir.path().join("b/c/file3")).unwrap();
|
||||
std::fs::remove_file(dir.path().join("b/c/file5")).unwrap();
|
||||
std::fs::rename(dir.path().join("b/c"), dir.path().join("d")).unwrap();
|
||||
std::fs::rename(dir.path().join("a/file2"), dir.path().join("a/file2.new")).unwrap();
|
||||
tree.update(&mut app, |tree, ctx| tree.next_scan_complete(ctx))
|
||||
.await;
|
||||
|
||||
app.read(|ctx| {
|
||||
assert_eq!(
|
||||
tree.read(ctx)
|
||||
.paths()
|
||||
.map(|p| p.to_str().unwrap())
|
||||
.collect::<Vec<_>>(),
|
||||
vec![
|
||||
"a",
|
||||
"a/file1",
|
||||
"a/file2.new",
|
||||
"b",
|
||||
"d",
|
||||
"d/file3",
|
||||
"d/file4"
|
||||
]
|
||||
);
|
||||
|
||||
assert_eq!(file2.path().to_str().unwrap(), "a/file2.new");
|
||||
assert_eq!(file4.path().as_ref(), Path::new("d/file4"));
|
||||
assert_eq!(file5.path().as_ref(), Path::new("d/file5"));
|
||||
assert!(!file2.is_deleted());
|
||||
assert!(!file4.is_deleted());
|
||||
assert!(file5.is_deleted());
|
||||
|
||||
// Right now, this rename isn't detected because the target path
|
||||
// no longer exists on the file system by the time we process the
|
||||
// rename event.
|
||||
assert_eq!(file3.path().as_ref(), Path::new("a/file3"));
|
||||
assert!(file3.is_deleted());
|
||||
});
|
||||
// Right now, this rename isn't detected because the target path
|
||||
// no longer exists on the file system by the time we process the
|
||||
// rename event.
|
||||
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 {
|
||||
let dir = temp_tree(json!({
|
||||
".git": {},
|
||||
".gitignore": "ignored-dir\n",
|
||||
"tracked-dir": {
|
||||
"tracked-file1": "tracked contents",
|
||||
},
|
||||
"ignored-dir": {
|
||||
"ignored-file1": "ignored contents",
|
||||
}
|
||||
}));
|
||||
#[gpui::test]
|
||||
async fn test_rescan_with_gitignore(mut app: gpui::TestAppContext) {
|
||||
let dir = temp_tree(json!({
|
||||
".git": {},
|
||||
".gitignore": "ignored-dir\n",
|
||||
"tracked-dir": {
|
||||
"tracked-file1": "tracked contents",
|
||||
},
|
||||
"ignored-dir": {
|
||||
"ignored-file1": "ignored contents",
|
||||
}
|
||||
}));
|
||||
|
||||
let tree = app.add_model(|ctx| Worktree::new(dir.path(), ctx));
|
||||
app.read(|ctx| tree.read(ctx).scan_complete()).await;
|
||||
tree.flush_fs_events(&app).await;
|
||||
app.read(|ctx| {
|
||||
let tree = tree.read(ctx);
|
||||
let tracked = tree.entry_for_path("tracked-dir/tracked-file1").unwrap();
|
||||
let ignored = tree.entry_for_path("ignored-dir/ignored-file1").unwrap();
|
||||
assert_eq!(tracked.is_ignored(), false);
|
||||
assert_eq!(ignored.is_ignored(), true);
|
||||
});
|
||||
let tree = app.add_model(|ctx| Worktree::new(dir.path(), ctx));
|
||||
app.read(|ctx| tree.read(ctx).scan_complete()).await;
|
||||
tree.flush_fs_events(&app).await;
|
||||
app.read(|ctx| {
|
||||
let tree = tree.read(ctx);
|
||||
let tracked = tree.entry_for_path("tracked-dir/tracked-file1").unwrap();
|
||||
let ignored = tree.entry_for_path("ignored-dir/ignored-file1").unwrap();
|
||||
assert_eq!(tracked.is_ignored(), false);
|
||||
assert_eq!(ignored.is_ignored(), true);
|
||||
});
|
||||
|
||||
fs::write(dir.path().join("tracked-dir/tracked-file2"), "").unwrap();
|
||||
fs::write(dir.path().join("ignored-dir/ignored-file2"), "").unwrap();
|
||||
tree.update(&mut app, |tree, ctx| tree.next_scan_complete(ctx))
|
||||
.await;
|
||||
app.read(|ctx| {
|
||||
let tree = tree.read(ctx);
|
||||
let dot_git = tree.entry_for_path(".git").unwrap();
|
||||
let tracked = tree.entry_for_path("tracked-dir/tracked-file2").unwrap();
|
||||
let ignored = tree.entry_for_path("ignored-dir/ignored-file2").unwrap();
|
||||
assert_eq!(tracked.is_ignored(), false);
|
||||
assert_eq!(ignored.is_ignored(), true);
|
||||
assert_eq!(dot_git.is_ignored(), true);
|
||||
});
|
||||
fs::write(dir.path().join("tracked-dir/tracked-file2"), "").unwrap();
|
||||
fs::write(dir.path().join("ignored-dir/ignored-file2"), "").unwrap();
|
||||
tree.update(&mut app, |tree, ctx| tree.next_scan_complete(ctx))
|
||||
.await;
|
||||
app.read(|ctx| {
|
||||
let tree = tree.read(ctx);
|
||||
let dot_git = tree.entry_for_path(".git").unwrap();
|
||||
let tracked = tree.entry_for_path("tracked-dir/tracked-file2").unwrap();
|
||||
let ignored = tree.entry_for_path("ignored-dir/ignored-file2").unwrap();
|
||||
assert_eq!(tracked.is_ignored(), false);
|
||||
assert_eq!(ignored.is_ignored(), true);
|
||||
assert_eq!(dot_git.is_ignored(), true);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user