## Summary Fixes the git commit template not loading in remote (SSH) projects. The `load_commit_template_text` method in `git_store.rs` was a no-op for `RepositoryState::Remote`, always returning `Ok(None)`. This patch adds a `LoadCommitTemplate` RPC so the client can ask the remote host to read its `commit.template` git config and return the file contents — mirroring the existing `GetBlobContent` pattern. ## Changes - **`crates/proto/proto/git.proto`** — new `LoadCommitTemplate` / `LoadCommitTemplateResponse` messages. - **`crates/proto/proto/zed.proto`** — registered envelope IDs 449/450. - **`crates/proto/src/proto.rs`** — wired up message priority, request pairing, and entity-message routing. - **`crates/project/src/git_store.rs`** — added `handle_load_commit_template` on the host side; replaced the `Ok(None)` no-op on the remote side with the RPC call. ## Self-Review Checklist - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments — *no unsafe code added* - [x] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) — *no UI changes* - [ ] Tests cover the new/changed behavior — *see "Testing notes" below* - [x] Performance impact has been considered and is acceptable — *one extra RPC on commit panel open for remote projects only; payload is a single optional string* ## Testing notes — why no automated test I did write an integration test (`test_remote_git_commit_template` in `collab/tests/integration/git_tests.rs`, modeled after `test_remote_git_head_sha`) along with the supporting changes to `FakeGitRepositoryState` (adding a `commit_template` field + `set_commit_template_for_repo` setter on `FakeFs`, since the fake hardcoded `load_commit_template` to `None`). The test compiled and the smaller crates (`fs`, `proto`, `project`) checked clean, but `cargo test -p collab --test collab_tests` cold-compile takes a very long time on my machine and I wasn't able to confirm the test actually passed locally. Rather than push a test I hadn't seen pass, I removed it. Happy to add it back in a follow-up PR (or in this one if reviewers prefer) once I can run the collab suite end-to-end — the diff is small and I can share it on request. Verification was done end-to-end manually using a Docker dev container as the SSH remote: #### Closes #55265 Video : [Screencast from 2026-05-02 18-09-03.webm](https://github.com/user-attachments/assets/9cb7f375-57fa-4af3-bde4-871c28f61efc) Release Notes: - Added support for loading git commit template messages in both remote and collab projects. --------- Co-authored-by: dino <dinojoaocosta@gmail.com>
3843 lines
112 KiB
Rust
3843 lines
112 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, NativeAgent, NativeAgentConnection, ReadFileTool, ReadFileToolInput, SkillTool,
|
|
SkillToolInput, SkillToolOutput, Templates, ThreadStore, ToolCallEventStream, ToolInput,
|
|
skill_body_resolver_for_project, skills_resolver_for_project,
|
|
};
|
|
use client::{Client, UserStore};
|
|
use clock::FakeSystemClock;
|
|
use collections::{HashMap, HashSet};
|
|
use language_model::{LanguageModelRegistry, LanguageModelToolResultContent};
|
|
use languages::rust_lang;
|
|
|
|
use editor::{
|
|
Editor, SelectionEffects,
|
|
actions::{ConfirmCodeAction, ToggleCodeActions},
|
|
code_context_menus::CodeContextMenu,
|
|
};
|
|
use extension::ExtensionHostProxy;
|
|
use fs::{FakeFs, Fs};
|
|
use git::{
|
|
Oid,
|
|
repository::{CommitData, GitCommitTemplate, RepoPath, Worktree as GitWorktree},
|
|
};
|
|
use gpui::{AppContext as _, Entity, SharedString, TestAppContext, UpdateGlobal, VisualContext};
|
|
use http_client::{BlockedHttpClient, FakeHttpClient};
|
|
use language::{
|
|
Buffer, FakeLspAdapter, LanguageConfig, LanguageMatcher, LanguageRegistry, LineEnding, Point,
|
|
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},
|
|
rc::Rc,
|
|
str::FromStr,
|
|
sync::Arc,
|
|
};
|
|
use unindent::Unindent as _;
|
|
use util::{path, path_list::PathList, 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 }"
|
|
);
|
|
});
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_remote_telemetry_event_forwarding(
|
|
cx: &mut TestAppContext,
|
|
server_cx: &mut TestAppContext,
|
|
) {
|
|
// This mirrors `init_test`, but retains the server-side session so the test
|
|
// can drive a forwarded telemetry event over the proto channel (as
|
|
// `init_telemetry_forwarding` does on a real remote server).
|
|
let server_fs = FakeFs::new(server_cx.executor());
|
|
server_fs
|
|
.insert_tree(
|
|
path!("/code"),
|
|
json!({ "project1": { "README.md": "# project 1" } }),
|
|
)
|
|
.await;
|
|
|
|
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, server_session, _) = 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: server_session.clone(),
|
|
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();
|
|
|
|
// The remote server forwards a bare `FlexibleEvent` as JSON; mirror that
|
|
// here by sending the proto message the forwarding task would send.
|
|
let event_json = json!({
|
|
"event_type": "fs_watcher_poll",
|
|
"event_properties": { "path": "/code/project1" },
|
|
})
|
|
.to_string();
|
|
server_session
|
|
.send(proto::TelemetryEvent {
|
|
project_id: proto::REMOTE_SERVER_PROJECT_ID,
|
|
event_json,
|
|
})
|
|
.unwrap();
|
|
cx.executor().run_until_parked();
|
|
|
|
let events = project.read_with(cx, |project, _| {
|
|
project.client().telemetry().queued_events()
|
|
});
|
|
assert_eq!(
|
|
events.len(),
|
|
1,
|
|
"the forwarded event should be reported once"
|
|
);
|
|
let event = &events[0];
|
|
assert_eq!(event.event_type, "fs_watcher_poll");
|
|
// The event's original properties survive the round-trip.
|
|
assert_eq!(
|
|
event.event_properties.get("path"),
|
|
Some(&serde_json::Value::String("/code/project1".to_string()))
|
|
);
|
|
// The client stamps the remote host metadata it learned at connection time.
|
|
// The mock connection reports a Linux/x86_64 host over a "mock" connection.
|
|
assert_eq!(
|
|
event.event_properties.get("remote"),
|
|
Some(&serde_json::Value::Bool(true))
|
|
);
|
|
assert_eq!(
|
|
event.event_properties.get("remote_connection_type"),
|
|
Some(&serde_json::Value::String("mock".to_string()))
|
|
);
|
|
assert_eq!(
|
|
event.event_properties.get("remote_os_name"),
|
|
Some(&serde_json::Value::String("Linux".to_string()))
|
|
);
|
|
assert_eq!(
|
|
event.event_properties.get("remote_architecture"),
|
|
Some(&serde_json::Value::String("x86_64".to_string()))
|
|
);
|
|
}
|
|
|
|
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_code_action_resolve(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);
|
|
});
|
|
|
|
let fs = FakeFs::new(server_cx.executor());
|
|
fs.insert_tree(
|
|
path!("/code"),
|
|
json!({
|
|
"project1": {
|
|
".git": {},
|
|
"src": {
|
|
"lib.rs": "fn one() -> usize { 1 }"
|
|
}
|
|
},
|
|
}),
|
|
)
|
|
.await;
|
|
|
|
let (project, headless) = init_test(&fs, cx, server_cx).await;
|
|
|
|
let capabilities = lsp::ServerCapabilities {
|
|
code_action_provider: Some(lsp::CodeActionProviderCapability::Options(
|
|
lsp::CodeActionOptions {
|
|
resolve_provider: Some(true),
|
|
..lsp::CodeActionOptions::default()
|
|
},
|
|
)),
|
|
..lsp::ServerCapabilities::default()
|
|
};
|
|
|
|
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: capabilities.clone(),
|
|
..FakeLspAdapter::default()
|
|
},
|
|
);
|
|
});
|
|
|
|
server_cx.update(|cx| {
|
|
headless.read(cx).languages.register_fake_lsp_server(
|
|
LanguageServerName("rust-analyzer".into()),
|
|
capabilities,
|
|
Some(Box::new(|fake_lsp| {
|
|
fake_lsp.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
|
|
|_, _| async move {
|
|
Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
|
|
lsp::CodeAction {
|
|
title: "Use two".to_string(),
|
|
data: Some(serde_json::json!({ "id": "action" })),
|
|
..lsp::CodeAction::default()
|
|
},
|
|
)]))
|
|
},
|
|
);
|
|
fake_lsp.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>(
|
|
|mut action, _| async move {
|
|
action.edit = 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(),
|
|
),
|
|
..lsp::WorkspaceEdit::default()
|
|
});
|
|
Ok(action)
|
|
},
|
|
);
|
|
})),
|
|
);
|
|
});
|
|
|
|
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());
|
|
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 cx = cx.add_empty_window();
|
|
let workspace = cx.new_window_entity(|window, cx| {
|
|
workspace::Workspace::test_new(project.clone(), window, cx)
|
|
});
|
|
let editor = cx.new_window_entity(|window, cx| {
|
|
Editor::for_buffer(buffer.clone(), Some(project.clone()), window, cx)
|
|
});
|
|
workspace.update_in(cx, |workspace, window, cx| {
|
|
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
|
|
});
|
|
cx.run_until_parked();
|
|
|
|
editor.update_in(cx, |editor, window, cx| {
|
|
editor.change_selections(SelectionEffects::default(), window, cx, |s| {
|
|
s.select_ranges([Point::new(0, 0)..Point::new(0, 0)]);
|
|
});
|
|
});
|
|
cx.executor()
|
|
.advance_clock(editor::CODE_ACTIONS_DEBOUNCE_TIMEOUT * 2);
|
|
cx.run_until_parked();
|
|
|
|
editor.update_in(cx, |editor, window, cx| {
|
|
editor.toggle_code_actions(
|
|
&ToggleCodeActions {
|
|
deployed_from: None,
|
|
quick_launch: false,
|
|
},
|
|
window,
|
|
cx,
|
|
);
|
|
});
|
|
cx.run_until_parked();
|
|
|
|
editor.update(cx, |editor, _| assert!(editor.context_menu_visible()));
|
|
|
|
let confirm_action = editor
|
|
.update_in(cx, |editor, window, cx| {
|
|
Editor::confirm_code_action(editor, &ConfirmCodeAction { item_ix: Some(0) }, window, cx)
|
|
})
|
|
.unwrap();
|
|
confirm_action.await.unwrap();
|
|
|
|
buffer.read_with(cx, |buffer, _| {
|
|
assert_eq!(buffer.text(), "fn two() -> usize { 1 }");
|
|
});
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_remote_code_lens_fetch_after_lsp_starts(
|
|
cx: &mut TestAppContext,
|
|
server_cx: &mut TestAppContext,
|
|
) {
|
|
let fs = FakeFs::new(server_cx.executor());
|
|
fs.insert_tree(
|
|
path!("/code"),
|
|
json!({
|
|
"project1": {
|
|
".git": {},
|
|
"src": {
|
|
"lib.rs": "fn one() -> usize { 1 }"
|
|
}
|
|
},
|
|
}),
|
|
)
|
|
.await;
|
|
|
|
let (project, headless) = init_test(&fs, cx, server_cx).await;
|
|
|
|
let capabilities = lsp::ServerCapabilities {
|
|
code_lens_provider: Some(lsp::CodeLensOptions {
|
|
resolve_provider: None,
|
|
}),
|
|
..lsp::ServerCapabilities::default()
|
|
};
|
|
|
|
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: capabilities.clone(),
|
|
..FakeLspAdapter::default()
|
|
},
|
|
);
|
|
});
|
|
|
|
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());
|
|
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();
|
|
|
|
// Prime the code lens cache while no LSP server exists on the host.
|
|
// This simulates the race where the editor fetches code lenses during
|
|
// initial paint before the language server has started.
|
|
let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
|
|
let initial_actions = lsp_store
|
|
.update(cx, |lsp_store, cx| lsp_store.code_lens_actions(&buffer, cx))
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(
|
|
initial_actions.map(|a| a.len()),
|
|
Some(0),
|
|
"Before any LSP starts, code lenses should be empty"
|
|
);
|
|
|
|
// Now register the LSP on the host. This triggers server startup and
|
|
// buffer registration, propagating capabilities to the client.
|
|
server_cx.update(|cx| {
|
|
headless.read(cx).languages.register_fake_lsp_server(
|
|
LanguageServerName("rust-analyzer".into()),
|
|
capabilities,
|
|
Some(Box::new(|fake_lsp| {
|
|
fake_lsp.set_request_handler::<lsp::request::CodeLensRequest, _, _>(
|
|
|_, _| async move {
|
|
Ok(Some(vec![lsp::CodeLens {
|
|
range: lsp::Range::new(
|
|
lsp::Position::new(0, 0),
|
|
lsp::Position::new(0, 9),
|
|
),
|
|
command: Some(lsp::Command {
|
|
title: "1 reference".to_string(),
|
|
command: "lens_cmd".to_string(),
|
|
arguments: None,
|
|
}),
|
|
data: None,
|
|
}]))
|
|
},
|
|
);
|
|
})),
|
|
);
|
|
});
|
|
|
|
// Trigger re-evaluation of language servers for the already-open buffer.
|
|
server_cx.update_entity(&headless, |headless, cx| {
|
|
headless.lsp_store.update(cx, |lsp_store, cx| {
|
|
lsp_store.restart_all_language_servers(cx);
|
|
});
|
|
});
|
|
cx.run_until_parked();
|
|
|
|
// A subsequent fetch must bypass the stale (empty) cache now that a
|
|
// new server is available.
|
|
let actions = lsp_store
|
|
.update(cx, |lsp_store, cx| lsp_store.code_lens_actions(&buffer, cx))
|
|
.await
|
|
.unwrap();
|
|
let actions = actions.expect("Should have code lens actions after LSP starts");
|
|
assert_eq!(
|
|
actions.len(),
|
|
1,
|
|
"Should have fetched one code lens from the newly started LSP"
|
|
);
|
|
assert_eq!(
|
|
actions.values().next().unwrap().lsp_action.title(),
|
|
"1 reference",
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_remote_code_lens_resolve(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);
|
|
SettingsStore::update_global(cx, |store, cx| {
|
|
store.update_user_settings(cx, |settings| {
|
|
settings.editor.code_lens = Some(settings::CodeLens::Menu);
|
|
});
|
|
});
|
|
});
|
|
|
|
let fs = FakeFs::new(server_cx.executor());
|
|
fs.insert_tree(
|
|
path!("/code"),
|
|
json!({
|
|
"project1": {
|
|
".git": {},
|
|
"src": {
|
|
"lib.rs": "fn one() -> usize { 1 }"
|
|
}
|
|
},
|
|
}),
|
|
)
|
|
.await;
|
|
|
|
let (project, headless) = init_test(&fs, cx, server_cx).await;
|
|
|
|
let capabilities = lsp::ServerCapabilities {
|
|
code_lens_provider: Some(lsp::CodeLensOptions {
|
|
resolve_provider: Some(true),
|
|
}),
|
|
..lsp::ServerCapabilities::default()
|
|
};
|
|
|
|
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: capabilities.clone(),
|
|
..FakeLspAdapter::default()
|
|
},
|
|
);
|
|
});
|
|
|
|
server_cx.update(|cx| {
|
|
headless.read(cx).languages.register_fake_lsp_server(
|
|
LanguageServerName("rust-analyzer".into()),
|
|
capabilities,
|
|
Some(Box::new(|fake_lsp| {
|
|
fake_lsp.set_request_handler::<lsp::request::CodeLensRequest, _, _>(
|
|
|_, _| async move {
|
|
Ok(Some(vec![lsp::CodeLens {
|
|
range: lsp::Range::new(
|
|
lsp::Position::new(0, 0),
|
|
lsp::Position::new(0, 9),
|
|
),
|
|
command: None,
|
|
data: Some(serde_json::json!({ "id": "lens" })),
|
|
}]))
|
|
},
|
|
);
|
|
fake_lsp.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
|
|
|lens, _| async move {
|
|
Ok(lsp::CodeLens {
|
|
command: Some(lsp::Command {
|
|
title: "1 reference".to_string(),
|
|
command: "noop".to_string(),
|
|
arguments: None,
|
|
}),
|
|
..lens
|
|
})
|
|
},
|
|
);
|
|
})),
|
|
);
|
|
});
|
|
|
|
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());
|
|
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 cx = cx.add_empty_window();
|
|
let workspace = cx.new_window_entity(|window, cx| {
|
|
workspace::Workspace::test_new(project.clone(), window, cx)
|
|
});
|
|
let editor = cx.new_window_entity(|window, cx| {
|
|
Editor::for_buffer(buffer.clone(), Some(project.clone()), window, cx)
|
|
});
|
|
workspace.update_in(cx, |workspace, window, cx| {
|
|
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
|
|
});
|
|
|
|
editor.update_in(cx, |editor, window, cx| {
|
|
editor.change_selections(SelectionEffects::default(), window, cx, |s| {
|
|
s.select_ranges([Point::new(0, 0)..Point::new(0, 0)]);
|
|
});
|
|
});
|
|
cx.executor()
|
|
.advance_clock(editor::CODE_ACTIONS_DEBOUNCE_TIMEOUT * 2);
|
|
cx.run_until_parked();
|
|
|
|
editor.update_in(cx, |editor, window, cx| {
|
|
editor.toggle_code_actions(
|
|
&ToggleCodeActions {
|
|
deployed_from: None,
|
|
quick_launch: false,
|
|
},
|
|
window,
|
|
cx,
|
|
);
|
|
});
|
|
cx.run_until_parked();
|
|
|
|
editor.update(cx, |editor, _| {
|
|
assert!(editor.context_menu_visible());
|
|
let menu = editor.context_menu().borrow();
|
|
let actions_menu = match menu.as_ref() {
|
|
Some(CodeContextMenu::CodeActions(m)) => m,
|
|
_ => panic!("Expected code actions menu to be visible"),
|
|
};
|
|
let item = actions_menu
|
|
.actions
|
|
.get(0)
|
|
.expect("Expected at least one item in menu");
|
|
assert_eq!(item.label(), "1 reference");
|
|
});
|
|
}
|
|
|
|
#[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, is_linked_worktree) = worktree_main.read_with(cx, |worktree, _| {
|
|
(
|
|
worktree.snapshot().root_repo_common_dir().cloned(),
|
|
worktree.snapshot().root_repo_is_linked_worktree(),
|
|
)
|
|
});
|
|
assert_eq!(
|
|
common_dir.as_deref(),
|
|
Some(Path::new("/code/main_repo/.git")),
|
|
);
|
|
assert!(!is_linked_worktree);
|
|
|
|
// 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, is_linked_worktree) = worktree_linked.read_with(cx, |worktree, _| {
|
|
(
|
|
worktree.snapshot().root_repo_common_dir().cloned(),
|
|
worktree.snapshot().root_repo_is_linked_worktree(),
|
|
)
|
|
});
|
|
assert_eq!(
|
|
common_dir.as_deref(),
|
|
Some(Path::new("/code/main_repo/.git")),
|
|
);
|
|
assert!(is_linked_worktree);
|
|
|
|
let main_worktree_paths = project.read_with(cx, |project, cx| {
|
|
project
|
|
.worktree_paths(cx)
|
|
.main_worktree_path_list()
|
|
.ordered_paths()
|
|
.map(|path| path.to_path_buf())
|
|
.collect::<Vec<_>>()
|
|
});
|
|
assert_eq!(
|
|
main_worktree_paths,
|
|
vec![
|
|
PathBuf::from("/code/main_repo"),
|
|
PathBuf::from("/code/main_repo"),
|
|
]
|
|
);
|
|
|
|
// 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, is_linked_worktree) = worktree_no_git.read_with(cx, |worktree, _| {
|
|
(
|
|
worktree.snapshot().root_repo_common_dir().cloned(),
|
|
worktree.snapshot().root_repo_is_linked_worktree(),
|
|
)
|
|
});
|
|
assert_eq!(common_dir, None);
|
|
assert!(!is_linked_worktree);
|
|
}
|
|
|
|
#[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_add_path_to_gitignore_in_remote_repository(
|
|
cx: &mut TestAppContext,
|
|
server_cx: &mut TestAppContext,
|
|
) {
|
|
let fs = FakeFs::new(server_cx.executor());
|
|
fs.insert_tree(
|
|
"/project",
|
|
json!({
|
|
".git": {},
|
|
".gitignore": "existing\n",
|
|
"logs": {
|
|
"app.log": ""
|
|
},
|
|
"tmp.txt": "",
|
|
}),
|
|
)
|
|
.await;
|
|
fs.set_branch_name(Path::new("/project/.git"), Some("main"));
|
|
fs.set_head_for_repo(
|
|
Path::new("/project/.git"),
|
|
&[(".gitignore", "existing\n".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")
|
|
});
|
|
|
|
for (path, is_dir) in [("tmp.txt", false), ("logs", true), ("tmp.txt", false)] {
|
|
let repo_path = RepoPath::new(path).expect("path should be a valid repo path");
|
|
cx.update(|cx| {
|
|
repository.update(cx, |repository, _| {
|
|
repository.add_path_to_gitignore(&repo_path, is_dir)
|
|
})
|
|
})
|
|
.await
|
|
.expect("add to .gitignore request should complete")
|
|
.expect("add to .gitignore should succeed for remote repository");
|
|
}
|
|
|
|
server_cx.run_until_parked();
|
|
cx.run_until_parked();
|
|
|
|
assert_eq!(
|
|
fs.load(Path::new("/project/.gitignore"))
|
|
.await
|
|
.expect(".gitignore should be readable"),
|
|
"existing\ntmp.txt\nlogs/\n"
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_add_path_to_git_info_exclude_in_remote_repository(
|
|
cx: &mut TestAppContext,
|
|
server_cx: &mut TestAppContext,
|
|
) {
|
|
let fs = FakeFs::new(server_cx.executor());
|
|
fs.insert_tree(
|
|
"/project",
|
|
json!({
|
|
".git": {},
|
|
"logs": {
|
|
"app.log": ""
|
|
},
|
|
"tmp.txt": "",
|
|
}),
|
|
)
|
|
.await;
|
|
fs.set_branch_name(Path::new("/project/.git"), Some("main"));
|
|
fs.set_head_for_repo(Path::new("/project/.git"), &[], "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")
|
|
});
|
|
|
|
for (path, is_dir) in [("tmp.txt", false), ("logs", true), ("tmp.txt", false)] {
|
|
let repo_path = RepoPath::new(path).expect("path should be a valid repo path");
|
|
cx.update(|cx| {
|
|
repository.update(cx, |repository, _| {
|
|
repository.add_path_to_git_info_exclude(&repo_path, is_dir)
|
|
})
|
|
})
|
|
.await
|
|
.expect("add to info/exclude request should complete")
|
|
.expect("add to info/exclude should succeed for remote repository");
|
|
}
|
|
|
|
server_cx.run_until_parked();
|
|
cx.run_until_parked();
|
|
|
|
assert_eq!(
|
|
fs.load(Path::new("/project/.git/info/exclude"))
|
|
.await
|
|
.expect("info/exclude should be readable"),
|
|
"tmp.txt\nlogs/\n"
|
|
);
|
|
}
|
|
|
|
#[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()
|
|
.branches;
|
|
|
|
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");
|
|
|
|
let default_branch = cx
|
|
.update(|cx| repository.update(cx, |repository, _cx| repository.default_branch(false)))
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
assert_eq!(default_branch.as_deref(), Some("main"));
|
|
|
|
let default_branch_with_remote = cx
|
|
.update(|cx| repository.update(cx, |repository, _cx| repository.default_branch(true)))
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
assert_eq!(default_branch_with_remote.as_deref(), Some("origin/main"));
|
|
}
|
|
|
|
#[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(" 1\tB".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_adding_remote_skill(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
|
|
use acp_thread::AgentConnection as _;
|
|
|
|
let fs = FakeFs::new(server_cx.executor());
|
|
fs.insert_tree(
|
|
path!("/project"),
|
|
json!({
|
|
".agents": {
|
|
"skills": {
|
|
"test-skill": {
|
|
"SKILL.md": "---\nname: test-skill\ndescription: test description\n---\ntest body"
|
|
}
|
|
}
|
|
}
|
|
}),
|
|
)
|
|
.await;
|
|
|
|
let (project, _headless_project) = init_test(&fs, cx, server_cx).await;
|
|
cx.update(|cx| {
|
|
LanguageModelRegistry::test(cx);
|
|
});
|
|
let (_worktree, _rel_path) = project
|
|
.update(cx, |project, cx| {
|
|
project.find_or_create_worktree(path!("/project"), true, cx)
|
|
})
|
|
.await
|
|
.unwrap();
|
|
cx.run_until_parked();
|
|
let thread_store = cx.new(|cx| ThreadStore::new(cx));
|
|
let agent = cx.update(|cx| NativeAgent::new(thread_store, Templates::new(), fs.clone(), cx));
|
|
let connection = Rc::new(NativeAgentConnection(agent.clone()));
|
|
let _acp_thread = cx
|
|
.update(|cx| {
|
|
connection.clone().new_session(
|
|
project.clone(),
|
|
PathList::new(&[Path::new("/project")]),
|
|
cx,
|
|
)
|
|
})
|
|
.await
|
|
.unwrap();
|
|
cx.run_until_parked();
|
|
|
|
let skill_tool = Arc::new(SkillTool::with_body_resolver(
|
|
skills_resolver_for_project(agent.downgrade(), project.entity_id()),
|
|
skill_body_resolver_for_project(project.clone(), fs.clone()),
|
|
));
|
|
let (event_stream, mut event_stream_rx) = ToolCallEventStream::test();
|
|
|
|
let input = SkillToolInput {
|
|
name: "test-skill".into(),
|
|
};
|
|
let task = cx.update(|cx| {
|
|
skill_tool
|
|
.clone()
|
|
.run(ToolInput::resolved(input), event_stream.clone(), cx)
|
|
});
|
|
|
|
// The project-local skill is not a built-in, so the tool requests
|
|
// authorization. Approve it so the tool can proceed.
|
|
let authorization = event_stream_rx.expect_authorization().await;
|
|
authorization
|
|
.response
|
|
.send(acp_thread::SelectedPermissionOutcome::new(
|
|
agent_client_protocol::schema::v1::PermissionOptionId::new("allow"),
|
|
agent_client_protocol::schema::v1::PermissionOptionKind::AllowOnce,
|
|
))
|
|
.unwrap();
|
|
|
|
let output = task.await.unwrap();
|
|
cx.run_until_parked();
|
|
let expected = format!(
|
|
concat!(
|
|
"<skill_content name=\"test-skill\">\n",
|
|
"<source>project-local</source>\n",
|
|
"<worktree>project</worktree>\n",
|
|
"<directory>{}</directory>\n",
|
|
"Relative paths in this skill resolve against <directory>.\n",
|
|
"\n",
|
|
"test body\n",
|
|
"</skill_content>\n",
|
|
),
|
|
path!("/project/.agents/skills/test-skill"),
|
|
);
|
|
assert_eq!(output, SkillToolOutput::Found { rendered: expected });
|
|
|
|
fs.create_dir(Path::new(path!("/project/.agents/skills/test-2")))
|
|
.await
|
|
.unwrap();
|
|
fs.insert_file(
|
|
path!("/project/.agents/skills/test-2/SKILL.md"),
|
|
"---\nname: test-2\ndescription: test description\n---\ntest body"
|
|
.as_bytes()
|
|
.into(),
|
|
)
|
|
.await;
|
|
|
|
cx.run_until_parked();
|
|
cx.update(|cx| connection.refresh_skills_for_project(project, cx));
|
|
cx.run_until_parked();
|
|
|
|
let input2 = SkillToolInput {
|
|
name: "test-2".into(),
|
|
};
|
|
let task = cx.update(|cx| {
|
|
skill_tool
|
|
.clone()
|
|
.run(ToolInput::resolved(input2), event_stream.clone(), cx)
|
|
});
|
|
|
|
let authorization = event_stream_rx.expect_authorization().await;
|
|
authorization
|
|
.response
|
|
.send(acp_thread::SelectedPermissionOutcome::new(
|
|
agent_client_protocol::schema::v1::PermissionOptionId::new("allow"),
|
|
agent_client_protocol::schema::v1::PermissionOptionKind::AllowOnce,
|
|
))
|
|
.unwrap();
|
|
|
|
let output = task.await.unwrap();
|
|
let expected2 = format!(
|
|
concat!(
|
|
"<skill_content name=\"test-2\">\n",
|
|
"<source>project-local</source>\n",
|
|
"<worktree>project</worktree>\n",
|
|
"<directory>{}</directory>\n",
|
|
"Relative paths in this skill resolve against <directory>.\n",
|
|
"\n",
|
|
"test body\n",
|
|
"</skill_content>\n",
|
|
),
|
|
path!("/project/.agents/skills/test-2"),
|
|
);
|
|
assert_eq!(
|
|
output,
|
|
SkillToolOutput::Found {
|
|
rendered: expected2
|
|
}
|
|
);
|
|
}
|
|
|
|
#[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");
|
|
});
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_remote_load_commit_template(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
|
|
let fs = FakeFs::new(server_cx.executor());
|
|
|
|
fs.insert_tree(
|
|
path!("/root"),
|
|
json!({
|
|
"project": {
|
|
".git": {},
|
|
"README.md": "# Project's README"
|
|
}
|
|
}),
|
|
)
|
|
.await;
|
|
|
|
fs.with_git_state(Path::new(path!("/root/project/.git")), false, |state| {
|
|
state.commit_template = Some(GitCommitTemplate {
|
|
template: "chore: commit template".to_string(),
|
|
})
|
|
})
|
|
.expect("Should successfully update repository state");
|
|
|
|
let (project, _) = init_test(&fs, cx, server_cx).await;
|
|
project
|
|
.update(cx, |project, cx| {
|
|
project.find_or_create_worktree(path!("/root/project"), true, cx)
|
|
})
|
|
.await
|
|
.unwrap();
|
|
cx.run_until_parked();
|
|
|
|
let repository = project.update(cx, |project, cx| project.active_repository(cx).unwrap());
|
|
let commit_template = repository
|
|
.update(cx, |repository, _| repository.load_commit_template_text())
|
|
.await
|
|
.unwrap()
|
|
.unwrap()
|
|
.expect("Loading commit template in remote should work");
|
|
|
|
assert_eq!(commit_template.template, "chore: commit template");
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_remote_delete_project_entry_with_trash(
|
|
cx: &mut TestAppContext,
|
|
server_cx: &mut TestAppContext,
|
|
) {
|
|
let fs = FakeFs::new(server_cx.executor());
|
|
|
|
fs.insert_tree(
|
|
path!("/root"),
|
|
json!({
|
|
"zed": {
|
|
"file_a.txt": "File A"
|
|
}
|
|
}),
|
|
)
|
|
.await;
|
|
|
|
let (project, _headless_project) = init_test(&fs, cx, server_cx).await;
|
|
let (worktree, _path) = project
|
|
.update(cx, |project, cx| {
|
|
project.find_or_create_worktree(path!("/root/zed"), true, cx)
|
|
})
|
|
.await
|
|
.unwrap();
|
|
cx.run_until_parked();
|
|
|
|
let entry_id = worktree.update(cx, |worktree, _cx| {
|
|
worktree.entry_for_path(rel_path("file_a.txt")).unwrap().id
|
|
});
|
|
|
|
let remote_client = project.read_with(cx, |project, _cx| {
|
|
project
|
|
.remote_client()
|
|
.expect("project should have a remote client")
|
|
});
|
|
|
|
let proto_client =
|
|
remote_client.read_with(cx, |remote_client, _cx| remote_client.proto_client());
|
|
|
|
proto_client
|
|
.request(proto::DeleteProjectEntry {
|
|
project_id: proto::REMOTE_SERVER_PROJECT_ID,
|
|
entry_id: entry_id.to_proto(),
|
|
#[allow(deprecated)]
|
|
use_trash: true,
|
|
})
|
|
.await
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
fs.trashed_paths(),
|
|
vec![PathBuf::from(path!("/root/zed/file_a.txt"))]
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_remote_trash_restore(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
|
|
let fs = FakeFs::new(server_cx.executor());
|
|
|
|
fs.insert_tree(
|
|
path!("/root"),
|
|
json!({
|
|
"zed": {
|
|
"file_a.txt": "File A"
|
|
}
|
|
}),
|
|
)
|
|
.await;
|
|
|
|
let (project, _headless_project) = init_test(&fs, cx, server_cx).await;
|
|
let (worktree, _path) = project
|
|
.update(cx, |project, cx| {
|
|
project.find_or_create_worktree(path!("/root/zed"), true, cx)
|
|
})
|
|
.await
|
|
.unwrap();
|
|
cx.run_until_parked();
|
|
|
|
let worktree_id = worktree.update(cx, |worktree, _cx| worktree.id());
|
|
let entry_id = worktree.update(cx, |worktree, _cx| {
|
|
worktree.entry_for_path(rel_path("file_a.txt")).unwrap().id
|
|
});
|
|
|
|
let trash_id = project
|
|
.update(cx, |project, cx| project.trash_entry(entry_id, cx))
|
|
.unwrap()
|
|
.await
|
|
.unwrap();
|
|
cx.run_until_parked();
|
|
|
|
worktree.update(cx, |worktree, _cx| {
|
|
assert!(worktree.entry_for_path(rel_path("file_a.txt")).is_none());
|
|
});
|
|
|
|
project
|
|
.update(cx, |project, cx| {
|
|
project.restore_entry(worktree_id, trash_id, cx)
|
|
})
|
|
.await
|
|
.unwrap();
|
|
cx.run_until_parked();
|
|
|
|
worktree.update(cx, |worktree, _cx| {
|
|
assert!(worktree.entry_for_path(rel_path("file_a.txt")).is_some());
|
|
});
|
|
}
|
|
|
|
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))
|
|
}
|