### Motivation This is the second of three PRs to add remote/collab support for the git graph and is a follow-up to #54468. I'm adding remote support for the search because it's not user accessible without the initial graph fetch having remote support, so it allows us to merge this without having to add full remote support. Collab guest support will be added in a follow-up PR. #### Summary For large repos, searching can take a while to fully stream in all matched results. For example, running a basic search on the Linux repo took over 10s for me. Because of that, we want to stream search results in chunks to downstream users to keep the time-to-first-match low. After this change, the first chunk gets sent back after ~50ms on the Linux repo from receiving the request. In order to accomplish that, I added a new proto client API that allows for a request to map to n responses. e.g. ```/dev/null/example.rs#L1-1 client.add_entity_stream_request_handler(Self::handle_search_commits); ``` Note: The proto API isn't supported over collab yet, that will be another PR 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: - N/A --------- Co-authored-by: cameron <cameron.studdstreet@gmail.com>
2705 lines
77 KiB
Rust
2705 lines
77 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);
|
|
}
|
|
|
|
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))
|
|
}
|