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.
2814 lines
80 KiB
Rust
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))
|
|
}
|