Files
zed/crates/remote_server/src/remote_editing_tests.rs
T
Cole Miller ec2c637aaf git: Fix diff hunks not being removed on restore in remote projects (#54823)
Closes #48032 

When restoring a diff hunk, we first unstage it unconditionally. That
unstaging operation is a no-op in terms of the index text if the hunk
was already not staged, but previously we would still always do
`spawn_set_index_text_job` and bump the
`hunk_staging_operation_count_as_of_write`. Bumping that count in turn
causes us to skip a diff recalculation in response to the change in the
buffer's text. That works out fine in the local case, because when the
worktree picks up the write to `.git/index` we kick off another diff
recalculation which is not skipped. But in the remote case, we don't get
an `UpdateDiffBases` proto message if the index text didn't actually
change, so there is no subsequent diff calculation to do the cleanup,
and we end up with a stale no-op hunk.

This PR fixes the issue by skipping the write to the index and the
`hunk_staging_operation_count_as_of_write` bump if the new and old index
texts are the same.

Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Closes #ISSUE

Release Notes:

- Fixed a bug where restoring diff hunks in remote projects would leave
stale no-op hunks in the UI.
2026-05-07 17:40:28 +00:00

2814 lines
80 KiB
Rust

/// todo(windows)
/// The tests in this file assume that server_cx is running on Windows too.
/// We neead to find a way to test Windows-Non-Windows interactions.
use crate::headless_project::HeadlessProject;
use agent::{AgentTool, ReadFileTool, ReadFileToolInput, ToolCallEventStream, ToolInput};
use client::{Client, UserStore};
use clock::FakeSystemClock;
use collections::{HashMap, HashSet};
use language_model::LanguageModelToolResultContent;
use languages::rust_lang;
use extension::ExtensionHostProxy;
use fs::{FakeFs, Fs};
use git::{
Oid,
repository::{CommitData, Worktree as GitWorktree},
};
use gpui::{AppContext as _, Entity, SharedString, TestAppContext};
use http_client::{BlockedHttpClient, FakeHttpClient};
use language::{
Buffer, FakeLspAdapter, LanguageConfig, LanguageMatcher, LanguageRegistry, LineEnding,
language_settings::{AllLanguageSettings, LanguageSettings},
};
use lsp::{
CompletionContext, CompletionResponse, CompletionTriggerKind, DEFAULT_LSP_REQUEST_TIMEOUT,
LanguageServerName,
};
use node_runtime::NodeRuntime;
use project::{
ProgressToken, Project,
agent_server_store::AgentServerCommand,
search::{SearchQuery, SearchResult},
};
use remote::RemoteClient;
use rpc::proto;
use serde_json::json;
use settings::{Settings, SettingsLocation, SettingsStore, initial_server_settings_content};
use smol::stream::StreamExt;
use std::{
path::{Path, PathBuf},
str::FromStr,
sync::Arc,
};
use unindent::Unindent as _;
use util::{path, paths::PathMatcher, rel_path::rel_path};
#[gpui::test]
async fn test_basic_remote_editing(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
let fs = FakeFs::new(server_cx.executor());
fs.insert_tree(
path!("/code"),
json!({
"project1": {
".git": {},
"README.md": "# project 1",
"src": {
"lib.rs": "fn one() -> usize { 1 }"
}
},
"project2": {
"README.md": "# project 2",
},
}),
)
.await;
fs.set_index_for_repo(
Path::new(path!("/code/project1/.git")),
&[("src/lib.rs", "fn one() -> usize { 0 }".into())],
);
let (project, _headless) = init_test(&fs, cx, server_cx).await;
let (worktree, _) = project
.update(cx, |project, cx| {
project.find_or_create_worktree(path!("/code/project1"), true, cx)
})
.await
.unwrap();
// The client sees the worktree's contents.
cx.executor().run_until_parked();
let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
worktree.update(cx, |worktree, _cx| {
assert_eq!(
worktree.paths().collect::<Vec<_>>(),
vec![
rel_path("README.md"),
rel_path("src"),
rel_path("src/lib.rs"),
]
);
});
// The user opens a buffer in the remote worktree. The buffer's
// contents are loaded from the remote filesystem.
let buffer = project
.update(cx, |project, cx| {
project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
})
.await
.unwrap();
let diff = project
.update(cx, |project, cx| {
project.open_unstaged_diff(buffer.clone(), cx)
})
.await
.unwrap();
diff.update(cx, |diff, cx| {
assert_eq!(
diff.base_text_string(cx).unwrap(),
"fn one() -> usize { 0 }"
);
});
buffer.update(cx, |buffer, cx| {
assert_eq!(buffer.text(), "fn one() -> usize { 1 }");
let ix = buffer.text().find('1').unwrap();
buffer.edit([(ix..ix + 1, "100")], None, cx);
});
// The user saves the buffer. The new contents are written to the
// remote filesystem.
project
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
.await
.unwrap();
assert_eq!(
fs.load("/code/project1/src/lib.rs".as_ref()).await.unwrap(),
"fn one() -> usize { 100 }"
);
// A new file is created in the remote filesystem. The user
// sees the new file.
fs.save(
path!("/code/project1/src/main.rs").as_ref(),
&"fn main() {}".into(),
Default::default(),
)
.await
.unwrap();
cx.executor().run_until_parked();
worktree.update(cx, |worktree, _cx| {
assert_eq!(
worktree.paths().collect::<Vec<_>>(),
vec![
rel_path("README.md"),
rel_path("src"),
rel_path("src/lib.rs"),
rel_path("src/main.rs"),
]
);
});
// A file that is currently open in a buffer is renamed.
fs.rename(
path!("/code/project1/src/lib.rs").as_ref(),
path!("/code/project1/src/lib2.rs").as_ref(),
Default::default(),
)
.await
.unwrap();
cx.executor().run_until_parked();
buffer.update(cx, |buffer, _| {
assert_eq!(&**buffer.file().unwrap().path(), rel_path("src/lib2.rs"));
});
fs.set_index_for_repo(
Path::new(path!("/code/project1/.git")),
&[("src/lib2.rs", "fn one() -> usize { 100 }".into())],
);
cx.executor().run_until_parked();
diff.update(cx, |diff, cx| {
assert_eq!(
diff.base_text_string(cx).unwrap(),
"fn one() -> usize { 100 }"
);
});
}
async fn do_search_and_assert(
project: &Entity<Project>,
query: &str,
files_to_include: PathMatcher,
match_full_paths: bool,
expected_paths: &[&str],
mut cx: TestAppContext,
) -> Vec<Entity<Buffer>> {
let query = query.to_string();
let receiver = project.update(&mut cx, |project, cx| {
project.search(
SearchQuery::text(
query,
false,
true,
false,
files_to_include,
Default::default(),
match_full_paths,
None,
)
.unwrap(),
cx,
)
});
let mut buffers = Vec::new();
for expected_path in expected_paths {
let buffer = loop {
let response = receiver.rx.recv().await.unwrap();
match response {
SearchResult::Buffer { buffer, .. } => break buffer,
SearchResult::LimitReached => panic!("incorrect result"),
SearchResult::WaitingForScan | SearchResult::Searching => continue,
}
};
buffer.update(&mut cx, |buffer, cx| {
assert_eq!(
buffer.file().unwrap().full_path(cx).to_string_lossy(),
*expected_path
)
});
buffers.push(buffer);
}
assert!(receiver.rx.recv().await.is_err());
buffers
}
#[gpui::test]
async fn test_remote_project_search(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
let fs = FakeFs::new(server_cx.executor());
fs.insert_tree(
path!("/code"),
json!({
"project1": {
".git": {},
"README.md": "# project 1",
"src": {
"lib.rs": "fn one() -> usize { 1 }"
}
},
}),
)
.await;
let (project, headless) = init_test(&fs, cx, server_cx).await;
project
.update(cx, |project, cx| {
project.find_or_create_worktree(path!("/code/project1"), true, cx)
})
.await
.unwrap();
cx.run_until_parked();
let buffers = do_search_and_assert(
&project,
"project",
Default::default(),
false,
&[path!("project1/README.md")],
cx.clone(),
)
.await;
let buffer = buffers.into_iter().next().unwrap();
// test that the headless server is tracking which buffers we have open correctly.
cx.run_until_parked();
headless.update(server_cx, |headless, cx| {
assert!(headless.buffer_store.read(cx).has_shared_buffers())
});
do_search_and_assert(
&project,
"project",
Default::default(),
false,
&[path!("project1/README.md")],
cx.clone(),
)
.await;
server_cx.run_until_parked();
cx.update(|_| {
drop(buffer);
});
cx.run_until_parked();
server_cx.run_until_parked();
headless.update(server_cx, |headless, cx| {
assert!(!headless.buffer_store.read(cx).has_shared_buffers())
});
do_search_and_assert(
&project,
"project",
Default::default(),
false,
&[path!("project1/README.md")],
cx.clone(),
)
.await;
}
#[gpui::test]
async fn test_remote_project_search_single_cpu(
cx: &mut TestAppContext,
server_cx: &mut TestAppContext,
) {
let fs = FakeFs::new(server_cx.executor());
fs.insert_tree(
path!("/code"),
json!({
"project1": {
".git": {},
"README.md": "# project 1",
"src": {
"lib.rs": "fn one() -> usize { 1 }"
}
},
}),
)
.await;
// Simulate a single-CPU environment (e.g. a devcontainer with 1 visible CPU).
// This causes the worker pool in project search to spawn num_cpus - 1 = 0 workers,
// which silently drops all search channels and produces zero results.
server_cx.executor().set_num_cpus(1);
let (project, _) = init_test(&fs, cx, server_cx).await;
project
.update(cx, |project, cx| {
project.find_or_create_worktree(path!("/code/project1"), true, cx)
})
.await
.unwrap();
cx.run_until_parked();
do_search_and_assert(
&project,
"project",
Default::default(),
false,
&[path!("project1/README.md")],
cx.clone(),
)
.await;
}
#[gpui::test]
async fn test_remote_project_search_inclusion(
cx: &mut TestAppContext,
server_cx: &mut TestAppContext,
) {
let fs = FakeFs::new(server_cx.executor());
fs.insert_tree(
path!("/code"),
json!({
"project1": {
"README.md": "# project 1",
},
"project2": {
"README.md": "# project 2",
},
}),
)
.await;
let (project, _) = init_test(&fs, cx, server_cx).await;
project
.update(cx, |project, cx| {
project.find_or_create_worktree(path!("/code/project1"), true, cx)
})
.await
.unwrap();
project
.update(cx, |project, cx| {
project.find_or_create_worktree(path!("/code/project2"), true, cx)
})
.await
.unwrap();
cx.run_until_parked();
// Case 1: Test search with path matcher limiting to only one worktree
let path_matcher = PathMatcher::new(
&["project1/*.md".to_owned()],
util::paths::PathStyle::local(),
)
.unwrap();
do_search_and_assert(
&project,
"project",
path_matcher,
true, // should be true in case of multiple worktrees
&[path!("project1/README.md")],
cx.clone(),
)
.await;
// Case 2: Test search without path matcher, matching both worktrees
do_search_and_assert(
&project,
"project",
Default::default(),
true, // should be true in case of multiple worktrees
&[path!("project1/README.md"), path!("project2/README.md")],
cx.clone(),
)
.await;
}
#[gpui::test]
async fn test_remote_settings(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
let fs = FakeFs::new(server_cx.executor());
fs.insert_tree(
"/code",
json!({
"project1": {
".git": {},
"README.md": "# project 1",
"src": {
"lib.rs": "fn one() -> usize { 1 }"
}
},
}),
)
.await;
let (project, headless) = init_test(&fs, cx, server_cx).await;
cx.update_global(|settings_store: &mut SettingsStore, cx| {
settings_store.set_user_settings(
r#"{"languages":{"Rust":{"language_servers":["from-local-settings"]}}}"#,
cx,
)
})
.unwrap();
cx.run_until_parked();
server_cx.read(|cx| {
assert_eq!(
AllLanguageSettings::get_global(cx)
.language(None, Some(&"Rust".into()), cx)
.language_servers,
["from-local-settings"],
"User language settings should be synchronized with the server settings"
)
});
server_cx
.update_global(|settings_store: &mut SettingsStore, cx| {
settings_store.set_server_settings(
r#"{"languages":{"Rust":{"language_servers":["from-server-settings"]}}}"#,
cx,
)
})
.unwrap();
cx.run_until_parked();
server_cx.read(|cx| {
assert_eq!(
AllLanguageSettings::get_global(cx)
.language(None, Some(&"Rust".into()), cx)
.language_servers,
["from-server-settings".to_string()],
"Server language settings should take precedence over the user settings"
)
});
fs.insert_tree(
"/code/project1/.zed",
json!({
"settings.json": r#"
{
"languages": {"Rust":{"language_servers":["override-rust-analyzer"]}},
"lsp": {
"override-rust-analyzer": {
"binary": {
"path": "~/.cargo/bin/rust-analyzer"
}
}
}
}"#
}),
)
.await;
let worktree_id = project
.update(cx, |project, cx| {
project.languages().add(rust_lang());
project.find_or_create_worktree("/code/project1", true, cx)
})
.await
.unwrap()
.0
.read_with(cx, |worktree, _| worktree.id());
let buffer = project
.update(cx, |project, cx| {
project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
})
.await
.unwrap();
cx.run_until_parked();
server_cx.read(|cx| {
let worktree_id = headless
.read(cx)
.worktree_store
.read(cx)
.worktrees()
.next()
.unwrap()
.read(cx)
.id();
assert_eq!(
AllLanguageSettings::get(
Some(SettingsLocation {
worktree_id,
path: rel_path("src/lib.rs")
}),
cx
)
.language(None, Some(&"Rust".into()), cx)
.language_servers,
["override-rust-analyzer".to_string()]
)
});
cx.read(|cx| {
assert_eq!(
LanguageSettings::for_buffer(buffer.read(cx), cx).language_servers,
["override-rust-analyzer".to_string()]
)
});
}
#[gpui::test]
async fn test_remote_lsp(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
let fs = FakeFs::new(server_cx.executor());
fs.insert_tree(
path!("/code"),
json!({
"project1": {
".git": {},
"README.md": "# project 1",
"src": {
"lib.rs": "fn one() -> usize { 1 }"
}
},
}),
)
.await;
let (project, headless) = init_test(&fs, cx, server_cx).await;
fs.insert_tree(
path!("/code/project1/.zed"),
json!({
"settings.json": r#"
{
"languages": {"Rust":{"language_servers":["rust-analyzer", "fake-analyzer"]}},
"lsp": {
"rust-analyzer": {
"binary": {
"path": "~/.cargo/bin/rust-analyzer"
}
},
"fake-analyzer": {
"binary": {
"path": "~/.cargo/bin/rust-analyzer"
}
}
}
}"#
}),
)
.await;
cx.update_entity(&project, |project, _| {
project.languages().register_test_language(LanguageConfig {
name: "Rust".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["rs".into()],
..Default::default()
},
..Default::default()
});
project.languages().register_fake_lsp_adapter(
"Rust",
FakeLspAdapter {
name: "rust-analyzer",
capabilities: lsp::ServerCapabilities {
completion_provider: Some(lsp::CompletionOptions::default()),
rename_provider: Some(lsp::OneOf::Left(true)),
..lsp::ServerCapabilities::default()
},
..FakeLspAdapter::default()
},
);
project.languages().register_fake_lsp_adapter(
"Rust",
FakeLspAdapter {
name: "fake-analyzer",
capabilities: lsp::ServerCapabilities {
completion_provider: Some(lsp::CompletionOptions::default()),
rename_provider: Some(lsp::OneOf::Left(true)),
..lsp::ServerCapabilities::default()
},
..FakeLspAdapter::default()
},
)
});
let mut fake_lsp = server_cx.update(|cx| {
headless.read(cx).languages.register_fake_lsp_server(
LanguageServerName("rust-analyzer".into()),
lsp::ServerCapabilities {
completion_provider: Some(lsp::CompletionOptions::default()),
rename_provider: Some(lsp::OneOf::Left(true)),
..lsp::ServerCapabilities::default()
},
None,
)
});
let mut fake_second_lsp = server_cx.update(|cx| {
headless.read(cx).languages.register_fake_lsp_adapter(
"Rust",
FakeLspAdapter {
name: "fake-analyzer",
capabilities: lsp::ServerCapabilities {
completion_provider: Some(lsp::CompletionOptions::default()),
rename_provider: Some(lsp::OneOf::Left(true)),
..lsp::ServerCapabilities::default()
},
..FakeLspAdapter::default()
},
);
headless.read(cx).languages.register_fake_lsp_server(
LanguageServerName("fake-analyzer".into()),
lsp::ServerCapabilities {
completion_provider: Some(lsp::CompletionOptions::default()),
rename_provider: Some(lsp::OneOf::Left(true)),
..lsp::ServerCapabilities::default()
},
None,
)
});
cx.run_until_parked();
let worktree_id = project
.update(cx, |project, cx| {
project.languages().add(rust_lang());
project.find_or_create_worktree(path!("/code/project1"), true, cx)
})
.await
.unwrap()
.0
.read_with(cx, |worktree, _| worktree.id());
// Wait for the settings to synchronize
cx.run_until_parked();
let (buffer, _handle) = project
.update(cx, |project, cx| {
project.open_buffer_with_lsp((worktree_id, rel_path("src/lib.rs")), cx)
})
.await
.unwrap();
cx.run_until_parked();
let fake_lsp = fake_lsp.next().await.unwrap();
let fake_second_lsp = fake_second_lsp.next().await.unwrap();
cx.read(|cx| {
assert_eq!(
LanguageSettings::for_buffer(buffer.read(cx), cx).language_servers,
["rust-analyzer".to_string(), "fake-analyzer".to_string()]
)
});
let buffer_id = cx.read(|cx| {
let buffer = buffer.read(cx);
assert_eq!(buffer.language().unwrap().name(), "Rust");
buffer.remote_id()
});
server_cx.read(|cx| {
let buffer = headless
.read(cx)
.buffer_store
.read(cx)
.get(buffer_id)
.unwrap();
assert_eq!(buffer.read(cx).language().unwrap().name(), "Rust");
});
server_cx.read(|cx| {
let lsp_store = headless.read(cx).lsp_store.read(cx);
assert_eq!(lsp_store.as_local().unwrap().language_servers.len(), 2);
});
fake_lsp.set_request_handler::<lsp::request::Completion, _, _>(|_, _| async move {
Ok(Some(CompletionResponse::Array(vec![lsp::CompletionItem {
label: "boop".to_string(),
..Default::default()
}])))
});
fake_second_lsp.set_request_handler::<lsp::request::Completion, _, _>(|_, _| async move {
Ok(Some(CompletionResponse::Array(vec![lsp::CompletionItem {
label: "beep".to_string(),
..Default::default()
}])))
});
let result = project
.update(cx, |project, cx| {
project.completions(
&buffer,
0,
CompletionContext {
trigger_kind: CompletionTriggerKind::INVOKED,
trigger_character: None,
},
cx,
)
})
.await
.unwrap();
assert_eq!(
result
.into_iter()
.flat_map(|response| response.completions)
.map(|c| c.label.text)
.collect::<Vec<_>>(),
vec!["boop".to_string(), "beep".to_string()]
);
fake_lsp.set_request_handler::<lsp::request::Rename, _, _>(|_, _| async move {
Ok(Some(lsp::WorkspaceEdit {
changes: Some(
[(
lsp::Uri::from_file_path(path!("/code/project1/src/lib.rs")).unwrap(),
vec![lsp::TextEdit::new(
lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 6)),
"two".to_string(),
)],
)]
.into_iter()
.collect(),
),
..Default::default()
}))
});
project
.update(cx, |project, cx| {
project.perform_rename(buffer.clone(), 3, "two".to_string(), cx)
})
.await
.unwrap();
cx.run_until_parked();
buffer.update(cx, |buffer, _| {
assert_eq!(buffer.text(), "fn two() -> usize { 1 }")
})
}
#[gpui::test]
async fn test_remote_cancel_language_server_work(
cx: &mut TestAppContext,
server_cx: &mut TestAppContext,
) {
let fs = FakeFs::new(server_cx.executor());
fs.insert_tree(
path!("/code"),
json!({
"project1": {
".git": {},
"README.md": "# project 1",
"src": {
"lib.rs": "fn one() -> usize { 1 }"
}
},
}),
)
.await;
let (project, headless) = init_test(&fs, cx, server_cx).await;
fs.insert_tree(
path!("/code/project1/.zed"),
json!({
"settings.json": r#"
{
"languages": {"Rust":{"language_servers":["rust-analyzer"]}},
"lsp": {
"rust-analyzer": {
"binary": {
"path": "~/.cargo/bin/rust-analyzer"
}
}
}
}"#
}),
)
.await;
cx.update_entity(&project, |project, _| {
project.languages().register_test_language(LanguageConfig {
name: "Rust".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["rs".into()],
..Default::default()
},
..Default::default()
});
project.languages().register_fake_lsp_adapter(
"Rust",
FakeLspAdapter {
name: "rust-analyzer",
..Default::default()
},
)
});
let mut fake_lsp = server_cx.update(|cx| {
headless.read(cx).languages.register_fake_lsp_server(
LanguageServerName("rust-analyzer".into()),
Default::default(),
None,
)
});
cx.run_until_parked();
let worktree_id = project
.update(cx, |project, cx| {
project.find_or_create_worktree(path!("/code/project1"), true, cx)
})
.await
.unwrap()
.0
.read_with(cx, |worktree, _| worktree.id());
cx.run_until_parked();
let (buffer, _handle) = project
.update(cx, |project, cx| {
project.open_buffer_with_lsp((worktree_id, rel_path("src/lib.rs")), cx)
})
.await
.unwrap();
cx.run_until_parked();
let mut fake_lsp = fake_lsp.next().await.unwrap();
// Cancelling all language server work for a given buffer
{
// Two operations, one cancellable and one not.
fake_lsp
.start_progress_with(
"another-token",
lsp::WorkDoneProgressBegin {
cancellable: Some(false),
..Default::default()
},
DEFAULT_LSP_REQUEST_TIMEOUT,
)
.await;
let progress_token = "the-progress-token";
fake_lsp
.start_progress_with(
progress_token,
lsp::WorkDoneProgressBegin {
cancellable: Some(true),
..Default::default()
},
DEFAULT_LSP_REQUEST_TIMEOUT,
)
.await;
cx.executor().run_until_parked();
project.update(cx, |project, cx| {
project.cancel_language_server_work_for_buffers([buffer.clone()], cx)
});
cx.executor().run_until_parked();
// Verify the cancellation was received on the server side
let cancel_notification = fake_lsp
.receive_notification::<lsp::notification::WorkDoneProgressCancel>()
.await;
assert_eq!(
cancel_notification.token,
lsp::NumberOrString::String(progress_token.into())
);
}
// Cancelling work by server_id and token
{
let server_id = fake_lsp.server.server_id();
let progress_token = "the-progress-token";
fake_lsp
.start_progress_with(
progress_token,
lsp::WorkDoneProgressBegin {
cancellable: Some(true),
..Default::default()
},
DEFAULT_LSP_REQUEST_TIMEOUT,
)
.await;
cx.executor().run_until_parked();
project.update(cx, |project, cx| {
project.cancel_language_server_work(
server_id,
Some(ProgressToken::String(SharedString::from(progress_token))),
cx,
)
});
cx.executor().run_until_parked();
// Verify the cancellation was received on the server side
let cancel_notification = fake_lsp
.receive_notification::<lsp::notification::WorkDoneProgressCancel>()
.await;
assert_eq!(
cancel_notification.token,
lsp::NumberOrString::String(progress_token.to_owned())
);
}
}
#[gpui::test]
async fn test_remote_reload(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
let fs = FakeFs::new(server_cx.executor());
fs.insert_tree(
path!("/code"),
json!({
"project1": {
".git": {},
"README.md": "# project 1",
"src": {
"lib.rs": "fn one() -> usize { 1 }"
}
},
}),
)
.await;
let (project, _headless) = init_test(&fs, cx, server_cx).await;
let (worktree, _) = project
.update(cx, |project, cx| {
project.find_or_create_worktree(path!("/code/project1"), true, cx)
})
.await
.unwrap();
let worktree_id = cx.update(|cx| worktree.read(cx).id());
let buffer = project
.update(cx, |project, cx| {
project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
})
.await
.unwrap();
fs.save(
&PathBuf::from(path!("/code/project1/src/lib.rs")),
&("bangles".to_string().into()),
LineEnding::Unix,
)
.await
.unwrap();
cx.run_until_parked();
buffer.update(cx, |buffer, cx| {
assert_eq!(buffer.text(), "bangles");
buffer.edit([(0..0, "a")], None, cx);
});
fs.save(
&PathBuf::from(path!("/code/project1/src/lib.rs")),
&("bloop".to_string().into()),
LineEnding::Unix,
)
.await
.unwrap();
cx.run_until_parked();
cx.update(|cx| {
assert!(buffer.read(cx).has_conflict());
});
project
.update(cx, |project, cx| {
project.reload_buffers([buffer.clone()].into_iter().collect(), false, cx)
})
.await
.unwrap();
cx.run_until_parked();
cx.update(|cx| {
assert!(!buffer.read(cx).has_conflict());
});
}
#[gpui::test]
async fn test_remote_resolve_path_in_buffer(
cx: &mut TestAppContext,
server_cx: &mut TestAppContext,
) {
let fs = FakeFs::new(server_cx.executor());
// Even though we are not testing anything from project1, it is necessary to test if project2 is picking up correct worktree
fs.insert_tree(
path!("/code"),
json!({
"project1": {
".git": {},
"README.md": "# project 1",
"src": {
"lib.rs": "fn one() -> usize { 1 }"
}
},
"project2": {
".git": {},
"README.md": "# project 2",
"src": {
"lib.rs": "fn two() -> usize { 2 }"
}
}
}),
)
.await;
let (project, _headless) = init_test(&fs, cx, server_cx).await;
let _ = project
.update(cx, |project, cx| {
project.find_or_create_worktree(path!("/code/project1"), true, cx)
})
.await
.unwrap();
let (worktree2, _) = project
.update(cx, |project, cx| {
project.find_or_create_worktree(path!("/code/project2"), true, cx)
})
.await
.unwrap();
let worktree2_id = cx.update(|cx| worktree2.read(cx).id());
cx.run_until_parked();
let buffer2 = project
.update(cx, |project, cx| {
project.open_buffer((worktree2_id, rel_path("src/lib.rs")), cx)
})
.await
.unwrap();
let path = project
.update(cx, |project, cx| {
project.resolve_path_in_buffer(path!("/code/project2/README.md"), &buffer2, cx)
})
.await
.unwrap();
assert!(path.is_file());
assert_eq!(path.abs_path().unwrap(), path!("/code/project2/README.md"));
let path = project
.update(cx, |project, cx| {
project.resolve_path_in_buffer("../README.md", &buffer2, cx)
})
.await
.unwrap();
assert!(path.is_file());
assert_eq!(
path.project_path().unwrap().clone(),
(worktree2_id, rel_path("README.md")).into()
);
let path = project
.update(cx, |project, cx| {
project.resolve_path_in_buffer("../src", &buffer2, cx)
})
.await
.unwrap();
assert_eq!(
path.project_path().unwrap().clone(),
(worktree2_id, rel_path("src")).into()
);
assert!(path.is_dir());
}
#[gpui::test]
async fn test_remote_resolve_abs_path(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
let fs = FakeFs::new(server_cx.executor());
fs.insert_tree(
path!("/code"),
json!({
"project1": {
".git": {},
"README.md": "# project 1",
"src": {
"lib.rs": "fn one() -> usize { 1 }"
}
},
}),
)
.await;
let (project, _headless) = init_test(&fs, cx, server_cx).await;
let path = project
.update(cx, |project, cx| {
project.resolve_abs_path(path!("/code/project1/README.md"), cx)
})
.await
.unwrap();
assert!(path.is_file());
assert_eq!(path.abs_path().unwrap(), path!("/code/project1/README.md"));
let path = project
.update(cx, |project, cx| {
project.resolve_abs_path(path!("/code/project1/src"), cx)
})
.await
.unwrap();
assert!(path.is_dir());
assert_eq!(path.abs_path().unwrap(), path!("/code/project1/src"));
let path = project
.update(cx, |project, cx| {
project.resolve_abs_path(path!("/code/project1/DOESNOTEXIST"), cx)
})
.await;
assert!(path.is_none());
}
#[gpui::test(iterations = 10)]
async fn test_canceling_buffer_opening(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
let fs = FakeFs::new(server_cx.executor());
fs.insert_tree(
"/code",
json!({
"project1": {
".git": {},
"README.md": "# project 1",
"src": {
"lib.rs": "fn one() -> usize { 1 }"
}
},
}),
)
.await;
let (project, _headless) = init_test(&fs, cx, server_cx).await;
let (worktree, _) = project
.update(cx, |project, cx| {
project.find_or_create_worktree("/code/project1", true, cx)
})
.await
.unwrap();
let worktree_id = worktree.read_with(cx, |tree, _| tree.id());
// Open a buffer on the client but cancel after a random amount of time.
let buffer = project.update(cx, |p, cx| {
p.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
});
cx.executor().simulate_random_delay().await;
drop(buffer);
// Try opening the same buffer again as the client, and ensure we can
// still do it despite the cancellation above.
let buffer = project
.update(cx, |p, cx| {
p.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
})
.await
.unwrap();
buffer.read_with(cx, |buf, _| {
assert_eq!(buf.text(), "fn one() -> usize { 1 }")
});
}
#[gpui::test]
async fn test_adding_then_removing_then_adding_worktrees(
cx: &mut TestAppContext,
server_cx: &mut TestAppContext,
) {
let fs = FakeFs::new(server_cx.executor());
fs.insert_tree(
path!("/code"),
json!({
"project1": {
".git": {},
"README.md": "# project 1",
"src": {
"lib.rs": "fn one() -> usize { 1 }"
}
},
"project2": {
"README.md": "# project 2",
},
}),
)
.await;
let (project, _headless) = init_test(&fs, cx, server_cx).await;
let (_worktree, _) = project
.update(cx, |project, cx| {
project.find_or_create_worktree(path!("/code/project1"), true, cx)
})
.await
.unwrap();
let (worktree_2, _) = project
.update(cx, |project, cx| {
project.find_or_create_worktree(path!("/code/project2"), true, cx)
})
.await
.unwrap();
let worktree_id_2 = worktree_2.read_with(cx, |tree, _| tree.id());
project.update(cx, |project, cx| project.remove_worktree(worktree_id_2, cx));
let (worktree_2, _) = project
.update(cx, |project, cx| {
project.find_or_create_worktree(path!("/code/project2"), true, cx)
})
.await
.unwrap();
cx.run_until_parked();
worktree_2.update(cx, |worktree, _cx| {
assert!(worktree.is_visible());
let entries = worktree.entries(true, 0).collect::<Vec<_>>();
assert_eq!(entries.len(), 2);
assert_eq!(entries[1].path.as_unix_str(), "README.md")
})
}
#[gpui::test]
async fn test_open_server_settings(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
let fs = FakeFs::new(server_cx.executor());
fs.insert_tree(
path!("/code"),
json!({
"project1": {
".git": {},
"README.md": "# project 1",
"src": {
"lib.rs": "fn one() -> usize { 1 }"
}
},
}),
)
.await;
let (project, _headless) = init_test(&fs, cx, server_cx).await;
let buffer = project.update(cx, |project, cx| project.open_server_settings(cx));
cx.executor().run_until_parked();
let buffer = buffer.await.unwrap();
cx.update(|cx| {
assert_eq!(
buffer.read(cx).text(),
initial_server_settings_content()
.to_string()
.replace("\r\n", "\n")
)
})
}
#[gpui::test(iterations = 20)]
async fn test_reconnect(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
let fs = FakeFs::new(server_cx.executor());
fs.insert_tree(
path!("/code"),
json!({
"project1": {
".git": {},
"README.md": "# project 1",
"src": {
"lib.rs": "fn one() -> usize { 1 }"
}
},
}),
)
.await;
let (project, _headless) = init_test(&fs, cx, server_cx).await;
let (worktree, _) = project
.update(cx, |project, cx| {
project.find_or_create_worktree(path!("/code/project1"), true, cx)
})
.await
.unwrap();
let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
let buffer = project
.update(cx, |project, cx| {
project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
})
.await
.unwrap();
buffer.update(cx, |buffer, cx| {
assert_eq!(buffer.text(), "fn one() -> usize { 1 }");
let ix = buffer.text().find('1').unwrap();
buffer.edit([(ix..ix + 1, "100")], None, cx);
});
let client = cx.read(|cx| project.read(cx).remote_client().unwrap());
client
.update(cx, |client, cx| client.simulate_disconnect(cx))
.detach();
project
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
.await
.unwrap();
assert_eq!(
fs.load(path!("/code/project1/src/lib.rs").as_ref())
.await
.unwrap(),
"fn one() -> usize { 100 }"
);
}
#[gpui::test]
async fn test_remote_root_rename(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
let fs = FakeFs::new(server_cx.executor());
fs.insert_tree(
"/code",
json!({
"project1": {
".git": {},
"README.md": "# project 1",
},
}),
)
.await;
let (project, _) = init_test(&fs, cx, server_cx).await;
let (worktree, _) = project
.update(cx, |project, cx| {
project.find_or_create_worktree("/code/project1", true, cx)
})
.await
.unwrap();
cx.run_until_parked();
fs.rename(
&PathBuf::from("/code/project1"),
&PathBuf::from("/code/project2"),
Default::default(),
)
.await
.unwrap();
cx.run_until_parked();
worktree.update(cx, |worktree, _| {
assert_eq!(worktree.root_name(), "project2")
})
}
#[gpui::test]
async fn test_remote_rename_entry(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
let fs = FakeFs::new(server_cx.executor());
fs.insert_tree(
"/code",
json!({
"project1": {
".git": {},
"README.md": "# project 1",
},
}),
)
.await;
let (project, _) = init_test(&fs, cx, server_cx).await;
let (worktree, _) = project
.update(cx, |project, cx| {
project.find_or_create_worktree("/code/project1", true, cx)
})
.await
.unwrap();
cx.run_until_parked();
let entry = project
.update(cx, |project, cx| {
let worktree = worktree.read(cx);
let entry = worktree.entry_for_path(rel_path("README.md")).unwrap();
project.rename_entry(entry.id, (worktree.id(), rel_path("README.rst")).into(), cx)
})
.await
.unwrap()
.into_included()
.unwrap();
cx.run_until_parked();
worktree.update(cx, |worktree, _| {
assert_eq!(
worktree.entry_for_path(rel_path("README.rst")).unwrap().id,
entry.id
)
});
}
#[gpui::test]
async fn test_copy_file_into_remote_project(
cx: &mut TestAppContext,
server_cx: &mut TestAppContext,
) {
let remote_fs = FakeFs::new(server_cx.executor());
remote_fs
.insert_tree(
path!("/code"),
json!({
"project1": {
".git": {},
"README.md": "# project 1",
"src": {
"main.rs": ""
}
},
}),
)
.await;
let (project, _) = init_test(&remote_fs, cx, server_cx).await;
let (worktree, _) = project
.update(cx, |project, cx| {
project.find_or_create_worktree(path!("/code/project1"), true, cx)
})
.await
.unwrap();
cx.run_until_parked();
let local_fs = project
.read_with(cx, |project, _| project.fs().clone())
.as_fake();
local_fs
.insert_tree(
path!("/local-code"),
json!({
"dir1": {
"file1": "file 1 content",
"dir2": {
"file2": "file 2 content",
"dir3": {
"file3": ""
},
"dir4": {}
},
"dir5": {}
},
"file4": "file 4 content"
}),
)
.await;
worktree
.update(cx, |worktree, cx| {
worktree.copy_external_entries(
rel_path("src").into(),
vec![
Path::new(path!("/local-code/dir1/file1")).into(),
Path::new(path!("/local-code/dir1/dir2")).into(),
],
local_fs.clone(),
cx,
)
})
.await
.unwrap();
assert_eq!(
remote_fs.paths(true),
vec![
PathBuf::from(path!("/")),
PathBuf::from(path!("/code")),
PathBuf::from(path!("/code/project1")),
PathBuf::from(path!("/code/project1/.git")),
PathBuf::from(path!("/code/project1/README.md")),
PathBuf::from(path!("/code/project1/src")),
PathBuf::from(path!("/code/project1/src/dir2")),
PathBuf::from(path!("/code/project1/src/file1")),
PathBuf::from(path!("/code/project1/src/main.rs")),
PathBuf::from(path!("/code/project1/src/dir2/dir3")),
PathBuf::from(path!("/code/project1/src/dir2/dir4")),
PathBuf::from(path!("/code/project1/src/dir2/file2")),
PathBuf::from(path!("/code/project1/src/dir2/dir3/file3")),
]
);
assert_eq!(
remote_fs
.load(path!("/code/project1/src/file1").as_ref())
.await
.unwrap(),
"file 1 content"
);
assert_eq!(
remote_fs
.load(path!("/code/project1/src/dir2/file2").as_ref())
.await
.unwrap(),
"file 2 content"
);
assert_eq!(
remote_fs
.load(path!("/code/project1/src/dir2/dir3/file3").as_ref())
.await
.unwrap(),
""
);
}
#[gpui::test]
async fn test_remote_root_repo_common_dir(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
let fs = FakeFs::new(server_cx.executor());
fs.insert_tree(
"/code",
json!({
"main_repo": {
".git": {},
"file.txt": "content",
},
"no_git": {
"file.txt": "content",
},
}),
)
.await;
// Create a linked worktree that points back to main_repo's .git.
fs.add_linked_worktree_for_repo(
Path::new("/code/main_repo/.git"),
false,
GitWorktree {
path: PathBuf::from("/code/linked_worktree"),
ref_name: Some("refs/heads/feature-branch".into()),
sha: "abc123".into(),
is_main: false,
is_bare: false,
},
)
.await;
let (project, _headless) = init_test(&fs, cx, server_cx).await;
// Main repo: root_repo_common_dir should be the .git directory itself.
let (worktree_main, _) = project
.update(cx, |project, cx| {
project.find_or_create_worktree("/code/main_repo", true, cx)
})
.await
.unwrap();
cx.executor().run_until_parked();
let common_dir = worktree_main.read_with(cx, |worktree, _| {
worktree.snapshot().root_repo_common_dir().cloned()
});
assert_eq!(
common_dir.as_deref(),
Some(Path::new("/code/main_repo/.git")),
);
// Linked worktree: root_repo_common_dir should point to the main repo's .git.
let (worktree_linked, _) = project
.update(cx, |project, cx| {
project.find_or_create_worktree("/code/linked_worktree", true, cx)
})
.await
.unwrap();
cx.executor().run_until_parked();
let common_dir = worktree_linked.read_with(cx, |worktree, _| {
worktree.snapshot().root_repo_common_dir().cloned()
});
assert_eq!(
common_dir.as_deref(),
Some(Path::new("/code/main_repo/.git")),
);
// No git repo: root_repo_common_dir should be None.
let (worktree_no_git, _) = project
.update(cx, |project, cx| {
project.find_or_create_worktree("/code/no_git", true, cx)
})
.await
.unwrap();
cx.executor().run_until_parked();
let common_dir = worktree_no_git.read_with(cx, |worktree, _| {
worktree.snapshot().root_repo_common_dir().cloned()
});
assert_eq!(common_dir, None);
}
#[gpui::test]
async fn test_remote_search_commits_streams_proto_chunks(
cx: &mut TestAppContext,
server_cx: &mut TestAppContext,
) {
const COMMIT_COUNT: usize = 900;
const RESPONSE_MAX_SIZE: usize = 100;
let fs = FakeFs::new(server_cx.executor());
fs.insert_tree(
path!("/code"),
json!({
"project1": {
".git": {},
"file.txt": "content",
},
}),
)
.await;
let commit_data = (0..COMMIT_COUNT)
.map(|index| {
let sha = Oid::from_str(&format!("{:040x}", index + 1)).unwrap();
(
CommitData {
sha,
parents: Default::default(),
author_name: SharedString::from("Author"),
author_email: SharedString::from("author@example.com"),
commit_timestamp: index as i64,
subject: SharedString::from(format!("Subject {index}")),
message: SharedString::from(format!("needle commit {index}")),
},
false,
)
})
.collect::<Vec<_>>();
let expected_shas = commit_data
.iter()
.map(|(commit_data, _)| commit_data.sha.to_string())
.collect::<HashSet<_>>();
fs.set_commit_data(Path::new(path!("/code/project1/.git")), commit_data);
let (project, _headless) = init_test(&fs, cx, server_cx).await;
project
.update(cx, |project, cx| {
project.find_or_create_worktree(path!("/code/project1"), true, cx)
})
.await
.expect("should open remote worktree");
server_cx.run_until_parked();
cx.run_until_parked();
project
.update(cx, |project, cx| project.git_scans_complete(cx))
.await;
let (remote_client, repository_id) = project.read_with(cx, |project, cx| {
let repository = project
.active_repository(cx)
.expect("remote project should have an active repository");
let repository_id = repository.read(cx).snapshot().id;
let remote_client = project
.remote_client()
.expect("project should have a remote client");
(remote_client, repository_id)
});
let proto_client = remote_client.read_with(cx, |remote_client, _| remote_client.proto_client());
let mut stream = proto_client
.request_stream(proto::SearchCommits {
project_id: proto::REMOTE_SERVER_PROJECT_ID,
repository_id: repository_id.to_proto(),
log_source: Some(proto::GitLogSource {
source: Some(proto::git_log_source::Source::All(
proto::GitLogSourceAll {},
)),
}),
query: "needle".to_string(),
case_sensitive: true,
})
.await
.expect("search commits stream should start");
let mut chunks = Vec::new();
while let Some(response) = futures::StreamExt::next(&mut stream).await {
chunks.push(response.expect("search commits chunk should succeed").shas);
}
assert!(
chunks.len() > 1,
"expected search results to stream in multiple chunks"
);
for chunk in chunks.iter().take(chunks.len() - 1) {
assert!(
chunk.len() <= RESPONSE_MAX_SIZE,
"non-final chunks should meet the target byte size"
);
}
let actual_shas = chunks.into_iter().flatten().collect::<HashSet<_>>();
assert_eq!(actual_shas, expected_shas);
}
#[gpui::test]
async fn test_remote_archive_git_operations_are_supported(
cx: &mut TestAppContext,
server_cx: &mut TestAppContext,
) {
let fs = FakeFs::new(server_cx.executor());
fs.insert_tree(
"/project",
json!({
".git": {},
"file.txt": "content",
}),
)
.await;
fs.set_branch_name(Path::new("/project/.git"), Some("main"));
fs.set_head_for_repo(
Path::new("/project/.git"),
&[("file.txt", "content".into())],
"head-sha",
);
let (project, _headless) = init_test(&fs, cx, server_cx).await;
project
.update(cx, |project, cx| {
project.find_or_create_worktree(Path::new("/project"), true, cx)
})
.await
.expect("should open remote worktree");
cx.run_until_parked();
let repository = project.read_with(cx, |project, cx| {
project
.active_repository(cx)
.expect("remote project should have an active repository")
});
let head_sha = cx
.update(|cx| repository.update(cx, |repository, _| repository.head_sha()))
.await
.expect("head_sha request should complete")
.expect("head_sha should succeed")
.expect("HEAD should exist");
cx.run_until_parked();
cx.update(|cx| {
repository.update(cx, |repository, _| {
repository.update_ref("refs/zed-tests/archive-checkpoint".to_string(), head_sha)
})
})
.await
.expect("update_ref request should complete")
.expect("update_ref should succeed for remote repository");
cx.run_until_parked();
cx.update(|cx| {
repository.update(cx, |repository, _| {
repository.delete_ref("refs/zed-tests/archive-checkpoint".to_string())
})
})
.await
.expect("delete_ref request should complete")
.expect("delete_ref should succeed for remote repository");
cx.run_until_parked();
cx.update(|cx| repository.update(cx, |repository, _| repository.repair_worktrees()))
.await
.expect("repair_worktrees request should complete")
.expect("repair_worktrees should succeed for remote repository");
cx.run_until_parked();
let (staged_commit_sha, unstaged_commit_sha) = cx
.update(|cx| repository.update(cx, |repository, _| repository.create_archive_checkpoint()))
.await
.expect("create_archive_checkpoint request should complete")
.expect("create_archive_checkpoint should succeed for remote repository");
cx.run_until_parked();
cx.update(|cx| {
repository.update(cx, |repository, _| {
repository.restore_archive_checkpoint(staged_commit_sha, unstaged_commit_sha)
})
})
.await
.expect("restore_archive_checkpoint request should complete")
.expect("restore_archive_checkpoint should succeed for remote repository");
}
#[gpui::test]
async fn test_remote_git_diffs(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
let text_2 = "
fn one() -> usize {
1
}
"
.unindent();
let text_1 = "
fn one() -> usize {
0
}
"
.unindent();
let fs = FakeFs::new(server_cx.executor());
fs.insert_tree(
"/code",
json!({
"project1": {
".git": {},
"src": {
"lib.rs": text_2
},
"README.md": "# project 1",
},
}),
)
.await;
fs.set_index_for_repo(
Path::new("/code/project1/.git"),
&[("src/lib.rs", text_1.clone())],
);
fs.set_head_for_repo(
Path::new("/code/project1/.git"),
&[("src/lib.rs", text_1.clone())],
"deadbeef",
);
let (project, _headless) = init_test(&fs, cx, server_cx).await;
let (worktree, _) = project
.update(cx, |project, cx| {
project.find_or_create_worktree("/code/project1", true, cx)
})
.await
.unwrap();
let worktree_id = cx.update(|cx| worktree.read(cx).id());
cx.executor().run_until_parked();
let buffer = project
.update(cx, |project, cx| {
project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
})
.await
.unwrap();
let diff = project
.update(cx, |project, cx| {
project.open_uncommitted_diff(buffer.clone(), cx)
})
.await
.unwrap();
diff.read_with(cx, |diff, cx| {
assert_eq!(diff.base_text_string(cx).unwrap(), text_1);
assert_eq!(
diff.secondary_diff()
.unwrap()
.read(cx)
.base_text_string(cx)
.unwrap(),
text_1
);
});
// stage the current buffer's contents
fs.set_index_for_repo(
Path::new("/code/project1/.git"),
&[("src/lib.rs", text_2.clone())],
);
cx.executor().run_until_parked();
diff.read_with(cx, |diff, cx| {
assert_eq!(diff.base_text_string(cx).unwrap(), text_1);
assert_eq!(
diff.secondary_diff()
.unwrap()
.read(cx)
.base_text_string(cx)
.unwrap(),
text_2
);
});
// commit the current buffer's contents
fs.set_head_for_repo(
Path::new("/code/project1/.git"),
&[("src/lib.rs", text_2.clone())],
"deadbeef",
);
cx.executor().run_until_parked();
diff.read_with(cx, |diff, cx| {
assert_eq!(diff.base_text_string(cx).unwrap(), text_2);
assert_eq!(
diff.secondary_diff()
.unwrap()
.read(cx)
.base_text_string(cx)
.unwrap(),
text_2
);
});
}
#[gpui::test]
async fn test_remote_git_diffs_when_recv_update_repository_delay(
cx: &mut TestAppContext,
server_cx: &mut TestAppContext,
) {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
theme_settings::init(theme::LoadThemes::JustBase, cx);
release_channel::init(semver::Version::new(0, 0, 0), cx);
editor::init(cx);
});
use editor::Editor;
use gpui::VisualContext;
let text_2 = "
fn one() -> usize {
1
}
"
.unindent();
let text_1 = "
fn one() -> usize {
0
}
"
.unindent();
let fs = FakeFs::new(server_cx.executor());
fs.insert_tree(
path!("/code"),
json!({
"project1": {
"src": {
"lib.rs": text_2
},
"README.md": "# project 1",
},
}),
)
.await;
let (project, _headless) = init_test(&fs, cx, server_cx).await;
let (worktree, _) = project
.update(cx, |project, cx| {
project.find_or_create_worktree(path!("/code/project1"), true, cx)
})
.await
.unwrap();
let worktree_id = cx.update(|cx| worktree.read(cx).id());
let buffer = project
.update(cx, |project, cx| {
project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
})
.await
.unwrap();
let buffer_id = cx.update(|cx| buffer.read(cx).remote_id());
let cx = cx.add_empty_window();
let editor = cx.new_window_entity(|window, cx| {
Editor::for_buffer(buffer, Some(project.clone()), window, cx)
});
// Remote server will send proto::UpdateRepository after the instance of Editor create.
fs.insert_tree(
path!("/code"),
json!({
"project1": {
".git": {},
},
}),
)
.await;
fs.set_index_for_repo(
Path::new(path!("/code/project1/.git")),
&[("src/lib.rs", text_1.clone())],
);
fs.set_head_for_repo(
Path::new(path!("/code/project1/.git")),
&[("src/lib.rs", text_1.clone())],
"sha",
);
cx.executor().run_until_parked();
let diff = editor
.read_with(cx, |editor, cx| {
editor
.buffer()
.read_with(cx, |buffer, _| buffer.diff_for(buffer_id))
})
.unwrap();
diff.read_with(cx, |diff, cx| {
assert_eq!(diff.base_text_string(cx).unwrap(), text_1);
assert_eq!(
diff.secondary_diff()
.unwrap()
.read(cx)
.base_text_string(cx)
.unwrap(),
text_1
);
});
// stage the current buffer's contents
fs.set_index_for_repo(
Path::new(path!("/code/project1/.git")),
&[("src/lib.rs", text_2.clone())],
);
cx.executor().run_until_parked();
diff.read_with(cx, |diff, cx| {
assert_eq!(diff.base_text_string(cx).unwrap(), text_1);
assert_eq!(
diff.secondary_diff()
.unwrap()
.read(cx)
.base_text_string(cx)
.unwrap(),
text_2
);
});
// commit the current buffer's contents
fs.set_head_for_repo(
Path::new(path!("/code/project1/.git")),
&[("src/lib.rs", text_2.clone())],
"sha",
);
cx.executor().run_until_parked();
diff.read_with(cx, |diff, cx| {
assert_eq!(diff.base_text_string(cx).unwrap(), text_2);
assert_eq!(
diff.secondary_diff()
.unwrap()
.read(cx)
.base_text_string(cx)
.unwrap(),
text_2
);
});
}
#[gpui::test]
async fn test_remote_git_branches(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
let fs = FakeFs::new(server_cx.executor());
fs.insert_tree(
path!("/code"),
json!({
"project1": {
".git": {},
"README.md": "# project 1",
},
}),
)
.await;
let (project, headless_project) = init_test(&fs, cx, server_cx).await;
let branches = ["main", "dev", "feature-1"];
let branches_set = branches
.iter()
.map(ToString::to_string)
.collect::<HashSet<_>>();
fs.insert_branches(Path::new(path!("/code/project1/.git")), &branches);
let (_worktree, _) = project
.update(cx, |project, cx| {
project.find_or_create_worktree(path!("/code/project1"), true, cx)
})
.await
.unwrap();
// Give the worktree a bit of time to index the file system
cx.run_until_parked();
let repository = project.update(cx, |project, cx| project.active_repository(cx).unwrap());
let remote_branches = repository
.update(cx, |repository, _| repository.branches())
.await
.unwrap()
.unwrap();
let new_branch = branches[2];
let remote_branches = remote_branches
.into_iter()
.map(|branch| branch.name().to_string())
.collect::<HashSet<_>>();
assert_eq!(&remote_branches, &branches_set);
cx.update(|cx| {
repository.update(cx, |repository, _cx| {
repository.change_branch(new_branch.to_string())
})
})
.await
.unwrap()
.unwrap();
cx.run_until_parked();
let server_branch = server_cx.update(|cx| {
headless_project.update(cx, |headless_project, cx| {
headless_project.git_store.update(cx, |git_store, cx| {
git_store
.repositories()
.values()
.next()
.unwrap()
.read(cx)
.branch
.as_ref()
.unwrap()
.clone()
})
})
});
assert_eq!(server_branch.name(), branches[2]);
// Also try creating a new branch
cx.update(|cx| {
repository.update(cx, |repo, _cx| {
repo.create_branch("totally-new-branch".to_string(), None)
})
})
.await
.unwrap()
.unwrap();
cx.update(|cx| {
repository.update(cx, |repo, _cx| {
repo.change_branch("totally-new-branch".to_string())
})
})
.await
.unwrap()
.unwrap();
cx.run_until_parked();
let server_branch = server_cx.update(|cx| {
headless_project.update(cx, |headless_project, cx| {
headless_project.git_store.update(cx, |git_store, cx| {
git_store
.repositories()
.values()
.next()
.unwrap()
.read(cx)
.branch
.as_ref()
.unwrap()
.clone()
})
})
});
assert_eq!(server_branch.name(), "totally-new-branch");
}
#[gpui::test]
async fn test_remote_git_checkpoints(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
let fs = FakeFs::new(server_cx.executor());
fs.insert_tree(
path!("/code"),
json!({
"project1": {
".git": {},
"file.txt": "original content",
},
}),
)
.await;
let (project, _headless) = init_test(&fs, cx, server_cx).await;
let (_worktree, _) = project
.update(cx, |project, cx| {
project.find_or_create_worktree(path!("/code/project1"), true, cx)
})
.await
.unwrap();
cx.run_until_parked();
let repository = project.update(cx, |project, cx| project.active_repository(cx).unwrap());
// 1. Create a checkpoint of the original state
let checkpoint_1 = repository
.update(cx, |repository, _| repository.checkpoint())
.await
.unwrap()
.unwrap();
// 2. Modify a file on the server-side fs
fs.write(
Path::new(path!("/code/project1/file.txt")),
b"modified content",
)
.await
.unwrap();
// 3. Create a second checkpoint with the modified state
let checkpoint_2 = repository
.update(cx, |repository, _| repository.checkpoint())
.await
.unwrap()
.unwrap();
// 4. compare_checkpoints: same checkpoint with itself => equal
let equal = repository
.update(cx, |repository, _| {
repository.compare_checkpoints(checkpoint_1.clone(), checkpoint_1.clone())
})
.await
.unwrap()
.unwrap();
assert!(equal, "a checkpoint compared with itself should be equal");
// 5. compare_checkpoints: different states => not equal
let equal = repository
.update(cx, |repository, _| {
repository.compare_checkpoints(checkpoint_1.clone(), checkpoint_2.clone())
})
.await
.unwrap()
.unwrap();
assert!(
!equal,
"checkpoints of different states should not be equal"
);
// 6. diff_checkpoints: same checkpoint => empty diff
let diff = repository
.update(cx, |repository, _| {
repository.diff_checkpoints(checkpoint_1.clone(), checkpoint_1.clone())
})
.await
.unwrap()
.unwrap();
assert!(
diff.is_empty(),
"diff of identical checkpoints should be empty"
);
// 7. diff_checkpoints: different checkpoints => non-empty diff mentioning the changed file
let diff = repository
.update(cx, |repository, _| {
repository.diff_checkpoints(checkpoint_1.clone(), checkpoint_2.clone())
})
.await
.unwrap()
.unwrap();
assert!(
!diff.is_empty(),
"diff of different checkpoints should be non-empty"
);
assert!(
diff.contains("file.txt"),
"diff should mention the changed file"
);
assert!(
diff.contains("original content"),
"diff should contain removed content"
);
assert!(
diff.contains("modified content"),
"diff should contain added content"
);
// 8. restore_checkpoint: restore to original state
repository
.update(cx, |repository, _| {
repository.restore_checkpoint(checkpoint_1.clone())
})
.await
.unwrap()
.unwrap();
cx.run_until_parked();
// 9. Create a checkpoint after restore
let checkpoint_3 = repository
.update(cx, |repository, _| repository.checkpoint())
.await
.unwrap()
.unwrap();
// 10. compare_checkpoints: restored state matches original
let equal = repository
.update(cx, |repository, _| {
repository.compare_checkpoints(checkpoint_1.clone(), checkpoint_3.clone())
})
.await
.unwrap()
.unwrap();
assert!(equal, "restored state should match original checkpoint");
// 11. diff_checkpoints: restored state vs original => empty diff
let diff = repository
.update(cx, |repository, _| {
repository.diff_checkpoints(checkpoint_1.clone(), checkpoint_3.clone())
})
.await
.unwrap()
.unwrap();
assert!(diff.is_empty(), "diff after restore should be empty");
}
#[gpui::test]
async fn test_remote_agent_fs_tool_calls(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
let fs = FakeFs::new(server_cx.executor());
fs.insert_tree(
path!("/project"),
json!({
"a.txt": "A",
"b.txt": "B",
}),
)
.await;
let (project, _headless_project) = init_test(&fs, cx, server_cx).await;
project
.update(cx, |project, cx| {
project.find_or_create_worktree(path!("/project"), true, cx)
})
.await
.unwrap();
let action_log = cx.new(|_| action_log::ActionLog::new(project.clone()));
let input = ReadFileToolInput {
path: "project/b.txt".into(),
start_line: None,
end_line: None,
};
let read_tool = Arc::new(ReadFileTool::new(project, action_log, true));
let (event_stream, _) = ToolCallEventStream::test();
let exists_result = cx.update(|cx| {
read_tool
.clone()
.run(ToolInput::resolved(input), event_stream.clone(), cx)
});
let output = exists_result.await.unwrap();
assert_eq!(output, LanguageModelToolResultContent::Text("B".into()));
let input = ReadFileToolInput {
path: "project/c.txt".into(),
start_line: None,
end_line: None,
};
let does_not_exist_result =
cx.update(|cx| read_tool.run(ToolInput::resolved(input), event_stream, cx));
does_not_exist_result.await.unwrap_err();
}
#[gpui::test]
async fn test_remote_external_agent_server(
cx: &mut TestAppContext,
server_cx: &mut TestAppContext,
) {
let fs = FakeFs::new(server_cx.executor());
fs.insert_tree(path!("/project"), json!({})).await;
let (project, _headless_project) = init_test(&fs, cx, server_cx).await;
project
.update(cx, |project, cx| {
project.find_or_create_worktree(path!("/project"), true, cx)
})
.await
.unwrap();
let names = project.update(cx, |project, cx| {
project
.agent_server_store()
.read(cx)
.external_agents()
.map(|name| name.to_string())
.collect::<Vec<_>>()
});
pretty_assertions::assert_eq!(names, Vec::<String>::new());
server_cx.update_global::<SettingsStore, _>(|settings_store, cx| {
settings_store
.set_server_settings(
&json!({
"agent_servers": {
"foo": {
"type": "custom",
"command": "foo-cli",
"args": ["--flag"],
"env": {
"VAR": "val"
}
}
}
})
.to_string(),
cx,
)
.unwrap();
});
server_cx.run_until_parked();
cx.run_until_parked();
let names = project.update(cx, |project, cx| {
project
.agent_server_store()
.read(cx)
.external_agents()
.map(|name| name.to_string())
.collect::<Vec<_>>()
});
pretty_assertions::assert_eq!(names, ["foo"]);
let command = project
.update(cx, |project, cx| {
project.agent_server_store().update(cx, |store, cx| {
store
.get_external_agent(&"foo".into())
.unwrap()
.get_command(
vec![],
HashMap::from_iter([("OTHER_VAR".into(), "other-val".into())]),
&mut cx.to_async(),
)
})
})
.await
.unwrap();
assert_eq!(
command,
AgentServerCommand {
path: "foo-cli".into(),
args: vec!["--flag".into()],
env: Some(HashMap::from_iter([
("NO_BROWSER".into(), "1".into()),
("VAR".into(), "val".into()),
("OTHER_VAR".into(), "other-val".into())
]))
}
);
}
#[gpui::test]
async fn test_remote_apply_code_action_skips_unadvertised_command(
cx: &mut TestAppContext,
server_cx: &mut TestAppContext,
) {
let fs = FakeFs::new(server_cx.executor());
fs.insert_tree(
path!("/code"),
json!({
"project1": {
".git": {},
"README.md": "# project 1",
"src": {
"lib.rs": "fn one() -> usize { 1 }"
}
},
}),
)
.await;
let (project, headless) = init_test(&fs, cx, server_cx).await;
fs.insert_tree(
path!("/code/project1/.zed"),
json!({
"settings.json": r#"
{
"languages": {"Rust":{"language_servers":["rust-analyzer"]}},
"lsp": {
"rust-analyzer": {
"binary": {
"path": "~/.cargo/bin/rust-analyzer"
}
}
}
}"#
}),
)
.await;
cx.update_entity(&project, |project, _| {
project.languages().register_test_language(LanguageConfig {
name: "Rust".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["rs".into()],
..Default::default()
},
..Default::default()
});
project.languages().register_fake_lsp_adapter(
"Rust",
FakeLspAdapter {
name: "rust-analyzer",
..Default::default()
},
)
});
// Register the fake LSP with an empty execute_command_provider and a handler that panics
// if it is ever reached: commands not advertised by the server must be rejected by
// `apply_code_action` before dispatching to the language server.
let mut fake_lsp = server_cx.update(|cx| {
headless.read(cx).languages.register_fake_lsp_server(
LanguageServerName("rust-analyzer".into()),
lsp::ServerCapabilities {
execute_command_provider: Some(lsp::ExecuteCommandOptions {
commands: Vec::new(),
..Default::default()
}),
..Default::default()
},
Some(Box::new(|fake| {
fake.set_request_handler::<lsp::request::ExecuteCommand, _, _>(
|params, _| async move {
panic!(
"Unadvertised command {} must not reach the language server",
params.command
);
},
);
})),
)
});
cx.run_until_parked();
let worktree_id = project
.update(cx, |project, cx| {
project.find_or_create_worktree(path!("/code/project1"), true, cx)
})
.await
.unwrap()
.0
.read_with(cx, |worktree, _| worktree.id());
cx.run_until_parked();
let (buffer, _handle) = project
.update(cx, |project, cx| {
project.open_buffer_with_lsp((worktree_id, rel_path("src/lib.rs")), cx)
})
.await
.unwrap();
cx.run_until_parked();
let _fake_lsp = fake_lsp.next().await.unwrap();
let server_id = server_cx.read(|cx| {
*headless
.read(cx)
.lsp_store
.read(cx)
.as_local()
.unwrap()
.language_servers
.keys()
.next()
.unwrap()
});
let buffer_id = cx.read(|cx| buffer.read(cx).remote_id());
let action = project::CodeAction {
server_id,
range: language::Anchor::min_min_range_for_buffer(buffer_id),
lsp_action: project::LspAction::Command(lsp::Command {
title: "\u{25b6}\u{fe0e} Run Tests".into(),
command: "rust-analyzer.runSingle".into(),
arguments: Some(vec![json!({"label": "test-mod tests"})]),
}),
resolved: true,
};
let transaction = project
.update(cx, |project, cx| {
project.apply_code_action(buffer.clone(), action, true, cx)
})
.await
.expect("Unadvertised command must not be forwarded to executeCommand");
assert_eq!(transaction.0.len(), 0);
}
#[gpui::test]
async fn test_remote_restore_unstaged_hunk_clears_diff(
cx: &mut TestAppContext,
server_cx: &mut TestAppContext,
) {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
theme_settings::init(theme::LoadThemes::JustBase, cx);
release_channel::init(semver::Version::new(0, 0, 0), cx);
editor::init(cx);
});
use editor::Editor;
use gpui::VisualContext;
let base_text = "
fn one() -> usize {
1
}
"
.unindent();
let modified_text = "
fn one() -> usize {
100
}
"
.unindent();
let fs = FakeFs::new(server_cx.executor());
fs.insert_tree(
path!("/code"),
json!({
"project1": {
".git": {},
"src": {
"lib.rs": modified_text
},
},
}),
)
.await;
fs.set_index_for_repo(
Path::new(path!("/code/project1/.git")),
&[("src/lib.rs", base_text.clone())],
);
fs.set_head_for_repo(
Path::new(path!("/code/project1/.git")),
&[("src/lib.rs", base_text.clone())],
"deadbeef",
);
let (project, _headless) = init_test(&fs, cx, server_cx).await;
let worktree_id = {
let (worktree, _) = project
.update(cx, |project, cx| {
project.find_or_create_worktree(path!("/code/project1"), true, cx)
})
.await
.unwrap();
cx.update(|cx| worktree.read(cx).id())
};
cx.executor().run_until_parked();
let buffer = project
.update(cx, |project, cx| {
project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
})
.await
.unwrap();
let cx = cx.add_empty_window();
let editor = cx.new_window_entity(|window, cx| {
Editor::for_buffer(buffer, Some(project.clone()), window, cx)
});
cx.executor().run_until_parked();
editor.update_in(cx, |editor, window, cx| {
let snapshot = editor.snapshot(window, cx);
let hunks: Vec<_> = editor
.diff_hunks_in_ranges(
&[editor::Anchor::Min..editor::Anchor::Max],
&snapshot.buffer_snapshot(),
)
.collect();
assert!(!hunks.is_empty(), "should have diff hunks before restore");
});
cx.update_window_entity(&editor, |editor, window, cx| {
editor.select_all(&editor::actions::SelectAll, window, cx);
editor.git_restore(&git::Restore, window, cx);
});
cx.executor().run_until_parked();
editor.update_in(cx, |editor, _window, cx| {
let snapshot = editor.buffer().read(cx).snapshot(cx);
assert_eq!(
snapshot.text(),
base_text,
"buffer text should match base after restoring all hunks"
);
let hunks: Vec<_> = editor
.diff_hunks_in_ranges(&[editor::Anchor::Min..editor::Anchor::Max], &snapshot)
.collect();
assert!(hunks.is_empty(), "should have no diff hunks after restore");
});
}
pub async fn init_test(
server_fs: &Arc<FakeFs>,
cx: &mut TestAppContext,
server_cx: &mut TestAppContext,
) -> (Entity<Project>, Entity<HeadlessProject>) {
let server_fs = server_fs.clone();
cx.update(|cx| {
release_channel::init(semver::Version::new(0, 0, 0), cx);
});
server_cx.update(|cx| {
release_channel::init(semver::Version::new(0, 0, 0), cx);
});
init_logger();
let (opts, ssh_server_client, _) = RemoteClient::fake_server(cx, server_cx);
let http_client = Arc::new(BlockedHttpClient);
let node_runtime = NodeRuntime::unavailable();
let languages = Arc::new(LanguageRegistry::new(cx.executor()));
let proxy = Arc::new(ExtensionHostProxy::new());
server_cx.update(HeadlessProject::init);
let headless = server_cx.new(|cx| {
HeadlessProject::new(
crate::HeadlessAppState {
session: ssh_server_client,
fs: server_fs.clone(),
http_client,
node_runtime,
languages,
extension_host_proxy: proxy,
startup_time: std::time::Instant::now(),
},
false,
cx,
)
});
let ssh = RemoteClient::connect_mock(opts, cx).await;
let project = build_project(ssh, cx);
project
.update(cx, {
let headless = headless.clone();
|_, cx| cx.on_release(|_, _| drop(headless))
})
.detach();
(project, headless)
}
fn init_logger() {
zlog::init_test();
}
fn build_project(ssh: Entity<RemoteClient>, cx: &mut TestAppContext) -> Entity<Project> {
cx.update(|cx| {
if !cx.has_global::<SettingsStore>() {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
}
});
let client = cx.update(|cx| {
Client::new(
Arc::new(FakeSystemClock::new()),
FakeHttpClient::with_404_response(),
cx,
)
});
let node = NodeRuntime::unavailable();
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
let languages = Arc::new(LanguageRegistry::test(cx.executor()));
let fs = FakeFs::new(cx.executor());
cx.update(|cx| {
Project::init(&client, cx);
});
cx.update(|cx| Project::remote(ssh, client, node, user_store, languages, fs, false, cx))
}