Use gpui::test in async tests in zed

Co-Authored-By: Max Brunsfeld <max@zed.dev>
This commit is contained in:
Antonio Scandurra
2021-05-12 18:20:49 +02:00
co-authored by Max Brunsfeld
parent 1c2b381f74
commit ab04d71508
3 changed files with 691 additions and 724 deletions
+192 -204
View File
@@ -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);
});
}
}
+293 -302
View File
@@ -729,7 +729,6 @@ 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;
@@ -793,332 +792,324 @@ mod tests {
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
View File
@@ -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);
});
}