Initial public release of Warp.

Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
David Stern
2026-04-28 08:43:33 -05:00
commit 0dbd3d567a
4982 changed files with 1431549 additions and 0 deletions
+987
View File
@@ -0,0 +1,987 @@
use std::sync::Arc;
use repo_metadata::entry::{DirectoryEntry, Entry, FileMetadata};
use repo_metadata::file_tree_store::FileTreeState;
use repo_metadata::local_model::IndexedRepoState;
use repo_metadata::repositories::DetectedRepositories;
use repo_metadata::watcher::DirectoryWatcher;
use repo_metadata::RepoMetadataModel;
use virtual_fs::{Stub, VirtualFS};
use warp_core::ui::appearance::Appearance;
use warpui::{platform::WindowStyle, App, ModelHandle};
use crate::auth::AuthStateProvider;
use crate::server::server_api::{team::MockTeamClient, workspace::MockWorkspaceClient};
use crate::settings_view::keybindings::KeybindingChangedNotifier;
use crate::test_util::settings::initialize_settings_for_tests;
use crate::vim_registers::VimRegisters;
use crate::workspace::sync_inputs::SyncedInputState;
use crate::workspace::ToastStack;
use crate::workspaces::user_workspaces::UserWorkspaces;
use super::FileTreeView;
fn std_path(path: &std::path::Path) -> warp_util::standardized_path::StandardizedPath {
warp_util::standardized_path::StandardizedPath::try_from_local(path).unwrap()
}
fn initialize_app(
app: &mut App,
) -> (
ModelHandle<DetectedRepositories>,
ModelHandle<RepoMetadataModel>,
) {
initialize_settings_for_tests(app);
app.add_singleton_model(|_| Appearance::mock());
app.add_singleton_model(|_| ToastStack);
app.add_singleton_model(|_| SyncedInputState::mock());
app.add_singleton_model(|_| VimRegisters::new());
app.add_singleton_model(|_| KeybindingChangedNotifier::mock());
app.add_singleton_model(|_| AuthStateProvider::new_for_test());
let team_client = Arc::new(MockTeamClient::new());
let workspace_client = Arc::new(MockWorkspaceClient::new());
app.add_singleton_model(|ctx| {
UserWorkspaces::mock(team_client.clone(), workspace_client.clone(), vec![], ctx)
});
let detected_repositories = app.add_singleton_model(|_| DetectedRepositories::default());
let repository_metadata_model = app.add_singleton_model(RepoMetadataModel::new);
(detected_repositories, repository_metadata_model)
}
fn build_repo_state(repo_root: &std::path::Path) -> FileTreeState {
let source_file = Entry::File(FileMetadata::new(
repo_root.join("packages/app/src/main.rs"),
false,
));
let src_dir = Entry::Directory(DirectoryEntry {
path: warp_util::standardized_path::StandardizedPath::try_from_local(
&repo_root.join("packages/app/src"),
)
.unwrap(),
children: vec![source_file],
ignored: false,
loaded: true,
});
let app_dir = Entry::Directory(DirectoryEntry {
path: warp_util::standardized_path::StandardizedPath::try_from_local(
&repo_root.join("packages/app"),
)
.unwrap(),
children: vec![src_dir],
ignored: false,
loaded: true,
});
let packages_dir = Entry::Directory(DirectoryEntry {
path: warp_util::standardized_path::StandardizedPath::try_from_local(
&repo_root.join("packages"),
)
.unwrap(),
children: vec![app_dir],
ignored: false,
loaded: true,
});
let root = Entry::Directory(DirectoryEntry {
path: std_path(repo_root),
children: vec![packages_dir],
ignored: false,
loaded: true,
});
FileTreeState::new(root, vec![], None)
}
fn build_repo_state_with_unloaded_directory(repo_root: &std::path::Path) -> FileTreeState {
let unloaded_src_dir = Entry::Directory(DirectoryEntry {
path: warp_util::standardized_path::StandardizedPath::try_from_local(
&repo_root.join("src"),
)
.unwrap(),
children: vec![],
ignored: false,
loaded: false,
});
let root = Entry::Directory(DirectoryEntry {
path: std_path(repo_root),
children: vec![unloaded_src_dir],
ignored: false,
loaded: true,
});
FileTreeState::new(root, vec![], None)
}
#[test]
fn repo_transition_unregisters_lazy_loaded_path() {
VirtualFS::test("file_tree_repo_transition", |dirs, mut vfs| {
vfs.mkdir("repo/.git/objects")
.mkdir("repo/packages/app/src")
.with_files(vec![
Stub::FileWithContent("repo/.git/HEAD", "ref: refs/heads/main"),
Stub::FileWithContent("repo/.git/config", "[core]\n\trepositoryformatversion = 0"),
Stub::FileWithContent("repo/packages/app/src/main.rs", "fn main() {}\n"),
]);
let repo_root = dirs.tests().join("repo");
let displayed_root = repo_root.join("packages/app");
let canonical_repo_root =
warp_util::standardized_path::StandardizedPath::from_local_canonicalized(&repo_root)
.unwrap();
App::test((), |mut app| async move {
let (detected_repositories, repository_metadata_model) = initialize_app(&mut app);
let (_, file_tree_view) = app.add_window(WindowStyle::NotStealFocus, FileTreeView::new);
detected_repositories.update(&mut app, |repositories, _ctx| {
repositories.insert_test_repo_root(canonical_repo_root.clone());
});
file_tree_view.update(&mut app, |view, ctx| {
view.set_is_active(true, ctx);
view.set_root_directories(vec![displayed_root.clone()], ctx);
});
file_tree_view.read(&app, |view, _ctx| {
assert!(view.registered_lazy_loaded_paths.contains(
&warp_util::standardized_path::StandardizedPath::try_from_local(
&displayed_root
)
.unwrap()
));
let displayed_std =
warp_util::standardized_path::StandardizedPath::try_from_local(&displayed_root)
.unwrap();
assert_eq!(
view.root_directories
.get(&displayed_std)
.map(|root_dir| root_dir.entry.root_directory().to_local_path_lossy()),
Some(displayed_root.clone())
);
});
repository_metadata_model.read(&app, |model, ctx| {
assert!(model.is_lazy_loaded_path(
&warp_util::standardized_path::StandardizedPath::try_from_local(
&displayed_root
)
.unwrap(),
ctx
));
});
repository_metadata_model.update(&mut app, |model, ctx| {
model.insert_test_state(canonical_repo_root, build_repo_state(&repo_root), ctx);
});
file_tree_view.update(&mut app, |view, ctx| {
view.set_root_directories(vec![displayed_root.clone()], ctx);
});
file_tree_view.read(&app, |view, _ctx| {
let displayed_std =
warp_util::standardized_path::StandardizedPath::try_from_local(&displayed_root)
.unwrap();
let repo_std =
warp_util::standardized_path::StandardizedPath::try_from_local(&repo_root)
.unwrap();
assert!(!view.registered_lazy_loaded_paths.contains(&displayed_std));
assert_eq!(view.root_for_path(&displayed_std), Some(repo_std.clone()));
assert_eq!(
view.root_directories
.get(&displayed_std)
.map(|root_dir| (**root_dir.entry.root_directory()).clone()),
Some(repo_std)
);
});
repository_metadata_model.read(&app, |model, ctx| {
assert!(!model.is_lazy_loaded_path(
&warp_util::standardized_path::StandardizedPath::try_from_local(
&displayed_root
)
.unwrap(),
ctx
));
});
});
});
}
#[test]
fn repo_backed_unloaded_directory_loads_through_model() {
VirtualFS::test("file_tree_repo_backed_load", |dirs, mut vfs| {
vfs.mkdir("repo/.git/objects")
.mkdir("repo/src/nested")
.with_files(vec![
Stub::FileWithContent("repo/.git/HEAD", "ref: refs/heads/main"),
Stub::FileWithContent(
"repo/.git/config",
"[core]
\trepositoryformatversion = 0",
),
Stub::FileWithContent(
"repo/src/nested/main.rs",
"fn main() {}
",
),
]);
let repo_root = dirs.tests().join("repo");
let src_dir = repo_root.join("src");
let nested_dir = repo_root.join("src/nested");
let source_file = repo_root.join("src/nested/main.rs");
let canonical_repo_root =
warp_util::standardized_path::StandardizedPath::from_local_canonicalized(&repo_root)
.unwrap();
App::test((), |mut app| async move {
let (detected_repositories, repository_metadata_model) = initialize_app(&mut app);
let (_, file_tree_view) = app.add_window(WindowStyle::NotStealFocus, FileTreeView::new);
detected_repositories.update(&mut app, |repositories, _ctx| {
repositories.insert_test_repo_root(canonical_repo_root.clone());
});
repository_metadata_model.update(&mut app, |model, ctx| {
model.insert_test_state(
canonical_repo_root,
build_repo_state_with_unloaded_directory(&repo_root),
ctx,
);
});
file_tree_view.update(&mut app, |view, ctx| {
view.set_is_active(true, ctx);
view.set_root_directories(vec![repo_root.clone()], ctx);
});
file_tree_view.read(&app, |view, _ctx| {
assert!(!view
.root_directories
.get(
&warp_util::standardized_path::StandardizedPath::try_from_local(&repo_root)
.unwrap()
)
.is_some_and(|root_dir| root_dir.entry.contains(
&warp_util::standardized_path::StandardizedPath::try_from_local(
&source_file
)
.unwrap()
)));
});
file_tree_view.update(&mut app, |view, ctx| {
view.ensure_loaded_path(
&warp_util::standardized_path::StandardizedPath::try_from_local(&repo_root)
.unwrap(),
&warp_util::standardized_path::StandardizedPath::try_from_local(&src_dir)
.unwrap(),
ctx,
);
});
file_tree_view.read(&app, |view, _ctx| {
assert!(view
.root_directories
.get(
&warp_util::standardized_path::StandardizedPath::try_from_local(&repo_root)
.unwrap()
)
.is_some_and(|root_dir| root_dir.entry.contains(
&warp_util::standardized_path::StandardizedPath::try_from_local(
&nested_dir
)
.unwrap()
)));
});
file_tree_view.update(&mut app, |view, ctx| {
view.ensure_loaded_path(
&warp_util::standardized_path::StandardizedPath::try_from_local(&repo_root)
.unwrap(),
&warp_util::standardized_path::StandardizedPath::try_from_local(&nested_dir)
.unwrap(),
ctx,
);
});
file_tree_view.read(&app, |view, _ctx| {
assert!(view
.root_directories
.get(
&warp_util::standardized_path::StandardizedPath::try_from_local(&repo_root)
.unwrap()
)
.is_some_and(|root_dir| root_dir.entry.contains(
&warp_util::standardized_path::StandardizedPath::try_from_local(
&source_file
)
.unwrap()
)));
});
repository_metadata_model.read(&app, |model, ctx| {
assert!(!model.is_lazy_loaded_path(
&warp_util::standardized_path::StandardizedPath::try_from_local(&repo_root)
.unwrap(),
ctx
));
let id = repo_metadata::RepositoryIdentifier::local(
warp_util::standardized_path::StandardizedPath::try_from_local(&repo_root)
.unwrap(),
);
assert!(model.get_repository(&id, ctx).is_some_and(|state| {
state.entry.contains(
&warp_util::standardized_path::StandardizedPath::try_from_local(
&source_file,
)
.unwrap(),
)
}));
});
});
});
}
#[test]
fn pending_repository_root_does_not_register_lazy_loaded_path() {
VirtualFS::test("file_tree_pending_repo_root", |dirs, mut vfs| {
vfs.mkdir("repo/.git/objects").with_files(vec![
Stub::FileWithContent("repo/.git/HEAD", "ref: refs/heads/main"),
Stub::FileWithContent("repo/.git/config", "[core]\n\trepositoryformatversion = 0"),
]);
let repo_root = dirs.tests().join("repo");
let canonical_repo_root =
warp_util::standardized_path::StandardizedPath::from_local_canonicalized(&repo_root)
.unwrap();
App::test((), |mut app| async move {
let (detected_repositories, repository_metadata_model) = initialize_app(&mut app);
let directory_watcher = app.add_singleton_model(DirectoryWatcher::new);
let (_, file_tree_view) = app.add_window(WindowStyle::NotStealFocus, FileTreeView::new);
let repository_handle = directory_watcher.update(&mut app, |watcher, ctx| {
watcher
.add_directory(canonical_repo_root.clone(), ctx)
.unwrap()
});
detected_repositories.update(&mut app, |repositories, _ctx| {
repositories.insert_test_repo_root(canonical_repo_root.clone());
});
repository_metadata_model.update(&mut app, |model, ctx| {
model.index_directory(repository_handle, ctx).unwrap();
});
repository_metadata_model.read(&app, |model, ctx| {
let id = repo_metadata::RepositoryIdentifier::local(
warp_util::standardized_path::StandardizedPath::try_from_local(&repo_root)
.unwrap(),
);
assert!(matches!(
model.repository_state(&id, ctx),
Some(IndexedRepoState::Pending)
));
});
file_tree_view.update(&mut app, |view, ctx| {
view.set_is_active(true, ctx);
view.set_root_directories(vec![repo_root.clone()], ctx);
});
file_tree_view.read(&app, |view, _ctx| {
assert!(!view.registered_lazy_loaded_paths.contains(
&warp_util::standardized_path::StandardizedPath::try_from_local(&repo_root)
.unwrap()
));
});
repository_metadata_model.read(&app, |model, ctx| {
assert!(!model.is_lazy_loaded_path(
&warp_util::standardized_path::StandardizedPath::try_from_local(&repo_root)
.unwrap(),
ctx
));
let id = repo_metadata::RepositoryIdentifier::local(
warp_util::standardized_path::StandardizedPath::try_from_local(&repo_root)
.unwrap(),
);
assert!(matches!(
model.repository_state(&id, ctx),
Some(IndexedRepoState::Pending)
));
});
});
});
}
#[test]
fn failed_lazy_loaded_path_registration_is_retried() {
VirtualFS::test("file_tree_lazy_loaded_path_retry", |dirs, mut vfs| {
let displayed_root = dirs.tests().join("late_dir");
App::test((), |mut app| async move {
let (_detected_repositories, repository_metadata_model) = initialize_app(&mut app);
let (_, file_tree_view) = app.add_window(WindowStyle::NotStealFocus, FileTreeView::new);
file_tree_view.update(&mut app, |view, ctx| {
view.set_is_active(true, ctx);
view.set_root_directories(vec![displayed_root.clone()], ctx);
});
file_tree_view.read(&app, |view, _ctx| {
assert!(!view.registered_lazy_loaded_paths.contains(
&warp_util::standardized_path::StandardizedPath::try_from_local(
&displayed_root
)
.unwrap()
));
});
repository_metadata_model.read(&app, |model, ctx| {
assert!(!model.is_lazy_loaded_path(
&warp_util::standardized_path::StandardizedPath::try_from_local(
&displayed_root
)
.unwrap(),
ctx
));
});
vfs.mkdir("late_dir")
.with_files(vec![Stub::FileWithContent("late_dir/file.txt", "content")]);
file_tree_view.update(&mut app, |view, ctx| {
view.set_root_directories(vec![displayed_root.clone()], ctx);
});
file_tree_view.read(&app, |view, _ctx| {
assert!(view.registered_lazy_loaded_paths.contains(&std_path(&displayed_root)));
assert!(matches!(
view.root_directories.get(&std_path(&displayed_root)).map(|root_dir| &root_dir.entry),
Some(entry)
if entry.contains(&std_path(&displayed_root.join("file.txt")))
));
});
repository_metadata_model.read(&app, |model, ctx| {
assert!(model.is_lazy_loaded_path(
&warp_util::standardized_path::StandardizedPath::try_from_local(
&displayed_root
)
.unwrap(),
ctx
));
});
});
});
}
// ── Ancestor grouping (APP-4106) ────────────────────────────────────
#[test]
fn sibling_roots_are_preserved() {
VirtualFS::test("file_tree_sibling_roots", |dirs, mut vfs| {
vfs.mkdir("tree/a").mkdir("tree/b").with_files(vec![
Stub::FileWithContent("tree/a/x.txt", "x"),
Stub::FileWithContent("tree/b/y.txt", "y"),
]);
let a = dirs.tests().join("tree/a");
let b = dirs.tests().join("tree/b");
App::test((), |mut app| async move {
let _ = initialize_app(&mut app);
let (_, file_tree_view) = app.add_window(WindowStyle::NotStealFocus, FileTreeView::new);
file_tree_view.update(&mut app, |view, ctx| {
view.set_is_active(true, ctx);
view.set_root_directories(vec![a.clone(), b.clone()], ctx);
});
file_tree_view.read(&app, |view, _ctx| {
assert_eq!(view.displayed_directories, vec![std_path(&a), std_path(&b)]);
});
});
});
}
#[test]
fn auto_expand_overrides_selection_when_most_recent_root_changes() {
VirtualFS::test(
"file_tree_auto_expand_overrides_on_new_root",
|dirs, mut vfs| {
vfs.mkdir("code/foo").mkdir("other").with_files(vec![
Stub::FileWithContent("code/foo/file.txt", "x"),
Stub::FileWithContent("other/file.txt", "y"),
]);
let code = dirs.tests().join("code");
let other = dirs.tests().join("other");
App::test((), |mut app| async move {
let _ = initialize_app(&mut app);
let (_, file_tree_view) =
app.add_window(WindowStyle::NotStealFocus, FileTreeView::new);
// Start with `code` as the only root and select its header.
file_tree_view.update(&mut app, |view, ctx| {
view.set_is_active(true, ctx);
view.set_root_directories(vec![code.clone()], ctx);
view.auto_expand_to_most_recent_directory(ctx);
});
file_tree_view.read(&app, |view, _ctx| {
let selected = view.selected_item.as_ref().unwrap();
assert_eq!(selected.root, std_path(&code));
});
// Now cd to a brand-new root. `other` becomes most-recent.
// Selection must move to `other`, not stay on `code`.
file_tree_view.update(&mut app, |view, ctx| {
view.set_root_directories(vec![other.clone(), code.clone()], ctx);
view.auto_expand_to_most_recent_directory(ctx);
});
file_tree_view.read(&app, |view, _ctx| {
let selected = view.selected_item.as_ref().expect("selection set");
assert_eq!(selected.root, std_path(&other));
});
});
},
);
}
#[test]
fn auto_expand_preserves_existing_selection() {
VirtualFS::test(
"file_tree_auto_expand_preserves_selection",
|dirs, mut vfs| {
vfs.mkdir("tree/sub")
.with_files(vec![Stub::FileWithContent("tree/sub/file.txt", "content")]);
let tree = dirs.tests().join("tree");
let sub = tree.join("sub");
App::test((), |mut app| async move {
let _ = initialize_app(&mut app);
let (_, file_tree_view) =
app.add_window(WindowStyle::NotStealFocus, FileTreeView::new);
file_tree_view.update(&mut app, |view, ctx| {
view.set_is_active(true, ctx);
view.set_root_directories(vec![tree.clone()], ctx);
});
// Simulate a prior explicit selection (e.g. user focused a
// file in the code editor and `scroll_to_file` selected it).
file_tree_view.update(&mut app, |view, ctx| {
view.toggle_folder_expansion(&std_path(&tree), &std_path(&sub), ctx);
let root_dir = view.root_directories.get(&std_path(&tree)).unwrap();
let (index, _) = root_dir
.items
.iter()
.enumerate()
.find(|(_, item)| item.path() == &std_path(&sub))
.expect("sub directory is flattened");
let id = super::FileTreeIdentifier {
root: std_path(&tree),
index,
};
view.select_id(&id, ctx);
});
// Auto-expand must not override that selection with the root header.
file_tree_view.update(&mut app, |view, ctx| {
view.auto_expand_to_most_recent_directory(ctx);
});
file_tree_view.read(&app, |view, _ctx| {
let selected = view.selected_item.clone().expect("selection set");
let root_dir = view.root_directories.get(&std_path(&tree)).unwrap();
let selected_path = root_dir.items.get(selected.index).unwrap().path();
assert_eq!(selected_path, &std_path(&sub));
});
});
},
);
}
#[test]
fn click_on_file_under_absorbed_descendant_keeps_file_selected() {
// Simulates: user clicks a file in the tree. The code view opens it,
// which causes `DirectoriesChanged` to fire with the file's
// parent/repo added. The resulting `set_root_directories` must NOT
// override the user's file selection with the cwd-follow parent.
VirtualFS::test(
"file_tree_click_file_preserves_selection",
|dirs, mut vfs| {
vfs.mkdir("code/warp-server")
.with_files(vec![Stub::FileWithContent(
"code/warp-server/main.rs",
"fn main() {}\n",
)]);
let code = dirs.tests().join("code");
let warp_server = code.join("warp-server");
let main_rs = warp_server.join("main.rs");
App::test((), |mut app| async move {
let _ = initialize_app(&mut app);
let (_, file_tree_view) =
app.add_window(WindowStyle::NotStealFocus, FileTreeView::new);
// Seed with `code` as the only root and expand warp-server so
// main.rs is materialized in the flattened items.
file_tree_view.update(&mut app, |view, ctx| {
view.set_is_active(true, ctx);
view.set_root_directories(vec![code.clone()], ctx);
view.toggle_folder_expansion(&std_path(&code), &std_path(&warp_server), ctx);
});
// Simulate a click on main.rs (select_id is what the click
// action and the active-file scroll both go through).
file_tree_view.update(&mut app, |view, ctx| {
let root_dir = view.root_directories.get(&std_path(&code)).unwrap();
let (index, _) = root_dir
.items
.iter()
.enumerate()
.find(|(_, item)| item.path() == &std_path(&main_rs))
.expect("main.rs materialized");
let id = super::FileTreeIdentifier {
root: std_path(&code),
index,
};
view.select_id(&id, ctx);
});
// Now `DirectoriesChanged` fires as a side effect of the file
// opening in a code view — the working-directories-model adds
// the file's repo/parent (warp-server) to the active set.
file_tree_view.update(&mut app, |view, ctx| {
view.set_root_directories(vec![warp_server.clone(), code.clone()], ctx);
});
file_tree_view.read(&app, |view, _ctx| {
// Selection is still on main.rs, not on warp-server.
let selected = view.selected_item.clone().expect("selection");
let root_dir = view.root_directories.get(&std_path(&code)).unwrap();
let path = root_dir.items.get(selected.index).unwrap().path();
assert_eq!(path, &std_path(&main_rs));
// And we didn't set a pending focus target that could
// later steal focus back to the parent directory.
assert!(view.pending_focus_target.is_none());
});
});
},
);
}
#[test]
fn pending_focus_target_does_not_re_scroll_after_first_apply() {
// After the initial focus-follow scrolls to the cwd, subsequent
// rebuilds (e.g. from repo-metadata updates) must keep the
// selection but NOT re-scroll, so user scrolling is respected.
VirtualFS::test("file_tree_pending_respects_user_scroll", |dirs, mut vfs| {
vfs.mkdir("tree/warp-server")
.with_files(vec![Stub::FileWithContent(
"tree/warp-server/main.rs",
"fn main() {}\n",
)]);
let tree = dirs.tests().join("tree");
let warp_server = tree.join("warp-server");
App::test((), |mut app| async move {
let _ = initialize_app(&mut app);
let (_, file_tree_view) = app.add_window(WindowStyle::NotStealFocus, FileTreeView::new);
file_tree_view.update(&mut app, |view, ctx| {
view.set_is_active(true, ctx);
view.set_root_directories(vec![warp_server.clone(), tree.clone()], ctx);
});
// Initial apply should have scrolled once.
file_tree_view.read(&app, |view, _ctx| {
let pending = view.pending_focus_target.as_ref().expect("pending");
assert!(pending.scrolled);
});
// Simulate a later rebuild (e.g. metadata update). Selection
// should still land on warp-server, but `scrolled` must stay
// true (no re-scroll).
file_tree_view.update(&mut app, |view, _ctx| {
view.rebuild_flattened_items();
view.apply_pending_focus_target();
});
file_tree_view.read(&app, |view, _ctx| {
let selected = view.selected_item.clone().expect("selection");
let root_dir = view.root_directories.get(&std_path(&tree)).unwrap();
let path = root_dir.items.get(selected.index).unwrap().path();
assert_eq!(path, &std_path(&warp_server));
let pending = view.pending_focus_target.as_ref().expect("pending");
assert!(pending.scrolled, "scrolled flag stays set after re-apply");
});
});
});
}
#[test]
fn focus_follows_absorbed_descendant_once_its_item_is_materialized() {
VirtualFS::test("file_tree_focus_follow_deferred", |dirs, mut vfs| {
vfs.mkdir("tree/warp-server")
.with_files(vec![Stub::FileWithContent(
"tree/warp-server/main.rs",
"fn main() {}\n",
)]);
let tree = dirs.tests().join("tree");
let warp_server = tree.join("warp-server");
App::test((), |mut app| async move {
let _ = initialize_app(&mut app);
let (_, file_tree_view) = app.add_window(WindowStyle::NotStealFocus, FileTreeView::new);
// User cd's into warp-server with ~/tree as the ancestor root.
// The warp-server entry should be materialized by indexing and
// selected as the focus-follow target.
file_tree_view.update(&mut app, |view, ctx| {
view.set_is_active(true, ctx);
view.set_root_directories(vec![warp_server.clone(), tree.clone()], ctx);
});
file_tree_view.read(&app, |view, _ctx| {
// Single displayed root, descendant absorbed.
assert_eq!(view.displayed_directories, vec![std_path(&tree)]);
// Selection landed on warp-server's directory header.
let selected = view.selected_item.clone().expect("selection set");
assert_eq!(selected.root, std_path(&tree));
let root_dir = view.root_directories.get(&std_path(&tree)).unwrap();
let selected_item = root_dir
.items
.get(selected.index)
.expect("selected index in range");
assert_eq!(selected_item.path(), &std_path(&warp_server));
// Pending target is preserved across rebuilds so later
// repo-metadata updates don't override the cwd-follow
// selection. It clears when the user interacts explicitly
// (see pending_focus_target_cleared_on_user_select).
let pending = view
.pending_focus_target
.as_ref()
.expect("pending target preserved");
assert_eq!(pending.root, std_path(&tree));
assert_eq!(pending.path, std_path(&warp_server));
// The initial apply scrolled; later applies must not
// re-scroll so user scrolling is respected.
assert!(pending.scrolled, "initial apply scrolls the tree");
});
// User clicks somewhere else (simulated via select_id). Pending
// target must clear so future rebuilds don't re-steal focus.
file_tree_view.update(&mut app, |view, ctx| {
let root_dir = view.root_directories.get(&std_path(&tree)).unwrap();
let id = super::FileTreeIdentifier {
root: std_path(&tree),
index: 0,
};
// Sanity: the first item is the root header, not warp-server.
assert_ne!(
root_dir.items.first().unwrap().path(),
&std_path(&warp_server)
);
view.select_id(&id, ctx);
});
file_tree_view.read(&app, |view, _ctx| {
assert!(view.pending_focus_target.is_none());
});
});
});
}
#[test]
fn descendant_is_absorbed_into_ancestor() {
VirtualFS::test("file_tree_absorb_descendant", |dirs, mut vfs| {
vfs.mkdir("tree/a")
.with_files(vec![Stub::FileWithContent("tree/a/x.txt", "x")]);
let tree = dirs.tests().join("tree");
let a = tree.join("a");
App::test((), |mut app| async move {
let _ = initialize_app(&mut app);
let (_, file_tree_view) = app.add_window(WindowStyle::NotStealFocus, FileTreeView::new);
file_tree_view.update(&mut app, |view, ctx| {
view.set_is_active(true, ctx);
// Input in most-recent-first order: descendant first.
view.set_root_directories(vec![a.clone(), tree.clone()], ctx);
});
file_tree_view.read(&app, |view, _ctx| {
// Only the ancestor survives as a displayed root.
assert_eq!(view.displayed_directories, vec![std_path(&tree)]);
assert!(view.root_directories.contains_key(&std_path(&tree)));
assert!(!view.root_directories.contains_key(&std_path(&a)));
// The absorbed descendant is expanded inside the surviving root.
let root_dir = view.root_directories.get(&std_path(&tree)).unwrap();
assert!(root_dir.expanded_folders.contains(&std_path(&a)));
});
});
});
}
#[test]
fn cd_into_descendant_absorbs_into_existing_ancestor_root() {
VirtualFS::test("file_tree_cd_into_descendant", |dirs, mut vfs| {
vfs.mkdir("tree/a/z")
.with_files(vec![Stub::FileWithContent("tree/a/z/file.txt", "f")]);
let tree = dirs.tests().join("tree");
let z = tree.join("a/z");
App::test((), |mut app| async move {
let _ = initialize_app(&mut app);
let (_, file_tree_view) = app.add_window(WindowStyle::NotStealFocus, FileTreeView::new);
// Start with only the ancestor displayed.
file_tree_view.update(&mut app, |view, ctx| {
view.set_is_active(true, ctx);
view.set_root_directories(vec![tree.clone()], ctx);
});
// Simulate cd-ing into ~/tree/a/z by emitting the descendant as the
// most-recent path.
file_tree_view.update(&mut app, |view, ctx| {
view.set_root_directories(vec![z.clone(), tree.clone()], ctx);
});
file_tree_view.read(&app, |view, _ctx| {
// Still a single root, no new top-level entry.
assert_eq!(view.displayed_directories, vec![std_path(&tree)]);
// Ancestor chain is auto-expanded down to the cwd.
let root_dir = view.root_directories.get(&std_path(&tree)).unwrap();
assert!(root_dir
.expanded_folders
.contains(&std_path(&tree.join("a"))));
assert!(root_dir.expanded_folders.contains(&std_path(&z)));
});
});
});
}
#[test]
fn explicit_collapse_blocks_auto_expand_on_absorption() {
VirtualFS::test("file_tree_collapse_blocks_expand", |dirs, mut vfs| {
vfs.mkdir("tree/a/z")
.with_files(vec![Stub::FileWithContent("tree/a/z/file.txt", "f")]);
let tree = dirs.tests().join("tree");
let a = tree.join("a");
let z = a.join("z");
App::test((), |mut app| async move {
let _ = initialize_app(&mut app);
let (_, file_tree_view) = app.add_window(WindowStyle::NotStealFocus, FileTreeView::new);
// Start with the ancestor displayed and explicitly collapse `a`.
file_tree_view.update(&mut app, |view, ctx| {
view.set_is_active(true, ctx);
view.set_root_directories(vec![tree.clone()], ctx);
// First expand so the toggle records a collapse.
view.toggle_folder_expansion(&std_path(&tree), &std_path(&a), ctx);
view.toggle_folder_expansion(&std_path(&tree), &std_path(&a), ctx);
});
file_tree_view.read(&app, |view, _ctx| {
assert!(view.is_explicitly_collapsed(&std_path(&tree), &std_path(&a)));
});
// Now cd into ~/tree/a/z. Auto-expansion must not re-open `a`.
file_tree_view.update(&mut app, |view, ctx| {
view.set_root_directories(vec![z.clone(), tree.clone()], ctx);
});
file_tree_view.read(&app, |view, _ctx| {
let root_dir = view.root_directories.get(&std_path(&tree)).unwrap();
assert!(!root_dir.expanded_folders.contains(&std_path(&a)));
assert!(!root_dir.expanded_folders.contains(&std_path(&z)));
assert!(view.is_explicitly_collapsed(&std_path(&tree), &std_path(&a)));
});
});
});
}
#[test]
fn absorption_migrates_expanded_and_explicitly_collapsed_state() {
VirtualFS::test("file_tree_absorb_migrates_state", |dirs, mut vfs| {
vfs.mkdir("tree/a/z")
.with_files(vec![Stub::FileWithContent("tree/a/z/file.txt", "f")]);
let tree = dirs.tests().join("tree");
let a = tree.join("a");
let z = a.join("z");
App::test((), |mut app| async move {
let _ = initialize_app(&mut app);
let (_, file_tree_view) = app.add_window(WindowStyle::NotStealFocus, FileTreeView::new);
// Start with `a` as a standalone top-level root and record
// an explicit collapse on `a/z` under that standalone root.
file_tree_view.update(&mut app, |view, ctx| {
view.set_is_active(true, ctx);
view.set_root_directories(vec![a.clone()], ctx);
// Expand then collapse z so the toggle records a collapse on it.
view.toggle_folder_expansion(&std_path(&a), &std_path(&z), ctx);
view.toggle_folder_expansion(&std_path(&a), &std_path(&z), ctx);
});
file_tree_view.read(&app, |view, _ctx| {
assert!(view.is_explicitly_collapsed(&std_path(&a), &std_path(&z)));
});
// Now absorb `a` into `tree` by adding the ancestor.
file_tree_view.update(&mut app, |view, ctx| {
view.set_root_directories(vec![a.clone(), tree.clone()], ctx);
});
file_tree_view.read(&app, |view, _ctx| {
// Standalone absorbed-root entry is gone.
assert!(!view.root_directories.contains_key(&std_path(&a)));
// Its explicit-collapse state moved over to the ancestor.
assert!(view.is_explicitly_collapsed(&std_path(&tree), &std_path(&z)));
});
});
});
}
#[test]
fn absorbed_descendant_is_unregistered_from_lazy_loaded_paths() {
VirtualFS::test("file_tree_absorb_unregisters_lazy", |dirs, mut vfs| {
vfs.mkdir("tree/a")
.with_files(vec![Stub::FileWithContent("tree/a/x.txt", "x")]);
let tree = dirs.tests().join("tree");
let a = tree.join("a");
App::test((), |mut app| async move {
let (_, repository_metadata_model) = initialize_app(&mut app);
let (_, file_tree_view) = app.add_window(WindowStyle::NotStealFocus, FileTreeView::new);
// Initial state: `a` alone is a standalone lazy-loaded root.
file_tree_view.update(&mut app, |view, ctx| {
view.set_is_active(true, ctx);
view.set_root_directories(vec![a.clone()], ctx);
});
file_tree_view.read(&app, |view, _ctx| {
assert!(view.registered_lazy_loaded_paths.contains(&std_path(&a)));
});
repository_metadata_model.read(&app, |model, ctx| {
assert!(model.is_lazy_loaded_path(&std_path(&a), ctx));
});
// Add the ancestor. `a` should be absorbed and its lazy-loaded
// registration should be cleaned up.
file_tree_view.update(&mut app, |view, ctx| {
view.set_root_directories(vec![a.clone(), tree.clone()], ctx);
});
file_tree_view.read(&app, |view, _ctx| {
assert!(!view.registered_lazy_loaded_paths.contains(&std_path(&a)));
assert!(view.registered_lazy_loaded_paths.contains(&std_path(&tree)));
});
repository_metadata_model.read(&app, |model, ctx| {
assert!(!model.is_lazy_loaded_path(&std_path(&a), ctx));
});
});
});
}