Skip to repository content4311 lines · 131.2 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T01:43:34.448Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
remote_editing_tests.rs
1/// todo(windows)
2/// The tests in this file assume that server_cx is running on Windows too.
3/// We neead to find a way to test Windows-Non-Windows interactions.
4use crate::headless_project::HeadlessProject;
5use agent::{
6 AgentTool, NativeAgent, NativeAgentConnection, ReadFileTool, ReadFileToolInput, SkillTool,
7 SkillToolInput, SkillToolOutput, Templates, ThreadStore, ToolCallEventStream, ToolInput,
8 skill_body_resolver_for_project, skills_resolver_for_project,
9};
10use client::{Client, UserStore};
11use clock::FakeSystemClock;
12use collections::{HashMap, HashSet};
13use language_model::{LanguageModelRegistry, LanguageModelToolResultContent};
14use languages::rust_lang;
15
16use editor::{
17 Editor, SelectionEffects,
18 actions::{ConfirmCodeAction, ToggleCodeActions},
19 code_context_menus::CodeContextMenu,
20};
21use extension::ExtensionHostProxy;
22use fs::{FakeFs, Fs};
23use git::{
24 Oid,
25 repository::{CommitData, GitCommitTemplate, RepoPath, Worktree as GitWorktree},
26};
27use gpui::{AppContext as _, Entity, SharedString, TestAppContext, UpdateGlobal, VisualContext};
28use http_client::{BlockedHttpClient, FakeHttpClient};
29use language::{
30 Buffer, FakeLspAdapter, LanguageConfig, LanguageMatcher, LanguageRegistry, LineEnding, Point,
31 language_settings::{AllLanguageSettings, ConfiguredLanguageServer, LanguageSettings},
32};
33use lsp::{
34 CompletionContext, CompletionResponse, CompletionTriggerKind, DEFAULT_LSP_REQUEST_TIMEOUT,
35 LanguageServerName,
36};
37use node_runtime::NodeRuntime;
38use project::{
39 ProgressToken, Project,
40 agent_server_store::AgentServerCommand,
41 search::{SearchQuery, SearchResult},
42};
43use remote::RemoteClient;
44use rpc::proto;
45use serde_json::json;
46use settings::{Settings, SettingsLocation, SettingsStore, initial_server_settings_content};
47use smol::stream::StreamExt;
48use std::{
49 path::{Path, PathBuf},
50 rc::Rc,
51 str::FromStr,
52 sync::{
53 Arc,
54 atomic::{AtomicUsize, Ordering},
55 },
56};
57use unindent::Unindent as _;
58use util::{path, path_list::PathList, paths::PathMatcher, rel_path::rel_path};
59
60#[gpui::test]
61async fn test_basic_remote_editing(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
62 let fs = FakeFs::new(server_cx.executor());
63 fs.insert_tree(
64 path!("/code"),
65 json!({
66 "project1": {
67 ".git": {},
68 "README.md": "# project 1",
69 "src": {
70 "lib.rs": "fn one() -> usize { 1 }"
71 }
72 },
73 "project2": {
74 "README.md": "# project 2",
75 },
76 }),
77 )
78 .await;
79 fs.set_index_for_repo(
80 Path::new(path!("/code/project1/.git")),
81 &[("src/lib.rs", "fn one() -> usize { 0 }".into())],
82 );
83
84 let (project, _headless) = init_test(&fs, cx, server_cx).await;
85 let (worktree, _) = project
86 .update(cx, |project, cx| {
87 project.find_or_create_worktree(path!("/code/project1"), true, cx)
88 })
89 .await
90 .unwrap();
91
92 // The client sees the worktree's contents.
93 cx.executor().run_until_parked();
94 let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
95 worktree.update(cx, |worktree, _cx| {
96 assert_eq!(
97 worktree.paths().collect::<Vec<_>>(),
98 vec![
99 rel_path("README.md"),
100 rel_path("src"),
101 rel_path("src/lib.rs"),
102 ]
103 );
104 });
105
106 // The user opens a buffer in the remote worktree. The buffer's
107 // contents are loaded from the remote filesystem.
108 let buffer = project
109 .update(cx, |project, cx| {
110 project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
111 })
112 .await
113 .unwrap();
114 let diff = project
115 .update(cx, |project, cx| {
116 project.open_unstaged_diff(buffer.clone(), cx)
117 })
118 .await
119 .unwrap();
120
121 diff.update(cx, |diff, cx| {
122 assert_eq!(
123 diff.base_text_string(cx).unwrap(),
124 "fn one() -> usize { 0 }"
125 );
126 });
127
128 buffer.update(cx, |buffer, cx| {
129 assert_eq!(buffer.text(), "fn one() -> usize { 1 }");
130 let ix = buffer.text().find('1').unwrap();
131 buffer.edit([(ix..ix + 1, "100")], None, cx);
132 });
133
134 // The user saves the buffer. The new contents are written to the
135 // remote filesystem.
136 project
137 .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
138 .await
139 .unwrap();
140 assert_eq!(
141 fs.load("/code/project1/src/lib.rs".as_ref()).await.unwrap(),
142 "fn one() -> usize { 100 }"
143 );
144
145 // A new file is created in the remote filesystem. The user
146 // sees the new file.
147 fs.save(
148 path!("/code/project1/src/main.rs").as_ref(),
149 &"fn main() {}".into(),
150 Default::default(),
151 )
152 .await
153 .unwrap();
154 cx.executor().run_until_parked();
155 worktree.update(cx, |worktree, _cx| {
156 assert_eq!(
157 worktree.paths().collect::<Vec<_>>(),
158 vec![
159 rel_path("README.md"),
160 rel_path("src"),
161 rel_path("src/lib.rs"),
162 rel_path("src/main.rs"),
163 ]
164 );
165 });
166
167 // A file that is currently open in a buffer is renamed.
168 fs.rename(
169 path!("/code/project1/src/lib.rs").as_ref(),
170 path!("/code/project1/src/lib2.rs").as_ref(),
171 Default::default(),
172 )
173 .await
174 .unwrap();
175 cx.executor().run_until_parked();
176 buffer.update(cx, |buffer, _| {
177 assert_eq!(&**buffer.file().unwrap().path(), rel_path("src/lib2.rs"));
178 });
179
180 fs.set_index_for_repo(
181 Path::new(path!("/code/project1/.git")),
182 &[("src/lib2.rs", "fn one() -> usize { 100 }".into())],
183 );
184 cx.executor().run_until_parked();
185 diff.update(cx, |diff, cx| {
186 assert_eq!(
187 diff.base_text_string(cx).unwrap(),
188 "fn one() -> usize { 100 }"
189 );
190 });
191}
192
193#[gpui::test]
194async fn test_remote_telemetry_event_forwarding(
195 cx: &mut TestAppContext,
196 server_cx: &mut TestAppContext,
197) {
198 // This mirrors `init_test`, but retains the server-side session so the test
199 // can drive a forwarded telemetry event over the proto channel (as
200 // `init_telemetry_forwarding` does on a real remote server).
201 let server_fs = FakeFs::new(server_cx.executor());
202 server_fs
203 .insert_tree(
204 path!("/code"),
205 json!({ "project1": { "README.md": "# project 1" } }),
206 )
207 .await;
208
209 cx.update(|cx| release_channel::init(semver::Version::new(0, 0, 0), cx));
210 server_cx.update(|cx| release_channel::init(semver::Version::new(0, 0, 0), cx));
211 init_logger();
212
213 let (opts, server_session, _) = RemoteClient::fake_server(cx, server_cx);
214 let http_client = Arc::new(BlockedHttpClient);
215 let node_runtime = NodeRuntime::unavailable();
216 let languages = Arc::new(LanguageRegistry::new(cx.executor()));
217 let proxy = Arc::new(ExtensionHostProxy::new());
218 server_cx.update(HeadlessProject::init);
219 let headless = server_cx.new(|cx| {
220 HeadlessProject::new(
221 crate::HeadlessAppState {
222 session: server_session.clone(),
223 fs: server_fs.clone(),
224 http_client,
225 node_runtime,
226 languages,
227 extension_host_proxy: proxy,
228 startup_time: std::time::Instant::now(),
229 },
230 false,
231 cx,
232 )
233 });
234
235 let ssh = RemoteClient::connect_mock(opts, cx).await;
236 let project = build_project(ssh, cx);
237 project
238 .update(cx, {
239 let headless = headless.clone();
240 |_, cx| cx.on_release(|_, _| drop(headless))
241 })
242 .detach();
243
244 // The remote server forwards a bare `FlexibleEvent` as JSON; mirror that
245 // here by sending the proto message the forwarding task would send.
246 let event_json = json!({
247 "event_type": "fs_watcher_poll",
248 "event_properties": { "path": "/code/project1" },
249 })
250 .to_string();
251 server_session
252 .send(proto::TelemetryEvent {
253 project_id: proto::REMOTE_SERVER_PROJECT_ID,
254 event_json,
255 })
256 .unwrap();
257 cx.executor().run_until_parked();
258
259 let events = project.read_with(cx, |project, _| {
260 project.client().telemetry().queued_events()
261 });
262 assert_eq!(
263 events.len(),
264 1,
265 "the forwarded event should be reported once"
266 );
267 let event = &events[0];
268 assert_eq!(event.event_type, "fs_watcher_poll");
269 // The event's original properties survive the round-trip.
270 assert_eq!(
271 event.event_properties.get("path"),
272 Some(&serde_json::Value::String("/code/project1".to_string()))
273 );
274 // The client stamps the remote host metadata it learned at connection time.
275 // The mock connection reports a Linux/x86_64 host over a "mock" connection.
276 assert_eq!(
277 event.event_properties.get("remote"),
278 Some(&serde_json::Value::Bool(true))
279 );
280 assert_eq!(
281 event.event_properties.get("remote_connection_type"),
282 Some(&serde_json::Value::String("mock".to_string()))
283 );
284 assert_eq!(
285 event.event_properties.get("remote_os_name"),
286 Some(&serde_json::Value::String("Linux".to_string()))
287 );
288 assert_eq!(
289 event.event_properties.get("remote_architecture"),
290 Some(&serde_json::Value::String("x86_64".to_string()))
291 );
292}
293
294async fn do_search_and_assert(
295 project: &Entity<Project>,
296 query: &str,
297 files_to_include: PathMatcher,
298 match_full_paths: bool,
299 expected_paths: &[&str],
300 mut cx: TestAppContext,
301) -> Vec<Entity<Buffer>> {
302 let query = query.to_string();
303 let receiver = project.update(&mut cx, |project, cx| {
304 project.search(
305 SearchQuery::text(
306 query,
307 false,
308 true,
309 false,
310 files_to_include,
311 Default::default(),
312 match_full_paths,
313 None,
314 )
315 .unwrap(),
316 cx,
317 )
318 });
319
320 let mut buffers = Vec::new();
321 for expected_path in expected_paths {
322 let buffer = loop {
323 let response = receiver.rx.recv().await.unwrap();
324 match response {
325 SearchResult::Buffer { buffer, .. } => break buffer,
326 SearchResult::LimitReached => panic!("incorrect result"),
327 SearchResult::WaitingForScan | SearchResult::Searching => continue,
328 }
329 };
330 buffer.update(&mut cx, |buffer, cx| {
331 assert_eq!(
332 buffer.file().unwrap().full_path(cx).to_string_lossy(),
333 *expected_path
334 )
335 });
336 buffers.push(buffer);
337 }
338
339 assert!(receiver.rx.recv().await.is_err());
340 buffers
341}
342
343#[gpui::test]
344async fn test_remote_project_search(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
345 let fs = FakeFs::new(server_cx.executor());
346 fs.insert_tree(
347 path!("/code"),
348 json!({
349 "project1": {
350 ".git": {},
351 "README.md": "# project 1",
352 "src": {
353 "lib.rs": "fn one() -> usize { 1 }"
354 }
355 },
356 }),
357 )
358 .await;
359
360 let (project, headless) = init_test(&fs, cx, server_cx).await;
361
362 project
363 .update(cx, |project, cx| {
364 project.find_or_create_worktree(path!("/code/project1"), true, cx)
365 })
366 .await
367 .unwrap();
368
369 cx.run_until_parked();
370
371 let buffers = do_search_and_assert(
372 &project,
373 "project",
374 Default::default(),
375 false,
376 &[path!("project1/README.md")],
377 cx.clone(),
378 )
379 .await;
380 let buffer = buffers.into_iter().next().unwrap();
381
382 // test that the headless server is tracking which buffers we have open correctly.
383 cx.run_until_parked();
384 headless.update(server_cx, |headless, cx| {
385 assert!(headless.buffer_store.read(cx).has_shared_buffers())
386 });
387 do_search_and_assert(
388 &project,
389 "project",
390 Default::default(),
391 false,
392 &[path!("project1/README.md")],
393 cx.clone(),
394 )
395 .await;
396 server_cx.run_until_parked();
397 cx.update(|_| {
398 drop(buffer);
399 });
400 cx.run_until_parked();
401 server_cx.run_until_parked();
402 headless.update(server_cx, |headless, cx| {
403 assert!(!headless.buffer_store.read(cx).has_shared_buffers())
404 });
405
406 do_search_and_assert(
407 &project,
408 "project",
409 Default::default(),
410 false,
411 &[path!("project1/README.md")],
412 cx.clone(),
413 )
414 .await;
415}
416
417#[gpui::test]
418async fn test_remote_project_search_single_cpu(
419 cx: &mut TestAppContext,
420 server_cx: &mut TestAppContext,
421) {
422 let fs = FakeFs::new(server_cx.executor());
423 fs.insert_tree(
424 path!("/code"),
425 json!({
426 "project1": {
427 ".git": {},
428 "README.md": "# project 1",
429 "src": {
430 "lib.rs": "fn one() -> usize { 1 }"
431 }
432 },
433 }),
434 )
435 .await;
436
437 // Simulate a single-CPU environment (e.g. a devcontainer with 1 visible CPU).
438 // This causes the worker pool in project search to spawn num_cpus - 1 = 0 workers,
439 // which silently drops all search channels and produces zero results.
440 server_cx.executor().set_num_cpus(1);
441
442 let (project, _) = init_test(&fs, cx, server_cx).await;
443
444 project
445 .update(cx, |project, cx| {
446 project.find_or_create_worktree(path!("/code/project1"), true, cx)
447 })
448 .await
449 .unwrap();
450
451 cx.run_until_parked();
452
453 do_search_and_assert(
454 &project,
455 "project",
456 Default::default(),
457 false,
458 &[path!("project1/README.md")],
459 cx.clone(),
460 )
461 .await;
462}
463
464#[gpui::test]
465async fn test_remote_project_search_inclusion(
466 cx: &mut TestAppContext,
467 server_cx: &mut TestAppContext,
468) {
469 let fs = FakeFs::new(server_cx.executor());
470 fs.insert_tree(
471 path!("/code"),
472 json!({
473 "project1": {
474 "README.md": "# project 1",
475 },
476 "project2": {
477 "README.md": "# project 2",
478 },
479 }),
480 )
481 .await;
482
483 let (project, _) = init_test(&fs, cx, server_cx).await;
484
485 project
486 .update(cx, |project, cx| {
487 project.find_or_create_worktree(path!("/code/project1"), true, cx)
488 })
489 .await
490 .unwrap();
491
492 project
493 .update(cx, |project, cx| {
494 project.find_or_create_worktree(path!("/code/project2"), true, cx)
495 })
496 .await
497 .unwrap();
498
499 cx.run_until_parked();
500
501 // Case 1: Test search with path matcher limiting to only one worktree
502 let path_matcher = PathMatcher::new(
503 &["project1/*.md".to_owned()],
504 util::paths::PathStyle::local(),
505 )
506 .unwrap();
507 do_search_and_assert(
508 &project,
509 "project",
510 path_matcher,
511 true, // should be true in case of multiple worktrees
512 &[path!("project1/README.md")],
513 cx.clone(),
514 )
515 .await;
516
517 // Case 2: Test search without path matcher, matching both worktrees
518 do_search_and_assert(
519 &project,
520 "project",
521 Default::default(),
522 true, // should be true in case of multiple worktrees
523 &[path!("project1/README.md"), path!("project2/README.md")],
524 cx.clone(),
525 )
526 .await;
527}
528
529#[gpui::test]
530async fn test_remote_settings(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
531 let fs = FakeFs::new(server_cx.executor());
532 fs.insert_tree(
533 "/code",
534 json!({
535 "project1": {
536 ".git": {},
537 "README.md": "# project 1",
538 "src": {
539 "lib.rs": "fn one() -> usize { 1 }"
540 }
541 },
542 }),
543 )
544 .await;
545
546 let (project, headless) = init_test(&fs, cx, server_cx).await;
547
548 cx.update_global(|settings_store: &mut SettingsStore, cx| {
549 settings_store.set_user_settings(
550 r#"{"languages":{"Rust":{"language_servers":["from-local-settings"]}}}"#,
551 cx,
552 )
553 })
554 .unwrap();
555
556 cx.run_until_parked();
557
558 server_cx.read(|cx| {
559 assert_eq!(
560 AllLanguageSettings::get_global(cx)
561 .language(None, Some(&"Rust".into()), cx)
562 .language_servers,
563 [ConfiguredLanguageServer::new("from-local-settings")],
564 "User language settings should be synchronized with the server settings"
565 )
566 });
567
568 server_cx
569 .update_global(|settings_store: &mut SettingsStore, cx| {
570 settings_store.set_server_settings(
571 r#"{"languages":{"Rust":{"language_servers":["from-server-settings"]}}}"#,
572 cx,
573 )
574 })
575 .unwrap();
576
577 cx.run_until_parked();
578
579 server_cx.read(|cx| {
580 assert_eq!(
581 AllLanguageSettings::get_global(cx)
582 .language(None, Some(&"Rust".into()), cx)
583 .language_servers,
584 [ConfiguredLanguageServer::new("from-server-settings")],
585 "Server language settings should take precedence over the user settings"
586 )
587 });
588
589 fs.insert_tree(
590 "/code/project1/.zed",
591 json!({
592 "settings.json": r#"
593 {
594 "languages": {"Rust":{"language_servers":["override-rust-analyzer"]}},
595 "lsp": {
596 "override-rust-analyzer": {
597 "binary": {
598 "path": "~/.cargo/bin/rust-analyzer"
599 }
600 }
601 }
602 }"#
603 }),
604 )
605 .await;
606
607 let worktree_id = project
608 .update(cx, |project, cx| {
609 project.languages().add(rust_lang());
610 project.find_or_create_worktree("/code/project1", true, cx)
611 })
612 .await
613 .unwrap()
614 .0
615 .read_with(cx, |worktree, _| worktree.id());
616
617 let buffer = project
618 .update(cx, |project, cx| {
619 project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
620 })
621 .await
622 .unwrap();
623 cx.run_until_parked();
624
625 server_cx.read(|cx| {
626 let worktree_id = headless
627 .read(cx)
628 .worktree_store
629 .read(cx)
630 .worktrees()
631 .next()
632 .unwrap()
633 .read(cx)
634 .id();
635 assert_eq!(
636 AllLanguageSettings::get(
637 Some(SettingsLocation {
638 worktree_id,
639 path: rel_path("src/lib.rs")
640 }),
641 cx
642 )
643 .language(None, Some(&"Rust".into()), cx)
644 .language_servers,
645 [ConfiguredLanguageServer::new("override-rust-analyzer")]
646 )
647 });
648
649 cx.read(|cx| {
650 assert_eq!(
651 LanguageSettings::for_buffer(buffer.read(cx), cx).language_servers,
652 [ConfiguredLanguageServer::new("override-rust-analyzer")]
653 )
654 });
655}
656
657#[gpui::test]
658async fn test_remote_lsp(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
659 let fs = FakeFs::new(server_cx.executor());
660 fs.insert_tree(
661 path!("/code"),
662 json!({
663 "project1": {
664 ".git": {},
665 "README.md": "# project 1",
666 "src": {
667 "lib.rs": "fn one() -> usize { 1 }"
668 }
669 },
670 }),
671 )
672 .await;
673
674 let (project, headless) = init_test(&fs, cx, server_cx).await;
675
676 fs.insert_tree(
677 path!("/code/project1/.zed"),
678 json!({
679 "settings.json": r#"
680 {
681 "languages": {"Rust":{"language_servers":["rust-analyzer", "fake-analyzer"]}},
682 "lsp": {
683 "rust-analyzer": {
684 "binary": {
685 "path": "~/.cargo/bin/rust-analyzer"
686 }
687 },
688 "fake-analyzer": {
689 "binary": {
690 "path": "~/.cargo/bin/rust-analyzer"
691 }
692 }
693 }
694 }"#
695 }),
696 )
697 .await;
698
699 cx.update_entity(&project, |project, _| {
700 project.languages().register_test_language(LanguageConfig {
701 name: "Rust".into(),
702 matcher: (LanguageMatcher {
703 path_suffixes: vec!["rs".into()],
704 ..Default::default()
705 })
706 .into(),
707 ..Default::default()
708 });
709 project.languages().register_fake_lsp_adapter(
710 "Rust",
711 FakeLspAdapter {
712 name: "rust-analyzer",
713 capabilities: lsp::ServerCapabilities {
714 completion_provider: Some(lsp::CompletionOptions::default()),
715 rename_provider: Some(lsp::OneOf::Left(true)),
716 ..lsp::ServerCapabilities::default()
717 },
718 ..FakeLspAdapter::default()
719 },
720 );
721 project.languages().register_fake_lsp_adapter(
722 "Rust",
723 FakeLspAdapter {
724 name: "fake-analyzer",
725 capabilities: lsp::ServerCapabilities {
726 completion_provider: Some(lsp::CompletionOptions::default()),
727 rename_provider: Some(lsp::OneOf::Left(true)),
728 ..lsp::ServerCapabilities::default()
729 },
730 ..FakeLspAdapter::default()
731 },
732 )
733 });
734
735 let mut fake_lsp = server_cx.update(|cx| {
736 headless.read(cx).languages.register_fake_lsp_server(
737 LanguageServerName("rust-analyzer".into()),
738 lsp::ServerCapabilities {
739 completion_provider: Some(lsp::CompletionOptions::default()),
740 rename_provider: Some(lsp::OneOf::Left(true)),
741 ..lsp::ServerCapabilities::default()
742 },
743 None,
744 )
745 });
746
747 let mut fake_second_lsp = server_cx.update(|cx| {
748 headless.read(cx).languages.register_fake_lsp_adapter(
749 "Rust",
750 FakeLspAdapter {
751 name: "fake-analyzer",
752 capabilities: lsp::ServerCapabilities {
753 completion_provider: Some(lsp::CompletionOptions::default()),
754 rename_provider: Some(lsp::OneOf::Left(true)),
755 ..lsp::ServerCapabilities::default()
756 },
757 ..FakeLspAdapter::default()
758 },
759 );
760 headless.read(cx).languages.register_fake_lsp_server(
761 LanguageServerName("fake-analyzer".into()),
762 lsp::ServerCapabilities {
763 completion_provider: Some(lsp::CompletionOptions::default()),
764 rename_provider: Some(lsp::OneOf::Left(true)),
765 ..lsp::ServerCapabilities::default()
766 },
767 None,
768 )
769 });
770
771 cx.run_until_parked();
772
773 let worktree_id = project
774 .update(cx, |project, cx| {
775 project.languages().add(rust_lang());
776 project.find_or_create_worktree(path!("/code/project1"), true, cx)
777 })
778 .await
779 .unwrap()
780 .0
781 .read_with(cx, |worktree, _| worktree.id());
782
783 // Wait for the settings to synchronize
784 cx.run_until_parked();
785
786 let (buffer, _handle) = project
787 .update(cx, |project, cx| {
788 project.open_buffer_with_lsp((worktree_id, rel_path("src/lib.rs")), cx)
789 })
790 .await
791 .unwrap();
792 cx.run_until_parked();
793
794 let fake_lsp = fake_lsp.next().await.unwrap();
795 let fake_second_lsp = fake_second_lsp.next().await.unwrap();
796
797 cx.read(|cx| {
798 assert_eq!(
799 LanguageSettings::for_buffer(buffer.read(cx), cx).language_servers,
800 [
801 ConfiguredLanguageServer::new("rust-analyzer"),
802 ConfiguredLanguageServer::new("fake-analyzer"),
803 ]
804 )
805 });
806
807 let buffer_id = cx.read(|cx| {
808 let buffer = buffer.read(cx);
809 assert_eq!(buffer.language().unwrap().name(), "Rust");
810 buffer.remote_id()
811 });
812
813 server_cx.read(|cx| {
814 let buffer = headless
815 .read(cx)
816 .buffer_store
817 .read(cx)
818 .get(buffer_id)
819 .unwrap();
820
821 assert_eq!(buffer.read(cx).language().unwrap().name(), "Rust");
822 });
823
824 server_cx.read(|cx| {
825 let lsp_store = headless.read(cx).lsp_store.read(cx);
826 assert_eq!(lsp_store.as_local().unwrap().language_servers.len(), 2);
827 });
828
829 fake_lsp.set_request_handler::<lsp::request::Completion, _, _>(|_, _| async move {
830 Ok(Some(CompletionResponse::Array(vec![lsp::CompletionItem {
831 label: "boop".to_string(),
832 ..Default::default()
833 }])))
834 });
835
836 fake_second_lsp.set_request_handler::<lsp::request::Completion, _, _>(|_, _| async move {
837 Ok(Some(CompletionResponse::Array(vec![lsp::CompletionItem {
838 label: "beep".to_string(),
839 ..Default::default()
840 }])))
841 });
842
843 let result = project
844 .update(cx, |project, cx| {
845 project.completions(
846 &buffer,
847 0,
848 CompletionContext {
849 trigger_kind: CompletionTriggerKind::INVOKED,
850 trigger_character: None,
851 },
852 cx,
853 )
854 })
855 .await
856 .unwrap();
857
858 assert_eq!(
859 result
860 .into_iter()
861 .flat_map(|response| response.completions)
862 .map(|c| c.label.text)
863 .collect::<Vec<_>>(),
864 vec!["boop".to_string(), "beep".to_string()]
865 );
866
867 fake_lsp.set_request_handler::<lsp::request::Rename, _, _>(|_, _| async move {
868 Ok(Some(lsp::WorkspaceEdit {
869 changes: Some(
870 [(
871 lsp::Uri::from_file_path(path!("/code/project1/src/lib.rs")).unwrap(),
872 vec![lsp::TextEdit::new(
873 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 6)),
874 "two".to_string(),
875 )],
876 )]
877 .into_iter()
878 .collect(),
879 ),
880 ..Default::default()
881 }))
882 });
883
884 project
885 .update(cx, |project, cx| {
886 project.perform_rename(buffer.clone(), 3, "two".to_string(), cx)
887 })
888 .await
889 .unwrap();
890
891 cx.run_until_parked();
892 buffer.update(cx, |buffer, _| {
893 assert_eq!(buffer.text(), "fn two() -> usize { 1 }")
894 })
895}
896
897#[gpui::test]
898async fn test_remote_code_action_resolve(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
899 cx.update(|cx| {
900 let settings_store = SettingsStore::test(cx);
901 cx.set_global(settings_store);
902 theme_settings::init(theme::LoadThemes::JustBase, cx);
903 release_channel::init(semver::Version::new(0, 0, 0), cx);
904 editor::init(cx);
905 });
906
907 let fs = FakeFs::new(server_cx.executor());
908 fs.insert_tree(
909 path!("/code"),
910 json!({
911 "project1": {
912 ".git": {},
913 "src": {
914 "lib.rs": "fn one() -> usize { 1 }"
915 }
916 },
917 }),
918 )
919 .await;
920
921 let (project, headless) = init_test(&fs, cx, server_cx).await;
922
923 let capabilities = lsp::ServerCapabilities {
924 code_action_provider: Some(lsp::CodeActionProviderCapability::Options(
925 lsp::CodeActionOptions {
926 resolve_provider: Some(true),
927 ..lsp::CodeActionOptions::default()
928 },
929 )),
930 ..lsp::ServerCapabilities::default()
931 };
932
933 cx.update_entity(&project, |project, _| {
934 project.languages().register_test_language(LanguageConfig {
935 name: "Rust".into(),
936 matcher: (LanguageMatcher {
937 path_suffixes: vec!["rs".into()],
938 ..Default::default()
939 })
940 .into(),
941 ..Default::default()
942 });
943 project.languages().register_fake_lsp_adapter(
944 "Rust",
945 FakeLspAdapter {
946 name: "rust-analyzer",
947 capabilities: capabilities.clone(),
948 ..FakeLspAdapter::default()
949 },
950 );
951 });
952
953 server_cx.update(|cx| {
954 headless.read(cx).languages.register_fake_lsp_server(
955 LanguageServerName("rust-analyzer".into()),
956 capabilities,
957 Some(Box::new(|fake_lsp| {
958 fake_lsp.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
959 |_, _| async move {
960 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
961 lsp::CodeAction {
962 title: "Use two".to_string(),
963 data: Some(serde_json::json!({ "id": "action" })),
964 ..lsp::CodeAction::default()
965 },
966 )]))
967 },
968 );
969 fake_lsp.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>(
970 |mut action, _| async move {
971 action.edit = Some(lsp::WorkspaceEdit {
972 changes: Some(
973 [(
974 lsp::Uri::from_file_path(path!("/code/project1/src/lib.rs"))
975 .unwrap(),
976 vec![lsp::TextEdit::new(
977 lsp::Range::new(
978 lsp::Position::new(0, 3),
979 lsp::Position::new(0, 6),
980 ),
981 "two".to_string(),
982 )],
983 )]
984 .into_iter()
985 .collect(),
986 ),
987 ..lsp::WorkspaceEdit::default()
988 });
989 Ok(action)
990 },
991 );
992 })),
993 );
994 });
995
996 cx.run_until_parked();
997
998 let worktree_id = project
999 .update(cx, |project, cx| {
1000 project.languages().add(rust_lang());
1001 project.find_or_create_worktree(path!("/code/project1"), true, cx)
1002 })
1003 .await
1004 .unwrap()
1005 .0
1006 .read_with(cx, |worktree, _| worktree.id());
1007 cx.run_until_parked();
1008
1009 let (buffer, _handle) = project
1010 .update(cx, |project, cx| {
1011 project.open_buffer_with_lsp((worktree_id, rel_path("src/lib.rs")), cx)
1012 })
1013 .await
1014 .unwrap();
1015 cx.run_until_parked();
1016
1017 let cx = cx.add_empty_window();
1018 let workspace = cx.new_window_entity(|window, cx| {
1019 workspace::Workspace::test_new(project.clone(), window, cx)
1020 });
1021 let editor = cx.new_window_entity(|window, cx| {
1022 Editor::for_buffer(buffer.clone(), Some(project.clone()), window, cx)
1023 });
1024 workspace.update_in(cx, |workspace, window, cx| {
1025 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
1026 });
1027 cx.run_until_parked();
1028
1029 editor.update_in(cx, |editor, window, cx| {
1030 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
1031 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)]);
1032 });
1033 });
1034 cx.executor()
1035 .advance_clock(editor::CODE_ACTIONS_DEBOUNCE_TIMEOUT * 2);
1036 cx.run_until_parked();
1037
1038 editor.update_in(cx, |editor, window, cx| {
1039 editor.toggle_code_actions(
1040 &ToggleCodeActions {
1041 deployed_from: None,
1042 quick_launch: false,
1043 },
1044 window,
1045 cx,
1046 );
1047 });
1048 cx.run_until_parked();
1049
1050 editor.update(cx, |editor, _| assert!(editor.context_menu_visible()));
1051
1052 let confirm_action = editor
1053 .update_in(cx, |editor, window, cx| {
1054 Editor::confirm_code_action(editor, &ConfirmCodeAction { item_ix: Some(0) }, window, cx)
1055 })
1056 .unwrap();
1057 confirm_action.await.unwrap();
1058
1059 buffer.read_with(cx, |buffer, _| {
1060 assert_eq!(buffer.text(), "fn two() -> usize { 1 }");
1061 });
1062}
1063
1064#[gpui::test]
1065async fn test_remote_code_lens_fetch_after_lsp_starts(
1066 cx: &mut TestAppContext,
1067 server_cx: &mut TestAppContext,
1068) {
1069 let fs = FakeFs::new(server_cx.executor());
1070 fs.insert_tree(
1071 path!("/code"),
1072 json!({
1073 "project1": {
1074 ".git": {},
1075 "src": {
1076 "lib.rs": "fn one() -> usize { 1 }"
1077 }
1078 },
1079 }),
1080 )
1081 .await;
1082
1083 let (project, headless) = init_test(&fs, cx, server_cx).await;
1084
1085 let capabilities = lsp::ServerCapabilities {
1086 code_lens_provider: Some(lsp::CodeLensOptions {
1087 resolve_provider: None,
1088 }),
1089 ..lsp::ServerCapabilities::default()
1090 };
1091
1092 cx.update_entity(&project, |project, _| {
1093 project.languages().register_test_language(LanguageConfig {
1094 name: "Rust".into(),
1095 matcher: (LanguageMatcher {
1096 path_suffixes: vec!["rs".into()],
1097 ..Default::default()
1098 })
1099 .into(),
1100 ..Default::default()
1101 });
1102 project.languages().register_fake_lsp_adapter(
1103 "Rust",
1104 FakeLspAdapter {
1105 name: "rust-analyzer",
1106 capabilities: capabilities.clone(),
1107 ..FakeLspAdapter::default()
1108 },
1109 );
1110 });
1111
1112 cx.run_until_parked();
1113
1114 let worktree_id = project
1115 .update(cx, |project, cx| {
1116 project.languages().add(rust_lang());
1117 project.find_or_create_worktree(path!("/code/project1"), true, cx)
1118 })
1119 .await
1120 .unwrap()
1121 .0
1122 .read_with(cx, |worktree, _| worktree.id());
1123 cx.run_until_parked();
1124
1125 let (buffer, _handle) = project
1126 .update(cx, |project, cx| {
1127 project.open_buffer_with_lsp((worktree_id, rel_path("src/lib.rs")), cx)
1128 })
1129 .await
1130 .unwrap();
1131 cx.run_until_parked();
1132
1133 // Prime the code lens cache while no LSP server exists on the host.
1134 // This simulates the race where the editor fetches code lenses during
1135 // initial paint before the language server has started.
1136 let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
1137 let initial_actions = lsp_store
1138 .update(cx, |lsp_store, cx| lsp_store.code_lens_actions(&buffer, cx))
1139 .await
1140 .unwrap();
1141 assert_eq!(
1142 initial_actions.map(|a| a.len()),
1143 Some(0),
1144 "Before any LSP starts, code lenses should be empty"
1145 );
1146
1147 // Now register the LSP on the host. This triggers server startup and
1148 // buffer registration, propagating capabilities to the client.
1149 server_cx.update(|cx| {
1150 headless.read(cx).languages.register_fake_lsp_server(
1151 LanguageServerName("rust-analyzer".into()),
1152 capabilities,
1153 Some(Box::new(|fake_lsp| {
1154 fake_lsp.set_request_handler::<lsp::request::CodeLensRequest, _, _>(
1155 |_, _| async move {
1156 Ok(Some(vec![lsp::CodeLens {
1157 range: lsp::Range::new(
1158 lsp::Position::new(0, 0),
1159 lsp::Position::new(0, 9),
1160 ),
1161 command: Some(lsp::Command {
1162 title: "1 reference".to_string(),
1163 command: "lens_cmd".to_string(),
1164 arguments: None,
1165 }),
1166 data: None,
1167 }]))
1168 },
1169 );
1170 })),
1171 );
1172 });
1173
1174 // Trigger re-evaluation of language servers for the already-open buffer.
1175 server_cx.update_entity(&headless, |headless, cx| {
1176 headless.lsp_store.update(cx, |lsp_store, cx| {
1177 lsp_store.restart_all_language_servers(cx);
1178 });
1179 });
1180 cx.run_until_parked();
1181
1182 // A subsequent fetch must bypass the stale (empty) cache now that a
1183 // new server is available.
1184 let actions = lsp_store
1185 .update(cx, |lsp_store, cx| lsp_store.code_lens_actions(&buffer, cx))
1186 .await
1187 .unwrap();
1188 let actions = actions.expect("Should have code lens actions after LSP starts");
1189 assert_eq!(
1190 actions.len(),
1191 1,
1192 "Should have fetched one code lens from the newly started LSP"
1193 );
1194 assert_eq!(
1195 actions.values().next().unwrap().lsp_action.title(),
1196 "1 reference",
1197 );
1198}
1199
1200#[gpui::test]
1201async fn test_remote_code_lens_refetch_after_refresh(
1202 cx: &mut TestAppContext,
1203 server_cx: &mut TestAppContext,
1204) {
1205 let fs = FakeFs::new(server_cx.executor());
1206 fs.insert_tree(
1207 path!("/code"),
1208 json!({
1209 "project1": {
1210 ".git": {},
1211 "src": {
1212 "lib.rs": "fn one() -> usize { 1 }"
1213 }
1214 },
1215 }),
1216 )
1217 .await;
1218
1219 let (project, headless) = init_test(&fs, cx, server_cx).await;
1220
1221 let capabilities = lsp::ServerCapabilities {
1222 code_lens_provider: Some(lsp::CodeLensOptions {
1223 resolve_provider: None,
1224 }),
1225 ..lsp::ServerCapabilities::default()
1226 };
1227
1228 cx.update_entity(&project, |project, _| {
1229 project.languages().register_test_language(LanguageConfig {
1230 name: "Rust".into(),
1231 matcher: Arc::new(LanguageMatcher {
1232 path_suffixes: vec!["rs".into()],
1233 ..LanguageMatcher::default()
1234 }),
1235 ..LanguageConfig::default()
1236 });
1237 project.languages().register_fake_lsp_adapter(
1238 "Rust",
1239 FakeLspAdapter {
1240 name: "rust-analyzer",
1241 capabilities: capabilities.clone(),
1242 ..FakeLspAdapter::default()
1243 },
1244 );
1245 });
1246
1247 let lens_generation = Arc::new(AtomicUsize::new(0));
1248 let mut fake_lsp_servers = server_cx.update(|cx| {
1249 headless.read(cx).languages.register_fake_lsp_server(
1250 LanguageServerName("rust-analyzer".into()),
1251 capabilities,
1252 Some(Box::new({
1253 let lens_generation = lens_generation.clone();
1254 move |fake_lsp| {
1255 let lens_generation = lens_generation.clone();
1256 fake_lsp.set_request_handler::<lsp::request::CodeLensRequest, _, _>(
1257 move |_, _| {
1258 let generation = lens_generation.load(Ordering::Acquire);
1259 async move {
1260 Ok(Some(vec![lsp::CodeLens {
1261 range: lsp::Range::new(
1262 lsp::Position::new(0, 0),
1263 lsp::Position::new(0, 9),
1264 ),
1265 command: Some(lsp::Command {
1266 title: format!("{generation} references"),
1267 command: "lens_cmd".to_string(),
1268 arguments: None,
1269 }),
1270 data: None,
1271 }]))
1272 }
1273 },
1274 );
1275 }
1276 })),
1277 )
1278 });
1279
1280 cx.run_until_parked();
1281
1282 let worktree_id = project
1283 .update(cx, |project, cx| {
1284 project.languages().add(rust_lang());
1285 project.find_or_create_worktree(path!("/code/project1"), true, cx)
1286 })
1287 .await
1288 .unwrap()
1289 .0
1290 .read_with(cx, |worktree, _| worktree.id());
1291 cx.run_until_parked();
1292
1293 let (buffer, _handle) = project
1294 .update(cx, |project, cx| {
1295 project.open_buffer_with_lsp((worktree_id, rel_path("src/lib.rs")), cx)
1296 })
1297 .await
1298 .unwrap();
1299 let fake_lsp = fake_lsp_servers.next().await.unwrap();
1300 cx.run_until_parked();
1301
1302 let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
1303 let actions = lsp_store
1304 .update(cx, |lsp_store, cx| lsp_store.code_lens_actions(&buffer, cx))
1305 .await
1306 .unwrap()
1307 .expect("Should have code lens actions after the initial fetch");
1308 assert_eq!(
1309 actions
1310 .values()
1311 .map(|action| action.lsp_action.title().to_owned())
1312 .collect::<Vec<_>>(),
1313 vec!["0 references".to_string()],
1314 "Expected the initial code lens data on the client",
1315 );
1316
1317 lens_generation.store(1, Ordering::Release);
1318 fake_lsp
1319 .request::<lsp::request::CodeLensRefresh>((), DEFAULT_LSP_REQUEST_TIMEOUT)
1320 .await
1321 .into_response()
1322 .unwrap();
1323 cx.run_until_parked();
1324
1325 let actions = lsp_store
1326 .update(cx, |lsp_store, cx| lsp_store.code_lens_actions(&buffer, cx))
1327 .await
1328 .unwrap()
1329 .expect("Should have code lens actions after the refresh");
1330 assert_eq!(
1331 actions
1332 .values()
1333 .map(|action| action.lsp_action.title().to_owned())
1334 .collect::<Vec<_>>(),
1335 vec!["1 references".to_string()],
1336 "Expected the client to re-fetch code lens data after the server-initiated refresh, without any buffer edits",
1337 );
1338}
1339
1340#[gpui::test]
1341async fn test_remote_lsp_data_cache_converges_with_multiple_servers(
1342 cx: &mut TestAppContext,
1343 server_cx: &mut TestAppContext,
1344) {
1345 let fs = FakeFs::new(server_cx.executor());
1346 fs.insert_tree(
1347 path!("/code"),
1348 json!({
1349 "project1": {
1350 ".git": {},
1351 "src": {
1352 "lib.rs": "fn one() -> usize { 1 }"
1353 }
1354 },
1355 "project2": {
1356 ".git": {},
1357 "src": {
1358 "lib.rs": "fn two() -> usize { 2 }"
1359 }
1360 },
1361 }),
1362 )
1363 .await;
1364
1365 let (project, headless) = init_test(&fs, cx, server_cx).await;
1366
1367 let capabilities = lsp::ServerCapabilities {
1368 code_lens_provider: Some(lsp::CodeLensOptions {
1369 resolve_provider: None,
1370 }),
1371 ..lsp::ServerCapabilities::default()
1372 };
1373
1374 cx.update_entity(&project, |project, _| {
1375 project.languages().register_test_language(LanguageConfig {
1376 name: "Rust".into(),
1377 matcher: Arc::new(LanguageMatcher {
1378 path_suffixes: vec!["rs".into()],
1379 ..LanguageMatcher::default()
1380 }),
1381 ..LanguageConfig::default()
1382 });
1383 project.languages().register_fake_lsp_adapter(
1384 "Rust",
1385 FakeLspAdapter {
1386 name: "rust-analyzer",
1387 capabilities: capabilities.clone(),
1388 ..FakeLspAdapter::default()
1389 },
1390 );
1391 });
1392
1393 let queried_uris = Arc::new(std::sync::Mutex::new(Vec::new()));
1394 let mut fake_lsp_servers = server_cx.update(|cx| {
1395 headless.read(cx).languages.register_fake_lsp_server(
1396 LanguageServerName("rust-analyzer".into()),
1397 capabilities,
1398 Some(Box::new({
1399 let queried_uris = queried_uris.clone();
1400 move |fake_lsp| {
1401 let queried_uris = queried_uris.clone();
1402 fake_lsp.set_request_handler::<lsp::request::CodeLensRequest, _, _>(
1403 move |params, _| {
1404 queried_uris.lock().unwrap().push(params.text_document.uri);
1405 async move {
1406 Ok(Some(vec![lsp::CodeLens {
1407 range: lsp::Range::new(
1408 lsp::Position::new(0, 0),
1409 lsp::Position::new(0, 9),
1410 ),
1411 command: Some(lsp::Command {
1412 title: "the lens".to_string(),
1413 command: "lens_cmd".to_string(),
1414 arguments: None,
1415 }),
1416 data: None,
1417 }]))
1418 }
1419 },
1420 );
1421 }
1422 })),
1423 )
1424 });
1425
1426 cx.run_until_parked();
1427
1428 project.update(cx, |project, cx| {
1429 project.languages().add(rust_lang());
1430 cx.notify();
1431 });
1432 let mut buffers = Vec::new();
1433 let mut fake_lsps = Vec::new();
1434 for project_root in [path!("/code/project1"), path!("/code/project2")] {
1435 let worktree_id = project
1436 .update(cx, |project, cx| {
1437 project.find_or_create_worktree(project_root, true, cx)
1438 })
1439 .await
1440 .unwrap()
1441 .0
1442 .read_with(cx, |worktree, _| worktree.id());
1443 cx.run_until_parked();
1444 buffers.push(
1445 project
1446 .update(cx, |project, cx| {
1447 project.open_buffer_with_lsp((worktree_id, rel_path("src/lib.rs")), cx)
1448 })
1449 .await
1450 .unwrap(),
1451 );
1452 fake_lsps.push(fake_lsp_servers.next().await.unwrap());
1453 cx.run_until_parked();
1454 }
1455 let (buffer_1, _handle_1) = &buffers[0];
1456 let queried_lib_1 = vec![lsp::Uri::from_file_path(path!("/code/project1/src/lib.rs")).unwrap()];
1457
1458 let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
1459 lsp_store
1460 .update(cx, |lsp_store, cx| {
1461 lsp_store.code_lens_actions(buffer_1, cx)
1462 })
1463 .await
1464 .unwrap();
1465 assert_eq!(
1466 *queried_uris.lock().unwrap(),
1467 queried_lib_1,
1468 "Expected the initial fetch to query the buffer's own language server only",
1469 );
1470
1471 lsp_store
1472 .update(cx, |lsp_store, cx| {
1473 lsp_store.code_lens_actions(buffer_1, cx)
1474 })
1475 .await
1476 .unwrap();
1477 assert_eq!(
1478 *queried_uris.lock().unwrap(),
1479 queried_lib_1,
1480 "Expected a repeated fetch to be served from the cache, despite the other worktree's server \
1481 never answering for this buffer",
1482 );
1483
1484 let (buffer_1, _) = &buffers[0];
1485 buffer_1.update(cx, |buffer, cx| buffer.set_language(None, cx));
1486 cx.run_until_parked();
1487 let actions = lsp_store
1488 .update(cx, |lsp_store, cx| {
1489 lsp_store.code_lens_actions(buffer_1, cx)
1490 })
1491 .await
1492 .unwrap()
1493 .expect("Should have code lens actions cached");
1494 assert_eq!(
1495 actions
1496 .values()
1497 .map(|action| action.lsp_action.title().to_owned())
1498 .collect::<Vec<_>>(),
1499 vec!["the lens".to_string()],
1500 "Expected the cached data to survive an empty relevant server set (buffer without a language)",
1501 );
1502 assert_eq!(
1503 *queried_uris.lock().unwrap(),
1504 queried_lib_1,
1505 "Expected no new queries for the buffer without a language",
1506 );
1507}
1508
1509#[gpui::test]
1510async fn test_remote_per_server_refresh_queries_only_the_refreshed_server(
1511 cx: &mut TestAppContext,
1512 server_cx: &mut TestAppContext,
1513) {
1514 let fs = FakeFs::new(server_cx.executor());
1515 fs.insert_tree(
1516 path!("/code"),
1517 json!({
1518 "project1": {
1519 ".git": {},
1520 "src": {
1521 "lib.fake": "fn one() -> usize { 1 }"
1522 }
1523 },
1524 }),
1525 )
1526 .await;
1527
1528 let (project, headless) = init_test(&fs, cx, server_cx).await;
1529
1530 let capabilities = lsp::ServerCapabilities {
1531 code_lens_provider: Some(lsp::CodeLensOptions {
1532 resolve_provider: None,
1533 }),
1534 ..lsp::ServerCapabilities::default()
1535 };
1536
1537 // Both the client and the headless registries need the language and the adapters:
1538 // the client filters relevant servers by adapter names, while the headless one
1539 // detects the buffer's language and starts the servers.
1540 let fake_language_config = || LanguageConfig {
1541 name: "Fake".into(),
1542 matcher: Arc::new(LanguageMatcher {
1543 path_suffixes: vec!["fake".into()],
1544 ..LanguageMatcher::default()
1545 }),
1546 ..LanguageConfig::default()
1547 };
1548 let fake_adapter =
1549 |server_name: &'static str, capabilities: &lsp::ServerCapabilities| FakeLspAdapter {
1550 name: server_name,
1551 capabilities: capabilities.clone(),
1552 ..FakeLspAdapter::default()
1553 };
1554 cx.update_entity(&project, |project, _| {
1555 project
1556 .languages()
1557 .register_test_language(fake_language_config());
1558 for server_name in ["server-a", "server-b"] {
1559 project
1560 .languages()
1561 .register_fake_lsp_adapter("Fake", fake_adapter(server_name, &capabilities));
1562 }
1563 });
1564
1565 let request_counter = |fake_lsp: &mut lsp::FakeLanguageServer, counter: &Arc<AtomicUsize>| {
1566 let counter = counter.clone();
1567 fake_lsp.set_request_handler::<lsp::request::CodeLensRequest, _, _>(move |_, _| {
1568 counter.fetch_add(1, Ordering::Release);
1569 async move { Ok(Some(Vec::new())) }
1570 });
1571 };
1572 let lens_requests_a = Arc::new(AtomicUsize::new(0));
1573 let lens_requests_b = Arc::new(AtomicUsize::new(0));
1574 let (mut fake_lsp_servers_a, mut fake_lsp_servers_b) = server_cx.update(|cx| {
1575 let languages = &headless.read(cx).languages;
1576 languages.register_test_language(fake_language_config());
1577 for server_name in ["server-a", "server-b"] {
1578 languages.register_fake_lsp_adapter("Fake", fake_adapter(server_name, &capabilities));
1579 }
1580 let fake_lsp_servers_a = languages.register_fake_lsp_server(
1581 LanguageServerName("server-a".into()),
1582 capabilities.clone(),
1583 Some(Box::new({
1584 let lens_requests_a = lens_requests_a.clone();
1585 move |fake_lsp| request_counter(fake_lsp, &lens_requests_a)
1586 })),
1587 );
1588 let fake_lsp_servers_b = languages.register_fake_lsp_server(
1589 LanguageServerName("server-b".into()),
1590 capabilities.clone(),
1591 Some(Box::new({
1592 let lens_requests_b = lens_requests_b.clone();
1593 move |fake_lsp| request_counter(fake_lsp, &lens_requests_b)
1594 })),
1595 );
1596 (fake_lsp_servers_a, fake_lsp_servers_b)
1597 });
1598
1599 cx.run_until_parked();
1600
1601 let worktree_id = project
1602 .update(cx, |project, cx| {
1603 project.find_or_create_worktree(path!("/code/project1"), true, cx)
1604 })
1605 .await
1606 .unwrap()
1607 .0
1608 .read_with(cx, |worktree, _| worktree.id());
1609 cx.run_until_parked();
1610
1611 let (buffer, _handle) = project
1612 .update(cx, |project, cx| {
1613 project.open_buffer_with_lsp((worktree_id, rel_path("src/lib.fake")), cx)
1614 })
1615 .await
1616 .unwrap();
1617 let fake_lsp_a = fake_lsp_servers_a.next().await.unwrap();
1618 let _fake_lsp_b = fake_lsp_servers_b.next().await.unwrap();
1619 cx.run_until_parked();
1620
1621 let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
1622 lsp_store
1623 .update(cx, |lsp_store, cx| lsp_store.code_lens_actions(&buffer, cx))
1624 .await
1625 .unwrap();
1626 assert_eq!(
1627 (
1628 lens_requests_a.load(Ordering::Acquire),
1629 lens_requests_b.load(Ordering::Acquire),
1630 ),
1631 (1, 1),
1632 "Expected the initial fetch to query both servers",
1633 );
1634
1635 fake_lsp_a
1636 .request::<lsp::request::CodeLensRefresh>((), DEFAULT_LSP_REQUEST_TIMEOUT)
1637 .await
1638 .into_response()
1639 .unwrap();
1640 cx.run_until_parked();
1641
1642 lsp_store
1643 .update(cx, |lsp_store, cx| lsp_store.code_lens_actions(&buffer, cx))
1644 .await
1645 .unwrap();
1646 assert_eq!(
1647 (
1648 lens_requests_a.load(Ordering::Acquire),
1649 lens_requests_b.load(Ordering::Acquire),
1650 ),
1651 (2, 1),
1652 "Expected the refetch after a per-server refresh to query the refreshed server only",
1653 );
1654}
1655
1656#[gpui::test]
1657async fn test_remote_code_lens_resolve(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1658 cx.update(|cx| {
1659 let settings_store = SettingsStore::test(cx);
1660 cx.set_global(settings_store);
1661 theme_settings::init(theme::LoadThemes::JustBase, cx);
1662 release_channel::init(semver::Version::new(0, 0, 0), cx);
1663 editor::init(cx);
1664 SettingsStore::update_global(cx, |store, cx| {
1665 store.update_user_settings(cx, |settings| {
1666 settings.editor.code_lens = Some(settings::CodeLens::Menu);
1667 });
1668 });
1669 });
1670
1671 let fs = FakeFs::new(server_cx.executor());
1672 fs.insert_tree(
1673 path!("/code"),
1674 json!({
1675 "project1": {
1676 ".git": {},
1677 "src": {
1678 "lib.rs": "fn one() -> usize { 1 }"
1679 }
1680 },
1681 }),
1682 )
1683 .await;
1684
1685 let (project, headless) = init_test(&fs, cx, server_cx).await;
1686
1687 let capabilities = lsp::ServerCapabilities {
1688 code_lens_provider: Some(lsp::CodeLensOptions {
1689 resolve_provider: Some(true),
1690 }),
1691 ..lsp::ServerCapabilities::default()
1692 };
1693
1694 cx.update_entity(&project, |project, _| {
1695 project.languages().register_test_language(LanguageConfig {
1696 name: "Rust".into(),
1697 matcher: (LanguageMatcher {
1698 path_suffixes: vec!["rs".into()],
1699 ..Default::default()
1700 })
1701 .into(),
1702 ..Default::default()
1703 });
1704 project.languages().register_fake_lsp_adapter(
1705 "Rust",
1706 FakeLspAdapter {
1707 name: "rust-analyzer",
1708 capabilities: capabilities.clone(),
1709 ..FakeLspAdapter::default()
1710 },
1711 );
1712 });
1713
1714 server_cx.update(|cx| {
1715 headless.read(cx).languages.register_fake_lsp_server(
1716 LanguageServerName("rust-analyzer".into()),
1717 capabilities,
1718 Some(Box::new(|fake_lsp| {
1719 fake_lsp.set_request_handler::<lsp::request::CodeLensRequest, _, _>(
1720 |_, _| async move {
1721 Ok(Some(vec![lsp::CodeLens {
1722 range: lsp::Range::new(
1723 lsp::Position::new(0, 0),
1724 lsp::Position::new(0, 9),
1725 ),
1726 command: None,
1727 data: Some(serde_json::json!({ "id": "lens" })),
1728 }]))
1729 },
1730 );
1731 fake_lsp.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
1732 |lens, _| async move {
1733 Ok(lsp::CodeLens {
1734 command: Some(lsp::Command {
1735 title: "1 reference".to_string(),
1736 command: "noop".to_string(),
1737 arguments: None,
1738 }),
1739 ..lens
1740 })
1741 },
1742 );
1743 })),
1744 );
1745 });
1746
1747 cx.run_until_parked();
1748
1749 let worktree_id = project
1750 .update(cx, |project, cx| {
1751 project.languages().add(rust_lang());
1752 project.find_or_create_worktree(path!("/code/project1"), true, cx)
1753 })
1754 .await
1755 .unwrap()
1756 .0
1757 .read_with(cx, |worktree, _| worktree.id());
1758 cx.run_until_parked();
1759
1760 let (buffer, _handle) = project
1761 .update(cx, |project, cx| {
1762 project.open_buffer_with_lsp((worktree_id, rel_path("src/lib.rs")), cx)
1763 })
1764 .await
1765 .unwrap();
1766 cx.run_until_parked();
1767
1768 let cx = cx.add_empty_window();
1769 let workspace = cx.new_window_entity(|window, cx| {
1770 workspace::Workspace::test_new(project.clone(), window, cx)
1771 });
1772 let editor = cx.new_window_entity(|window, cx| {
1773 Editor::for_buffer(buffer.clone(), Some(project.clone()), window, cx)
1774 });
1775 workspace.update_in(cx, |workspace, window, cx| {
1776 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
1777 });
1778
1779 editor.update_in(cx, |editor, window, cx| {
1780 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
1781 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)]);
1782 });
1783 });
1784 cx.executor()
1785 .advance_clock(editor::CODE_ACTIONS_DEBOUNCE_TIMEOUT * 2);
1786 cx.run_until_parked();
1787
1788 editor.update_in(cx, |editor, window, cx| {
1789 editor.toggle_code_actions(
1790 &ToggleCodeActions {
1791 deployed_from: None,
1792 quick_launch: false,
1793 },
1794 window,
1795 cx,
1796 );
1797 });
1798 cx.run_until_parked();
1799
1800 editor.update(cx, |editor, _| {
1801 assert!(editor.context_menu_visible());
1802 let menu = editor.context_menu().borrow();
1803 let actions_menu = match menu.as_ref() {
1804 Some(CodeContextMenu::CodeActions(m)) => m,
1805 _ => panic!("Expected code actions menu to be visible"),
1806 };
1807 let item = actions_menu
1808 .actions
1809 .get(0)
1810 .expect("Expected at least one item in menu");
1811 assert_eq!(item.label(), "1 reference");
1812 });
1813}
1814
1815#[gpui::test]
1816async fn test_remote_cancel_language_server_work(
1817 cx: &mut TestAppContext,
1818 server_cx: &mut TestAppContext,
1819) {
1820 let fs = FakeFs::new(server_cx.executor());
1821 fs.insert_tree(
1822 path!("/code"),
1823 json!({
1824 "project1": {
1825 ".git": {},
1826 "README.md": "# project 1",
1827 "src": {
1828 "lib.rs": "fn one() -> usize { 1 }"
1829 }
1830 },
1831 }),
1832 )
1833 .await;
1834
1835 let (project, headless) = init_test(&fs, cx, server_cx).await;
1836
1837 fs.insert_tree(
1838 path!("/code/project1/.zed"),
1839 json!({
1840 "settings.json": r#"
1841 {
1842 "languages": {"Rust":{"language_servers":["rust-analyzer"]}},
1843 "lsp": {
1844 "rust-analyzer": {
1845 "binary": {
1846 "path": "~/.cargo/bin/rust-analyzer"
1847 }
1848 }
1849 }
1850 }"#
1851 }),
1852 )
1853 .await;
1854
1855 cx.update_entity(&project, |project, _| {
1856 project.languages().register_test_language(LanguageConfig {
1857 name: "Rust".into(),
1858 matcher: (LanguageMatcher {
1859 path_suffixes: vec!["rs".into()],
1860 ..Default::default()
1861 })
1862 .into(),
1863 ..Default::default()
1864 });
1865 project.languages().register_fake_lsp_adapter(
1866 "Rust",
1867 FakeLspAdapter {
1868 name: "rust-analyzer",
1869 ..Default::default()
1870 },
1871 )
1872 });
1873
1874 let mut fake_lsp = server_cx.update(|cx| {
1875 headless.read(cx).languages.register_fake_lsp_server(
1876 LanguageServerName("rust-analyzer".into()),
1877 Default::default(),
1878 None,
1879 )
1880 });
1881
1882 cx.run_until_parked();
1883
1884 let worktree_id = project
1885 .update(cx, |project, cx| {
1886 project.find_or_create_worktree(path!("/code/project1"), true, cx)
1887 })
1888 .await
1889 .unwrap()
1890 .0
1891 .read_with(cx, |worktree, _| worktree.id());
1892
1893 cx.run_until_parked();
1894
1895 let (buffer, _handle) = project
1896 .update(cx, |project, cx| {
1897 project.open_buffer_with_lsp((worktree_id, rel_path("src/lib.rs")), cx)
1898 })
1899 .await
1900 .unwrap();
1901
1902 cx.run_until_parked();
1903
1904 let mut fake_lsp = fake_lsp.next().await.unwrap();
1905
1906 // Cancelling all language server work for a given buffer
1907 {
1908 // Two operations, one cancellable and one not.
1909 fake_lsp
1910 .start_progress_with(
1911 "another-token",
1912 lsp::WorkDoneProgressBegin {
1913 cancellable: Some(false),
1914 ..Default::default()
1915 },
1916 DEFAULT_LSP_REQUEST_TIMEOUT,
1917 )
1918 .await;
1919
1920 let progress_token = "the-progress-token";
1921 fake_lsp
1922 .start_progress_with(
1923 progress_token,
1924 lsp::WorkDoneProgressBegin {
1925 cancellable: Some(true),
1926 ..Default::default()
1927 },
1928 DEFAULT_LSP_REQUEST_TIMEOUT,
1929 )
1930 .await;
1931
1932 cx.executor().run_until_parked();
1933
1934 project.update(cx, |project, cx| {
1935 project.cancel_language_server_work_for_buffers([buffer.clone()], cx)
1936 });
1937
1938 cx.executor().run_until_parked();
1939
1940 // Verify the cancellation was received on the server side
1941 let cancel_notification = fake_lsp
1942 .receive_notification::<lsp::notification::WorkDoneProgressCancel>()
1943 .await;
1944 assert_eq!(
1945 cancel_notification.token,
1946 lsp::NumberOrString::String(progress_token.into())
1947 );
1948 }
1949
1950 // Cancelling work by server_id and token
1951 {
1952 let server_id = fake_lsp.server.server_id();
1953 let progress_token = "the-progress-token";
1954
1955 fake_lsp
1956 .start_progress_with(
1957 progress_token,
1958 lsp::WorkDoneProgressBegin {
1959 cancellable: Some(true),
1960 ..Default::default()
1961 },
1962 DEFAULT_LSP_REQUEST_TIMEOUT,
1963 )
1964 .await;
1965
1966 cx.executor().run_until_parked();
1967
1968 project.update(cx, |project, cx| {
1969 project.cancel_language_server_work(
1970 server_id,
1971 Some(ProgressToken::String(SharedString::from(progress_token))),
1972 cx,
1973 )
1974 });
1975
1976 cx.executor().run_until_parked();
1977
1978 // Verify the cancellation was received on the server side
1979 let cancel_notification = fake_lsp
1980 .receive_notification::<lsp::notification::WorkDoneProgressCancel>()
1981 .await;
1982 assert_eq!(
1983 cancel_notification.token,
1984 lsp::NumberOrString::String(progress_token.to_owned())
1985 );
1986 }
1987}
1988
1989#[gpui::test]
1990async fn test_remote_reload(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1991 let fs = FakeFs::new(server_cx.executor());
1992 fs.insert_tree(
1993 path!("/code"),
1994 json!({
1995 "project1": {
1996 ".git": {},
1997 "README.md": "# project 1",
1998 "src": {
1999 "lib.rs": "fn one() -> usize { 1 }"
2000 }
2001 },
2002 }),
2003 )
2004 .await;
2005
2006 let (project, _headless) = init_test(&fs, cx, server_cx).await;
2007 let (worktree, _) = project
2008 .update(cx, |project, cx| {
2009 project.find_or_create_worktree(path!("/code/project1"), true, cx)
2010 })
2011 .await
2012 .unwrap();
2013
2014 let worktree_id = cx.update(|cx| worktree.read(cx).id());
2015
2016 let buffer = project
2017 .update(cx, |project, cx| {
2018 project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
2019 })
2020 .await
2021 .unwrap();
2022
2023 fs.save(
2024 &PathBuf::from(path!("/code/project1/src/lib.rs")),
2025 &("bangles".to_string().into()),
2026 LineEnding::Unix,
2027 )
2028 .await
2029 .unwrap();
2030
2031 cx.run_until_parked();
2032
2033 buffer.update(cx, |buffer, cx| {
2034 assert_eq!(buffer.text(), "bangles");
2035 buffer.edit([(0..0, "a")], None, cx);
2036 });
2037
2038 fs.save(
2039 &PathBuf::from(path!("/code/project1/src/lib.rs")),
2040 &("bloop".to_string().into()),
2041 LineEnding::Unix,
2042 )
2043 .await
2044 .unwrap();
2045
2046 cx.run_until_parked();
2047 cx.update(|cx| {
2048 assert!(buffer.read(cx).has_conflict());
2049 });
2050
2051 project
2052 .update(cx, |project, cx| {
2053 project.reload_buffers([buffer.clone()].into_iter().collect(), false, cx)
2054 })
2055 .await
2056 .unwrap();
2057 cx.run_until_parked();
2058
2059 cx.update(|cx| {
2060 assert!(!buffer.read(cx).has_conflict());
2061 });
2062}
2063
2064#[gpui::test]
2065async fn test_remote_resolve_path_in_buffer(
2066 cx: &mut TestAppContext,
2067 server_cx: &mut TestAppContext,
2068) {
2069 let fs = FakeFs::new(server_cx.executor());
2070 // Even though we are not testing anything from project1, it is necessary to test if project2 is picking up correct worktree
2071 fs.insert_tree(
2072 path!("/code"),
2073 json!({
2074 "project1": {
2075 ".git": {},
2076 "README.md": "# project 1",
2077 "src": {
2078 "lib.rs": "fn one() -> usize { 1 }"
2079 }
2080 },
2081 "project2": {
2082 ".git": {},
2083 "README.md": "# project 2",
2084 "src": {
2085 "lib.rs": "fn two() -> usize { 2 }"
2086 }
2087 }
2088 }),
2089 )
2090 .await;
2091
2092 let (project, _headless) = init_test(&fs, cx, server_cx).await;
2093
2094 let _ = project
2095 .update(cx, |project, cx| {
2096 project.find_or_create_worktree(path!("/code/project1"), true, cx)
2097 })
2098 .await
2099 .unwrap();
2100
2101 let (worktree2, _) = project
2102 .update(cx, |project, cx| {
2103 project.find_or_create_worktree(path!("/code/project2"), true, cx)
2104 })
2105 .await
2106 .unwrap();
2107
2108 let worktree2_id = cx.update(|cx| worktree2.read(cx).id());
2109
2110 cx.run_until_parked();
2111
2112 let buffer2 = project
2113 .update(cx, |project, cx| {
2114 project.open_buffer((worktree2_id, rel_path("src/lib.rs")), cx)
2115 })
2116 .await
2117 .unwrap();
2118
2119 let path = project
2120 .update(cx, |project, cx| {
2121 project.resolve_path_in_buffer(path!("/code/project2/README.md"), &buffer2, cx)
2122 })
2123 .await
2124 .unwrap();
2125 assert!(path.is_file());
2126 assert_eq!(path.abs_path().unwrap(), path!("/code/project2/README.md"));
2127
2128 let path = project
2129 .update(cx, |project, cx| {
2130 project.resolve_path_in_buffer("../README.md", &buffer2, cx)
2131 })
2132 .await
2133 .unwrap();
2134 assert!(path.is_file());
2135 assert_eq!(
2136 path.project_path().unwrap().clone(),
2137 (worktree2_id, rel_path("README.md")).into()
2138 );
2139
2140 let path = project
2141 .update(cx, |project, cx| {
2142 project.resolve_path_in_buffer("../src", &buffer2, cx)
2143 })
2144 .await
2145 .unwrap();
2146 assert_eq!(
2147 path.project_path().unwrap().clone(),
2148 (worktree2_id, rel_path("src")).into()
2149 );
2150 assert!(path.is_dir());
2151}
2152
2153#[gpui::test]
2154async fn test_remote_resolve_abs_path(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
2155 let fs = FakeFs::new(server_cx.executor());
2156 fs.insert_tree(
2157 path!("/code"),
2158 json!({
2159 "project1": {
2160 ".git": {},
2161 "README.md": "# project 1",
2162 "src": {
2163 "lib.rs": "fn one() -> usize { 1 }"
2164 }
2165 },
2166 }),
2167 )
2168 .await;
2169
2170 let (project, _headless) = init_test(&fs, cx, server_cx).await;
2171
2172 let path = project
2173 .update(cx, |project, cx| {
2174 project.resolve_abs_path(path!("/code/project1/README.md"), cx)
2175 })
2176 .await
2177 .unwrap();
2178
2179 assert!(path.is_file());
2180 assert_eq!(path.abs_path().unwrap(), path!("/code/project1/README.md"));
2181
2182 let path = project
2183 .update(cx, |project, cx| {
2184 project.resolve_abs_path(path!("/code/project1/src"), cx)
2185 })
2186 .await
2187 .unwrap();
2188
2189 assert!(path.is_dir());
2190 assert_eq!(path.abs_path().unwrap(), path!("/code/project1/src"));
2191
2192 let path = project
2193 .update(cx, |project, cx| {
2194 project.resolve_abs_path(path!("/code/project1/DOESNOTEXIST"), cx)
2195 })
2196 .await;
2197 assert!(path.is_none());
2198}
2199
2200#[gpui::test(iterations = 10)]
2201async fn test_canceling_buffer_opening(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
2202 let fs = FakeFs::new(server_cx.executor());
2203 fs.insert_tree(
2204 "/code",
2205 json!({
2206 "project1": {
2207 ".git": {},
2208 "README.md": "# project 1",
2209 "src": {
2210 "lib.rs": "fn one() -> usize { 1 }"
2211 }
2212 },
2213 }),
2214 )
2215 .await;
2216
2217 let (project, _headless) = init_test(&fs, cx, server_cx).await;
2218 let (worktree, _) = project
2219 .update(cx, |project, cx| {
2220 project.find_or_create_worktree("/code/project1", true, cx)
2221 })
2222 .await
2223 .unwrap();
2224 let worktree_id = worktree.read_with(cx, |tree, _| tree.id());
2225
2226 // Open a buffer on the client but cancel after a random amount of time.
2227 let buffer = project.update(cx, |p, cx| {
2228 p.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
2229 });
2230 cx.executor().simulate_random_delay().await;
2231 drop(buffer);
2232
2233 // Try opening the same buffer again as the client, and ensure we can
2234 // still do it despite the cancellation above.
2235 let buffer = project
2236 .update(cx, |p, cx| {
2237 p.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
2238 })
2239 .await
2240 .unwrap();
2241
2242 buffer.read_with(cx, |buf, _| {
2243 assert_eq!(buf.text(), "fn one() -> usize { 1 }")
2244 });
2245}
2246
2247#[gpui::test]
2248async fn test_adding_then_removing_then_adding_worktrees(
2249 cx: &mut TestAppContext,
2250 server_cx: &mut TestAppContext,
2251) {
2252 let fs = FakeFs::new(server_cx.executor());
2253 fs.insert_tree(
2254 path!("/code"),
2255 json!({
2256 "project1": {
2257 ".git": {},
2258 "README.md": "# project 1",
2259 "src": {
2260 "lib.rs": "fn one() -> usize { 1 }"
2261 }
2262 },
2263 "project2": {
2264 "README.md": "# project 2",
2265 },
2266 }),
2267 )
2268 .await;
2269
2270 let (project, _headless) = init_test(&fs, cx, server_cx).await;
2271 let (_worktree, _) = project
2272 .update(cx, |project, cx| {
2273 project.find_or_create_worktree(path!("/code/project1"), true, cx)
2274 })
2275 .await
2276 .unwrap();
2277
2278 let (worktree_2, _) = project
2279 .update(cx, |project, cx| {
2280 project.find_or_create_worktree(path!("/code/project2"), true, cx)
2281 })
2282 .await
2283 .unwrap();
2284 let worktree_id_2 = worktree_2.read_with(cx, |tree, _| tree.id());
2285
2286 project.update(cx, |project, cx| project.remove_worktree(worktree_id_2, cx));
2287
2288 let (worktree_2, _) = project
2289 .update(cx, |project, cx| {
2290 project.find_or_create_worktree(path!("/code/project2"), true, cx)
2291 })
2292 .await
2293 .unwrap();
2294
2295 cx.run_until_parked();
2296 worktree_2.update(cx, |worktree, _cx| {
2297 assert!(worktree.is_visible());
2298 let entries = worktree.entries(true, 0).collect::<Vec<_>>();
2299 assert_eq!(entries.len(), 2);
2300 assert_eq!(entries[1].path.as_unix_str(), "README.md")
2301 })
2302}
2303
2304#[gpui::test]
2305async fn test_open_server_settings(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
2306 let fs = FakeFs::new(server_cx.executor());
2307 fs.insert_tree(
2308 path!("/code"),
2309 json!({
2310 "project1": {
2311 ".git": {},
2312 "README.md": "# project 1",
2313 "src": {
2314 "lib.rs": "fn one() -> usize { 1 }"
2315 }
2316 },
2317 }),
2318 )
2319 .await;
2320
2321 let (project, _headless) = init_test(&fs, cx, server_cx).await;
2322 let buffer = project.update(cx, |project, cx| project.open_server_settings(cx));
2323 cx.executor().run_until_parked();
2324
2325 let buffer = buffer.await.unwrap();
2326
2327 cx.update(|cx| {
2328 assert_eq!(
2329 buffer.read(cx).text(),
2330 initial_server_settings_content()
2331 .to_string()
2332 .replace("\r\n", "\n")
2333 )
2334 })
2335}
2336
2337#[gpui::test(iterations = 20)]
2338async fn test_reconnect(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
2339 let fs = FakeFs::new(server_cx.executor());
2340 fs.insert_tree(
2341 path!("/code"),
2342 json!({
2343 "project1": {
2344 ".git": {},
2345 "README.md": "# project 1",
2346 "src": {
2347 "lib.rs": "fn one() -> usize { 1 }"
2348 }
2349 },
2350 }),
2351 )
2352 .await;
2353
2354 let (project, _headless) = init_test(&fs, cx, server_cx).await;
2355
2356 let (worktree, _) = project
2357 .update(cx, |project, cx| {
2358 project.find_or_create_worktree(path!("/code/project1"), true, cx)
2359 })
2360 .await
2361 .unwrap();
2362
2363 let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
2364 let buffer = project
2365 .update(cx, |project, cx| {
2366 project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
2367 })
2368 .await
2369 .unwrap();
2370
2371 buffer.update(cx, |buffer, cx| {
2372 assert_eq!(buffer.text(), "fn one() -> usize { 1 }");
2373 let ix = buffer.text().find('1').unwrap();
2374 buffer.edit([(ix..ix + 1, "100")], None, cx);
2375 });
2376
2377 let client = cx.read(|cx| project.read(cx).remote_client().unwrap());
2378 client
2379 .update(cx, |client, cx| client.simulate_disconnect(cx))
2380 .detach();
2381
2382 project
2383 .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
2384 .await
2385 .unwrap();
2386
2387 assert_eq!(
2388 fs.load(path!("/code/project1/src/lib.rs").as_ref())
2389 .await
2390 .unwrap(),
2391 "fn one() -> usize { 100 }"
2392 );
2393}
2394
2395#[gpui::test]
2396async fn test_remote_root_rename(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
2397 let fs = FakeFs::new(server_cx.executor());
2398 fs.insert_tree(
2399 "/code",
2400 json!({
2401 "project1": {
2402 ".git": {},
2403 "README.md": "# project 1",
2404 },
2405 }),
2406 )
2407 .await;
2408
2409 let (project, _) = init_test(&fs, cx, server_cx).await;
2410
2411 let (worktree, _) = project
2412 .update(cx, |project, cx| {
2413 project.find_or_create_worktree("/code/project1", true, cx)
2414 })
2415 .await
2416 .unwrap();
2417
2418 cx.run_until_parked();
2419
2420 fs.rename(
2421 &PathBuf::from("/code/project1"),
2422 &PathBuf::from("/code/project2"),
2423 Default::default(),
2424 )
2425 .await
2426 .unwrap();
2427
2428 cx.run_until_parked();
2429 worktree.update(cx, |worktree, _| {
2430 assert_eq!(worktree.root_name(), "project2")
2431 })
2432}
2433
2434#[gpui::test]
2435async fn test_remote_rename_entry(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
2436 let fs = FakeFs::new(server_cx.executor());
2437 fs.insert_tree(
2438 "/code",
2439 json!({
2440 "project1": {
2441 ".git": {},
2442 "README.md": "# project 1",
2443 },
2444 }),
2445 )
2446 .await;
2447
2448 let (project, _) = init_test(&fs, cx, server_cx).await;
2449 let (worktree, _) = project
2450 .update(cx, |project, cx| {
2451 project.find_or_create_worktree("/code/project1", true, cx)
2452 })
2453 .await
2454 .unwrap();
2455
2456 cx.run_until_parked();
2457
2458 let entry = project
2459 .update(cx, |project, cx| {
2460 let worktree = worktree.read(cx);
2461 let entry = worktree.entry_for_path(rel_path("README.md")).unwrap();
2462 project.rename_entry(entry.id, (worktree.id(), rel_path("README.rst")).into(), cx)
2463 })
2464 .await
2465 .unwrap()
2466 .into_included()
2467 .unwrap();
2468
2469 cx.run_until_parked();
2470
2471 worktree.update(cx, |worktree, _| {
2472 assert_eq!(
2473 worktree.entry_for_path(rel_path("README.rst")).unwrap().id,
2474 entry.id
2475 )
2476 });
2477}
2478
2479#[gpui::test]
2480async fn test_copy_file_into_remote_project(
2481 cx: &mut TestAppContext,
2482 server_cx: &mut TestAppContext,
2483) {
2484 let remote_fs = FakeFs::new(server_cx.executor());
2485 remote_fs
2486 .insert_tree(
2487 path!("/code"),
2488 json!({
2489 "project1": {
2490 ".git": {},
2491 "README.md": "# project 1",
2492 "src": {
2493 "main.rs": ""
2494 }
2495 },
2496 }),
2497 )
2498 .await;
2499
2500 let (project, _) = init_test(&remote_fs, cx, server_cx).await;
2501 let (worktree, _) = project
2502 .update(cx, |project, cx| {
2503 project.find_or_create_worktree(path!("/code/project1"), true, cx)
2504 })
2505 .await
2506 .unwrap();
2507
2508 cx.run_until_parked();
2509
2510 let local_fs = project
2511 .read_with(cx, |project, _| project.fs().clone())
2512 .as_fake();
2513 local_fs
2514 .insert_tree(
2515 path!("/local-code"),
2516 json!({
2517 "dir1": {
2518 "file1": "file 1 content",
2519 "dir2": {
2520 "file2": "file 2 content",
2521 "dir3": {
2522 "file3": ""
2523 },
2524 "dir4": {}
2525 },
2526 "dir5": {}
2527 },
2528 "file4": "file 4 content"
2529 }),
2530 )
2531 .await;
2532
2533 worktree
2534 .update(cx, |worktree, cx| {
2535 worktree.copy_external_entries(
2536 rel_path("src").into(),
2537 vec![
2538 Path::new(path!("/local-code/dir1/file1")).into(),
2539 Path::new(path!("/local-code/dir1/dir2")).into(),
2540 ],
2541 local_fs.clone(),
2542 cx,
2543 )
2544 })
2545 .await
2546 .unwrap();
2547
2548 assert_eq!(
2549 remote_fs.paths(true),
2550 vec![
2551 PathBuf::from(path!("/")),
2552 PathBuf::from(path!("/code")),
2553 PathBuf::from(path!("/code/project1")),
2554 PathBuf::from(path!("/code/project1/.git")),
2555 PathBuf::from(path!("/code/project1/README.md")),
2556 PathBuf::from(path!("/code/project1/src")),
2557 PathBuf::from(path!("/code/project1/src/dir2")),
2558 PathBuf::from(path!("/code/project1/src/file1")),
2559 PathBuf::from(path!("/code/project1/src/main.rs")),
2560 PathBuf::from(path!("/code/project1/src/dir2/dir3")),
2561 PathBuf::from(path!("/code/project1/src/dir2/dir4")),
2562 PathBuf::from(path!("/code/project1/src/dir2/file2")),
2563 PathBuf::from(path!("/code/project1/src/dir2/dir3/file3")),
2564 ]
2565 );
2566 assert_eq!(
2567 remote_fs
2568 .load(path!("/code/project1/src/file1").as_ref())
2569 .await
2570 .unwrap(),
2571 "file 1 content"
2572 );
2573 assert_eq!(
2574 remote_fs
2575 .load(path!("/code/project1/src/dir2/file2").as_ref())
2576 .await
2577 .unwrap(),
2578 "file 2 content"
2579 );
2580 assert_eq!(
2581 remote_fs
2582 .load(path!("/code/project1/src/dir2/dir3/file3").as_ref())
2583 .await
2584 .unwrap(),
2585 ""
2586 );
2587}
2588
2589#[gpui::test]
2590async fn test_remote_root_repo_common_dir(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
2591 let fs = FakeFs::new(server_cx.executor());
2592 fs.insert_tree(
2593 "/code",
2594 json!({
2595 "main_repo": {
2596 ".git": {},
2597 "file.txt": "content",
2598 },
2599 "no_git": {
2600 "file.txt": "content",
2601 },
2602 }),
2603 )
2604 .await;
2605
2606 // Create a linked worktree that points back to main_repo's .git.
2607 fs.add_linked_worktree_for_repo(
2608 Path::new("/code/main_repo/.git"),
2609 false,
2610 GitWorktree {
2611 path: PathBuf::from("/code/linked_worktree"),
2612 ref_name: Some("refs/heads/feature-branch".into()),
2613 sha: "abc123".into(),
2614 is_main: false,
2615 is_bare: false,
2616 },
2617 )
2618 .await;
2619
2620 let (project, _headless) = init_test(&fs, cx, server_cx).await;
2621
2622 // Main repo: root_repo_common_dir should be the .git directory itself.
2623 let (worktree_main, _) = project
2624 .update(cx, |project, cx| {
2625 project.find_or_create_worktree("/code/main_repo", true, cx)
2626 })
2627 .await
2628 .unwrap();
2629 cx.executor().run_until_parked();
2630
2631 let (common_dir, is_linked_worktree) = worktree_main.read_with(cx, |worktree, _| {
2632 (
2633 worktree.snapshot().root_repo_common_dir().cloned(),
2634 worktree.snapshot().root_repo_is_linked_worktree(),
2635 )
2636 });
2637 assert_eq!(
2638 common_dir.as_deref(),
2639 Some(Path::new("/code/main_repo/.git")),
2640 );
2641 assert!(!is_linked_worktree);
2642
2643 // Linked worktree: root_repo_common_dir should point to the main repo's .git.
2644 let (worktree_linked, _) = project
2645 .update(cx, |project, cx| {
2646 project.find_or_create_worktree("/code/linked_worktree", true, cx)
2647 })
2648 .await
2649 .unwrap();
2650 cx.executor().run_until_parked();
2651
2652 let (common_dir, is_linked_worktree) = worktree_linked.read_with(cx, |worktree, _| {
2653 (
2654 worktree.snapshot().root_repo_common_dir().cloned(),
2655 worktree.snapshot().root_repo_is_linked_worktree(),
2656 )
2657 });
2658 assert_eq!(
2659 common_dir.as_deref(),
2660 Some(Path::new("/code/main_repo/.git")),
2661 );
2662 assert!(is_linked_worktree);
2663
2664 let main_worktree_paths = project.read_with(cx, |project, cx| {
2665 project
2666 .worktree_paths(cx)
2667 .main_worktree_path_list()
2668 .ordered_paths()
2669 .map(|path| path.to_path_buf())
2670 .collect::<Vec<_>>()
2671 });
2672 assert_eq!(
2673 main_worktree_paths,
2674 vec![
2675 PathBuf::from("/code/main_repo"),
2676 PathBuf::from("/code/main_repo"),
2677 ]
2678 );
2679
2680 // No git repo: root_repo_common_dir should be None.
2681 let (worktree_no_git, _) = project
2682 .update(cx, |project, cx| {
2683 project.find_or_create_worktree("/code/no_git", true, cx)
2684 })
2685 .await
2686 .unwrap();
2687 cx.executor().run_until_parked();
2688
2689 let (common_dir, is_linked_worktree) = worktree_no_git.read_with(cx, |worktree, _| {
2690 (
2691 worktree.snapshot().root_repo_common_dir().cloned(),
2692 worktree.snapshot().root_repo_is_linked_worktree(),
2693 )
2694 });
2695 assert_eq!(common_dir, None);
2696 assert!(!is_linked_worktree);
2697}
2698
2699#[gpui::test]
2700async fn test_remote_search_commits_streams_proto_chunks(
2701 cx: &mut TestAppContext,
2702 server_cx: &mut TestAppContext,
2703) {
2704 const COMMIT_COUNT: usize = 900;
2705 const RESPONSE_MAX_SIZE: usize = 100;
2706
2707 let fs = FakeFs::new(server_cx.executor());
2708 fs.insert_tree(
2709 path!("/code"),
2710 json!({
2711 "project1": {
2712 ".git": {},
2713 "file.txt": "content",
2714 },
2715 }),
2716 )
2717 .await;
2718
2719 let commit_data = (0..COMMIT_COUNT)
2720 .map(|index| {
2721 let sha = Oid::from_str(&format!("{:040x}", index + 1)).unwrap();
2722 (
2723 CommitData {
2724 sha,
2725 parents: Default::default(),
2726 author_name: SharedString::from("Author"),
2727 author_email: SharedString::from("author@example.com"),
2728 commit_timestamp: index as i64,
2729 subject: SharedString::from(format!("Subject {index}")),
2730 message: SharedString::from(format!("needle commit {index}")),
2731 },
2732 false,
2733 )
2734 })
2735 .collect::<Vec<_>>();
2736 let expected_shas = commit_data
2737 .iter()
2738 .map(|(commit_data, _)| commit_data.sha.to_string())
2739 .collect::<HashSet<_>>();
2740 fs.set_commit_data(Path::new(path!("/code/project1/.git")), commit_data);
2741
2742 let (project, _headless) = init_test(&fs, cx, server_cx).await;
2743 project
2744 .update(cx, |project, cx| {
2745 project.find_or_create_worktree(path!("/code/project1"), true, cx)
2746 })
2747 .await
2748 .expect("should open remote worktree");
2749 server_cx.run_until_parked();
2750 cx.run_until_parked();
2751 project
2752 .update(cx, |project, cx| project.git_scans_complete(cx))
2753 .await;
2754
2755 let (remote_client, repository_id) = project.read_with(cx, |project, cx| {
2756 let repository = project
2757 .active_repository(cx)
2758 .expect("remote project should have an active repository");
2759 let repository_id = repository.read(cx).snapshot().id;
2760 let remote_client = project
2761 .remote_client()
2762 .expect("project should have a remote client");
2763 (remote_client, repository_id)
2764 });
2765 let proto_client = remote_client.read_with(cx, |remote_client, _| remote_client.proto_client());
2766 let mut stream = proto_client
2767 .request_stream(proto::SearchCommits {
2768 project_id: proto::REMOTE_SERVER_PROJECT_ID,
2769 repository_id: repository_id.to_proto(),
2770 log_source: Some(proto::GitLogSource {
2771 source: Some(proto::git_log_source::Source::All(
2772 proto::GitLogSourceAll {},
2773 )),
2774 }),
2775 query: "needle".to_string(),
2776 case_sensitive: true,
2777 })
2778 .await
2779 .expect("search commits stream should start");
2780
2781 let mut chunks = Vec::new();
2782 while let Some(response) = futures::StreamExt::next(&mut stream).await {
2783 chunks.push(response.expect("search commits chunk should succeed").shas);
2784 }
2785
2786 assert!(
2787 chunks.len() > 1,
2788 "expected search results to stream in multiple chunks"
2789 );
2790 for chunk in chunks.iter().take(chunks.len() - 1) {
2791 assert!(
2792 chunk.len() <= RESPONSE_MAX_SIZE,
2793 "non-final chunks should meet the target byte size"
2794 );
2795 }
2796
2797 let actual_shas = chunks.into_iter().flatten().collect::<HashSet<_>>();
2798 assert_eq!(actual_shas, expected_shas);
2799}
2800
2801#[gpui::test]
2802async fn test_remote_archive_git_operations_are_supported(
2803 cx: &mut TestAppContext,
2804 server_cx: &mut TestAppContext,
2805) {
2806 let fs = FakeFs::new(server_cx.executor());
2807 fs.insert_tree(
2808 "/project",
2809 json!({
2810 ".git": {},
2811 "file.txt": "content",
2812 }),
2813 )
2814 .await;
2815 fs.set_branch_name(Path::new("/project/.git"), Some("main"));
2816 fs.set_head_for_repo(
2817 Path::new("/project/.git"),
2818 &[("file.txt", "content".into())],
2819 "head-sha",
2820 );
2821
2822 let (project, _headless) = init_test(&fs, cx, server_cx).await;
2823 project
2824 .update(cx, |project, cx| {
2825 project.find_or_create_worktree(Path::new("/project"), true, cx)
2826 })
2827 .await
2828 .expect("should open remote worktree");
2829 cx.run_until_parked();
2830
2831 let repository = project.read_with(cx, |project, cx| {
2832 project
2833 .active_repository(cx)
2834 .expect("remote project should have an active repository")
2835 });
2836
2837 let head_sha = cx
2838 .update(|cx| repository.update(cx, |repository, _| repository.head_sha()))
2839 .await
2840 .expect("head_sha request should complete")
2841 .expect("head_sha should succeed")
2842 .expect("HEAD should exist");
2843
2844 cx.run_until_parked();
2845
2846 cx.update(|cx| {
2847 repository.update(cx, |repository, _| {
2848 repository.update_ref("refs/zed-tests/archive-checkpoint".to_string(), head_sha)
2849 })
2850 })
2851 .await
2852 .expect("update_ref request should complete")
2853 .expect("update_ref should succeed for remote repository");
2854
2855 cx.run_until_parked();
2856
2857 cx.update(|cx| {
2858 repository.update(cx, |repository, _| {
2859 repository.delete_ref("refs/zed-tests/archive-checkpoint".to_string())
2860 })
2861 })
2862 .await
2863 .expect("delete_ref request should complete")
2864 .expect("delete_ref should succeed for remote repository");
2865
2866 cx.run_until_parked();
2867
2868 cx.update(|cx| repository.update(cx, |repository, _| repository.repair_worktrees()))
2869 .await
2870 .expect("repair_worktrees request should complete")
2871 .expect("repair_worktrees should succeed for remote repository");
2872
2873 cx.run_until_parked();
2874
2875 let (staged_commit_sha, unstaged_commit_sha) = cx
2876 .update(|cx| repository.update(cx, |repository, _| repository.create_archive_checkpoint()))
2877 .await
2878 .expect("create_archive_checkpoint request should complete")
2879 .expect("create_archive_checkpoint should succeed for remote repository");
2880
2881 cx.run_until_parked();
2882
2883 cx.update(|cx| {
2884 repository.update(cx, |repository, _| {
2885 repository.restore_archive_checkpoint(staged_commit_sha, unstaged_commit_sha)
2886 })
2887 })
2888 .await
2889 .expect("restore_archive_checkpoint request should complete")
2890 .expect("restore_archive_checkpoint should succeed for remote repository");
2891}
2892
2893#[gpui::test]
2894async fn test_add_path_to_gitignore_in_remote_repository(
2895 cx: &mut TestAppContext,
2896 server_cx: &mut TestAppContext,
2897) {
2898 let fs = FakeFs::new(server_cx.executor());
2899 fs.insert_tree(
2900 "/project",
2901 json!({
2902 ".git": {},
2903 ".gitignore": "existing\n",
2904 "logs": {
2905 "app.log": ""
2906 },
2907 "tmp.txt": "",
2908 }),
2909 )
2910 .await;
2911 fs.set_branch_name(Path::new("/project/.git"), Some("main"));
2912 fs.set_head_for_repo(
2913 Path::new("/project/.git"),
2914 &[(".gitignore", "existing\n".into())],
2915 "head-sha",
2916 );
2917
2918 let (project, _headless) = init_test(&fs, cx, server_cx).await;
2919 project
2920 .update(cx, |project, cx| {
2921 project.find_or_create_worktree(Path::new("/project"), true, cx)
2922 })
2923 .await
2924 .expect("should open remote worktree");
2925 cx.run_until_parked();
2926
2927 let repository = project.read_with(cx, |project, cx| {
2928 project
2929 .active_repository(cx)
2930 .expect("remote project should have an active repository")
2931 });
2932
2933 for (path, is_dir) in [("tmp.txt", false), ("logs", true), ("tmp.txt", false)] {
2934 let repo_path = RepoPath::new(path).expect("path should be a valid repo path");
2935 cx.update(|cx| {
2936 repository.update(cx, |repository, _| {
2937 repository.add_path_to_gitignore(&repo_path, is_dir)
2938 })
2939 })
2940 .await
2941 .expect("add to .gitignore request should complete")
2942 .expect("add to .gitignore should succeed for remote repository");
2943 }
2944
2945 server_cx.run_until_parked();
2946 cx.run_until_parked();
2947
2948 assert_eq!(
2949 fs.load(Path::new("/project/.gitignore"))
2950 .await
2951 .expect(".gitignore should be readable"),
2952 "existing\ntmp.txt\nlogs/\n"
2953 );
2954}
2955
2956#[gpui::test]
2957async fn test_add_path_to_git_info_exclude_in_remote_repository(
2958 cx: &mut TestAppContext,
2959 server_cx: &mut TestAppContext,
2960) {
2961 let fs = FakeFs::new(server_cx.executor());
2962 fs.insert_tree(
2963 "/project",
2964 json!({
2965 ".git": {},
2966 "logs": {
2967 "app.log": ""
2968 },
2969 "tmp.txt": "",
2970 }),
2971 )
2972 .await;
2973 fs.set_branch_name(Path::new("/project/.git"), Some("main"));
2974 fs.set_head_for_repo(Path::new("/project/.git"), &[], "head-sha");
2975
2976 let (project, _headless) = init_test(&fs, cx, server_cx).await;
2977 project
2978 .update(cx, |project, cx| {
2979 project.find_or_create_worktree(Path::new("/project"), true, cx)
2980 })
2981 .await
2982 .expect("should open remote worktree");
2983 cx.run_until_parked();
2984
2985 let repository = project.read_with(cx, |project, cx| {
2986 project
2987 .active_repository(cx)
2988 .expect("remote project should have an active repository")
2989 });
2990
2991 for (path, is_dir) in [("tmp.txt", false), ("logs", true), ("tmp.txt", false)] {
2992 let repo_path = RepoPath::new(path).expect("path should be a valid repo path");
2993 cx.update(|cx| {
2994 repository.update(cx, |repository, _| {
2995 repository.add_path_to_git_info_exclude(&repo_path, is_dir)
2996 })
2997 })
2998 .await
2999 .expect("add to info/exclude request should complete")
3000 .expect("add to info/exclude should succeed for remote repository");
3001 }
3002
3003 server_cx.run_until_parked();
3004 cx.run_until_parked();
3005
3006 assert_eq!(
3007 fs.load(Path::new("/project/.git/info/exclude"))
3008 .await
3009 .expect("info/exclude should be readable"),
3010 "tmp.txt\nlogs/\n"
3011 );
3012}
3013
3014#[gpui::test]
3015async fn test_remote_git_diffs(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
3016 let text_2 = "
3017 fn one() -> usize {
3018 1
3019 }
3020 "
3021 .unindent();
3022 let text_1 = "
3023 fn one() -> usize {
3024 0
3025 }
3026 "
3027 .unindent();
3028
3029 let fs = FakeFs::new(server_cx.executor());
3030 fs.insert_tree(
3031 "/code",
3032 json!({
3033 "project1": {
3034 ".git": {},
3035 "src": {
3036 "lib.rs": text_2
3037 },
3038 "README.md": "# project 1",
3039 },
3040 }),
3041 )
3042 .await;
3043 fs.set_index_for_repo(
3044 Path::new("/code/project1/.git"),
3045 &[("src/lib.rs", text_1.clone())],
3046 );
3047 fs.set_head_for_repo(
3048 Path::new("/code/project1/.git"),
3049 &[("src/lib.rs", text_1.clone())],
3050 "deadbeef",
3051 );
3052
3053 let (project, _headless) = init_test(&fs, cx, server_cx).await;
3054 let (worktree, _) = project
3055 .update(cx, |project, cx| {
3056 project.find_or_create_worktree("/code/project1", true, cx)
3057 })
3058 .await
3059 .unwrap();
3060 let worktree_id = cx.update(|cx| worktree.read(cx).id());
3061 cx.executor().run_until_parked();
3062
3063 let buffer = project
3064 .update(cx, |project, cx| {
3065 project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
3066 })
3067 .await
3068 .unwrap();
3069 let diff = project
3070 .update(cx, |project, cx| {
3071 project.open_uncommitted_diff(buffer.clone(), cx)
3072 })
3073 .await
3074 .unwrap();
3075
3076 diff.read_with(cx, |diff, cx| {
3077 assert_eq!(diff.base_text_string(cx).unwrap(), text_1);
3078 assert_eq!(
3079 diff.secondary_diff()
3080 .unwrap()
3081 .read(cx)
3082 .base_text_string(cx)
3083 .unwrap(),
3084 text_1
3085 );
3086 });
3087
3088 // stage the current buffer's contents
3089 fs.set_index_for_repo(
3090 Path::new("/code/project1/.git"),
3091 &[("src/lib.rs", text_2.clone())],
3092 );
3093
3094 cx.executor().run_until_parked();
3095 diff.read_with(cx, |diff, cx| {
3096 assert_eq!(diff.base_text_string(cx).unwrap(), text_1);
3097 assert_eq!(
3098 diff.secondary_diff()
3099 .unwrap()
3100 .read(cx)
3101 .base_text_string(cx)
3102 .unwrap(),
3103 text_2
3104 );
3105 });
3106
3107 // commit the current buffer's contents
3108 fs.set_head_for_repo(
3109 Path::new("/code/project1/.git"),
3110 &[("src/lib.rs", text_2.clone())],
3111 "deadbeef",
3112 );
3113
3114 cx.executor().run_until_parked();
3115 diff.read_with(cx, |diff, cx| {
3116 assert_eq!(diff.base_text_string(cx).unwrap(), text_2);
3117 assert_eq!(
3118 diff.secondary_diff()
3119 .unwrap()
3120 .read(cx)
3121 .base_text_string(cx)
3122 .unwrap(),
3123 text_2
3124 );
3125 });
3126}
3127
3128#[gpui::test]
3129async fn test_remote_git_diffs_when_recv_update_repository_delay(
3130 cx: &mut TestAppContext,
3131 server_cx: &mut TestAppContext,
3132) {
3133 cx.update(|cx| {
3134 let settings_store = SettingsStore::test(cx);
3135 cx.set_global(settings_store);
3136 theme_settings::init(theme::LoadThemes::JustBase, cx);
3137 release_channel::init(semver::Version::new(0, 0, 0), cx);
3138 editor::init(cx);
3139 });
3140
3141 use editor::Editor;
3142 use gpui::VisualContext;
3143 let text_2 = "
3144 fn one() -> usize {
3145 1
3146 }
3147 "
3148 .unindent();
3149 let text_1 = "
3150 fn one() -> usize {
3151 0
3152 }
3153 "
3154 .unindent();
3155
3156 let fs = FakeFs::new(server_cx.executor());
3157 fs.insert_tree(
3158 path!("/code"),
3159 json!({
3160 "project1": {
3161 "src": {
3162 "lib.rs": text_2
3163 },
3164 "README.md": "# project 1",
3165 },
3166 }),
3167 )
3168 .await;
3169
3170 let (project, _headless) = init_test(&fs, cx, server_cx).await;
3171 let (worktree, _) = project
3172 .update(cx, |project, cx| {
3173 project.find_or_create_worktree(path!("/code/project1"), true, cx)
3174 })
3175 .await
3176 .unwrap();
3177 let worktree_id = cx.update(|cx| worktree.read(cx).id());
3178 let buffer = project
3179 .update(cx, |project, cx| {
3180 project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
3181 })
3182 .await
3183 .unwrap();
3184 let buffer_id = cx.update(|cx| buffer.read(cx).remote_id());
3185
3186 let cx = cx.add_empty_window();
3187 let editor = cx.new_window_entity(|window, cx| {
3188 Editor::for_buffer(buffer, Some(project.clone()), window, cx)
3189 });
3190
3191 // Remote server will send proto::UpdateRepository after the instance of Editor create.
3192 fs.insert_tree(
3193 path!("/code"),
3194 json!({
3195 "project1": {
3196 ".git": {},
3197 },
3198 }),
3199 )
3200 .await;
3201
3202 fs.set_index_for_repo(
3203 Path::new(path!("/code/project1/.git")),
3204 &[("src/lib.rs", text_1.clone())],
3205 );
3206 fs.set_head_for_repo(
3207 Path::new(path!("/code/project1/.git")),
3208 &[("src/lib.rs", text_1.clone())],
3209 "sha",
3210 );
3211
3212 cx.executor().run_until_parked();
3213 let diff = editor
3214 .read_with(cx, |editor, cx| {
3215 editor
3216 .buffer()
3217 .read_with(cx, |buffer, _| buffer.diff_for(buffer_id))
3218 })
3219 .unwrap();
3220
3221 diff.read_with(cx, |diff, cx| {
3222 assert_eq!(diff.base_text_string(cx).unwrap(), text_1);
3223 assert_eq!(
3224 diff.secondary_diff()
3225 .unwrap()
3226 .read(cx)
3227 .base_text_string(cx)
3228 .unwrap(),
3229 text_1
3230 );
3231 });
3232
3233 // stage the current buffer's contents
3234 fs.set_index_for_repo(
3235 Path::new(path!("/code/project1/.git")),
3236 &[("src/lib.rs", text_2.clone())],
3237 );
3238
3239 cx.executor().run_until_parked();
3240 diff.read_with(cx, |diff, cx| {
3241 assert_eq!(diff.base_text_string(cx).unwrap(), text_1);
3242 assert_eq!(
3243 diff.secondary_diff()
3244 .unwrap()
3245 .read(cx)
3246 .base_text_string(cx)
3247 .unwrap(),
3248 text_2
3249 );
3250 });
3251
3252 // commit the current buffer's contents
3253 fs.set_head_for_repo(
3254 Path::new(path!("/code/project1/.git")),
3255 &[("src/lib.rs", text_2.clone())],
3256 "sha",
3257 );
3258
3259 cx.executor().run_until_parked();
3260 diff.read_with(cx, |diff, cx| {
3261 assert_eq!(diff.base_text_string(cx).unwrap(), text_2);
3262 assert_eq!(
3263 diff.secondary_diff()
3264 .unwrap()
3265 .read(cx)
3266 .base_text_string(cx)
3267 .unwrap(),
3268 text_2
3269 );
3270 });
3271}
3272
3273#[gpui::test]
3274async fn test_remote_git_branches(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
3275 let fs = FakeFs::new(server_cx.executor());
3276 fs.insert_tree(
3277 path!("/code"),
3278 json!({
3279 "project1": {
3280 ".git": {},
3281 "README.md": "# project 1",
3282 },
3283 }),
3284 )
3285 .await;
3286
3287 let (project, headless_project) = init_test(&fs, cx, server_cx).await;
3288 let branches = ["main", "dev", "feature-1"];
3289 let branches_set = branches
3290 .iter()
3291 .map(ToString::to_string)
3292 .collect::<HashSet<_>>();
3293 fs.insert_branches(Path::new(path!("/code/project1/.git")), &branches);
3294
3295 let (_worktree, _) = project
3296 .update(cx, |project, cx| {
3297 project.find_or_create_worktree(path!("/code/project1"), true, cx)
3298 })
3299 .await
3300 .unwrap();
3301 // Give the worktree a bit of time to index the file system
3302 cx.run_until_parked();
3303
3304 let repository = project.update(cx, |project, cx| project.active_repository(cx).unwrap());
3305
3306 let remote_branches = repository
3307 .update(cx, |repository, _| repository.branches())
3308 .await
3309 .unwrap()
3310 .unwrap()
3311 .branches;
3312
3313 let new_branch = branches[2];
3314
3315 let remote_branches = remote_branches
3316 .into_iter()
3317 .map(|branch| branch.name().to_string())
3318 .collect::<HashSet<_>>();
3319
3320 assert_eq!(&remote_branches, &branches_set);
3321
3322 cx.update(|cx| {
3323 repository.update(cx, |repository, _cx| {
3324 repository.change_branch(new_branch.to_string())
3325 })
3326 })
3327 .await
3328 .unwrap()
3329 .unwrap();
3330
3331 cx.run_until_parked();
3332
3333 let server_branch = server_cx.update(|cx| {
3334 headless_project.update(cx, |headless_project, cx| {
3335 headless_project.git_store.update(cx, |git_store, cx| {
3336 git_store
3337 .repositories()
3338 .values()
3339 .next()
3340 .unwrap()
3341 .read(cx)
3342 .branch
3343 .as_ref()
3344 .unwrap()
3345 .clone()
3346 })
3347 })
3348 });
3349
3350 assert_eq!(server_branch.name(), branches[2]);
3351
3352 // Also try creating a new branch
3353 cx.update(|cx| {
3354 repository.update(cx, |repo, _cx| {
3355 repo.create_branch("totally-new-branch".to_string(), None)
3356 })
3357 })
3358 .await
3359 .unwrap()
3360 .unwrap();
3361
3362 cx.update(|cx| {
3363 repository.update(cx, |repo, _cx| {
3364 repo.change_branch("totally-new-branch".to_string())
3365 })
3366 })
3367 .await
3368 .unwrap()
3369 .unwrap();
3370
3371 cx.run_until_parked();
3372
3373 let server_branch = server_cx.update(|cx| {
3374 headless_project.update(cx, |headless_project, cx| {
3375 headless_project.git_store.update(cx, |git_store, cx| {
3376 git_store
3377 .repositories()
3378 .values()
3379 .next()
3380 .unwrap()
3381 .read(cx)
3382 .branch
3383 .as_ref()
3384 .unwrap()
3385 .clone()
3386 })
3387 })
3388 });
3389
3390 assert_eq!(server_branch.name(), "totally-new-branch");
3391
3392 let default_branch = cx
3393 .update(|cx| repository.update(cx, |repository, _cx| repository.default_branch(false)))
3394 .await
3395 .unwrap()
3396 .unwrap();
3397 assert_eq!(default_branch.as_deref(), Some("main"));
3398
3399 let default_branch_with_remote = cx
3400 .update(|cx| repository.update(cx, |repository, _cx| repository.default_branch(true)))
3401 .await
3402 .unwrap()
3403 .unwrap();
3404 assert_eq!(default_branch_with_remote.as_deref(), Some("origin/main"));
3405}
3406
3407#[gpui::test]
3408async fn test_remote_git_checkpoints(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
3409 let fs = FakeFs::new(server_cx.executor());
3410 fs.insert_tree(
3411 path!("/code"),
3412 json!({
3413 "project1": {
3414 ".git": {},
3415 "file.txt": "original content",
3416 },
3417 }),
3418 )
3419 .await;
3420
3421 let (project, _headless) = init_test(&fs, cx, server_cx).await;
3422
3423 let (_worktree, _) = project
3424 .update(cx, |project, cx| {
3425 project.find_or_create_worktree(path!("/code/project1"), true, cx)
3426 })
3427 .await
3428 .unwrap();
3429 cx.run_until_parked();
3430
3431 let repository = project.update(cx, |project, cx| project.active_repository(cx).unwrap());
3432
3433 // 1. Create a checkpoint of the original state
3434 let checkpoint_1 = repository
3435 .update(cx, |repository, _| repository.checkpoint())
3436 .await
3437 .unwrap()
3438 .unwrap();
3439
3440 // 2. Modify a file on the server-side fs
3441 fs.write(
3442 Path::new(path!("/code/project1/file.txt")),
3443 b"modified content",
3444 )
3445 .await
3446 .unwrap();
3447
3448 // 3. Create a second checkpoint with the modified state
3449 let checkpoint_2 = repository
3450 .update(cx, |repository, _| repository.checkpoint())
3451 .await
3452 .unwrap()
3453 .unwrap();
3454
3455 // 4. compare_checkpoints: same checkpoint with itself => equal
3456 let equal = repository
3457 .update(cx, |repository, _| {
3458 repository.compare_checkpoints(checkpoint_1.clone(), checkpoint_1.clone())
3459 })
3460 .await
3461 .unwrap()
3462 .unwrap();
3463 assert!(equal, "a checkpoint compared with itself should be equal");
3464
3465 // 5. compare_checkpoints: different states => not equal
3466 let equal = repository
3467 .update(cx, |repository, _| {
3468 repository.compare_checkpoints(checkpoint_1.clone(), checkpoint_2.clone())
3469 })
3470 .await
3471 .unwrap()
3472 .unwrap();
3473 assert!(
3474 !equal,
3475 "checkpoints of different states should not be equal"
3476 );
3477
3478 // 6. diff_checkpoints: same checkpoint => empty diff
3479 let diff = repository
3480 .update(cx, |repository, _| {
3481 repository.diff_checkpoints(checkpoint_1.clone(), checkpoint_1.clone())
3482 })
3483 .await
3484 .unwrap()
3485 .unwrap();
3486 assert!(
3487 diff.is_empty(),
3488 "diff of identical checkpoints should be empty"
3489 );
3490
3491 // 7. diff_checkpoints: different checkpoints => non-empty diff mentioning the changed file
3492 let diff = repository
3493 .update(cx, |repository, _| {
3494 repository.diff_checkpoints(checkpoint_1.clone(), checkpoint_2.clone())
3495 })
3496 .await
3497 .unwrap()
3498 .unwrap();
3499 assert!(
3500 !diff.is_empty(),
3501 "diff of different checkpoints should be non-empty"
3502 );
3503 assert!(
3504 diff.contains("file.txt"),
3505 "diff should mention the changed file"
3506 );
3507 assert!(
3508 diff.contains("original content"),
3509 "diff should contain removed content"
3510 );
3511 assert!(
3512 diff.contains("modified content"),
3513 "diff should contain added content"
3514 );
3515
3516 // 8. restore_checkpoint: restore to original state
3517 repository
3518 .update(cx, |repository, _| {
3519 repository.restore_checkpoint(checkpoint_1.clone())
3520 })
3521 .await
3522 .unwrap()
3523 .unwrap();
3524 cx.run_until_parked();
3525
3526 // 9. Create a checkpoint after restore
3527 let checkpoint_3 = repository
3528 .update(cx, |repository, _| repository.checkpoint())
3529 .await
3530 .unwrap()
3531 .unwrap();
3532
3533 // 10. compare_checkpoints: restored state matches original
3534 let equal = repository
3535 .update(cx, |repository, _| {
3536 repository.compare_checkpoints(checkpoint_1.clone(), checkpoint_3.clone())
3537 })
3538 .await
3539 .unwrap()
3540 .unwrap();
3541 assert!(equal, "restored state should match original checkpoint");
3542
3543 // 11. diff_checkpoints: restored state vs original => empty diff
3544 let diff = repository
3545 .update(cx, |repository, _| {
3546 repository.diff_checkpoints(checkpoint_1.clone(), checkpoint_3.clone())
3547 })
3548 .await
3549 .unwrap()
3550 .unwrap();
3551 assert!(diff.is_empty(), "diff after restore should be empty");
3552}
3553
3554#[gpui::test]
3555async fn test_remote_agent_fs_tool_calls(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
3556 let fs = FakeFs::new(server_cx.executor());
3557 fs.insert_tree(
3558 path!("/project"),
3559 json!({
3560 "a.txt": "A",
3561 "b.txt": "B",
3562 }),
3563 )
3564 .await;
3565
3566 let (project, _headless_project) = init_test(&fs, cx, server_cx).await;
3567 project
3568 .update(cx, |project, cx| {
3569 project.find_or_create_worktree(path!("/project"), true, cx)
3570 })
3571 .await
3572 .unwrap();
3573
3574 let action_log = cx.new(|_| action_log::ActionLog::new(project.clone()));
3575
3576 let input = ReadFileToolInput {
3577 path: "project/b.txt".into(),
3578 start_line: None,
3579 end_line: None,
3580 };
3581 let read_tool = Arc::new(ReadFileTool::new(project, action_log, true));
3582 let (event_stream, _) = ToolCallEventStream::test();
3583
3584 let exists_result = cx.update(|cx| {
3585 read_tool
3586 .clone()
3587 .run(ToolInput::resolved(input), event_stream.clone(), cx)
3588 });
3589 let output = exists_result.await.unwrap();
3590 assert_eq!(
3591 output,
3592 LanguageModelToolResultContent::Text(" 1\tB".into())
3593 );
3594
3595 let input = ReadFileToolInput {
3596 path: "project/c.txt".into(),
3597 start_line: None,
3598 end_line: None,
3599 };
3600 let does_not_exist_result =
3601 cx.update(|cx| read_tool.run(ToolInput::resolved(input), event_stream, cx));
3602 does_not_exist_result.await.unwrap_err();
3603}
3604
3605#[gpui::test]
3606async fn test_adding_remote_skill(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
3607 use acp_thread::AgentConnection as _;
3608
3609 let fs = FakeFs::new(server_cx.executor());
3610 fs.insert_tree(
3611 path!("/project"),
3612 json!({
3613 ".agents": {
3614 "skills": {
3615 "test-skill": {
3616 "SKILL.md": "---\nname: test-skill\ndescription: test description\n---\ntest body"
3617 }
3618 }
3619 }
3620 }),
3621 )
3622 .await;
3623
3624 let (project, _headless_project) = init_test(&fs, cx, server_cx).await;
3625 cx.update(|cx| {
3626 LanguageModelRegistry::test(cx);
3627 });
3628 let (_worktree, _rel_path) = project
3629 .update(cx, |project, cx| {
3630 project.find_or_create_worktree(path!("/project"), true, cx)
3631 })
3632 .await
3633 .unwrap();
3634 cx.run_until_parked();
3635 let thread_store = cx.new(|cx| ThreadStore::new(cx));
3636 let agent = cx.update(|cx| NativeAgent::new(thread_store, Templates::new(), fs.clone(), cx));
3637 let connection = Rc::new(NativeAgentConnection(agent.clone()));
3638 let _acp_thread = cx
3639 .update(|cx| {
3640 connection.clone().new_session(
3641 project.clone(),
3642 PathList::new(&[Path::new("/project")]),
3643 cx,
3644 )
3645 })
3646 .await
3647 .unwrap();
3648 cx.run_until_parked();
3649
3650 let skill_tool = Arc::new(SkillTool::with_body_resolver(
3651 skills_resolver_for_project(agent.downgrade(), project.entity_id()),
3652 skill_body_resolver_for_project(project.clone(), fs.clone()),
3653 ));
3654 let (event_stream, mut event_stream_rx) = ToolCallEventStream::test();
3655
3656 let input = SkillToolInput {
3657 name: "test-skill".into(),
3658 };
3659 let task = cx.update(|cx| {
3660 skill_tool
3661 .clone()
3662 .run(ToolInput::resolved(input), event_stream.clone(), cx)
3663 });
3664
3665 // The project-local skill is not a built-in, so the tool requests
3666 // authorization. Approve it so the tool can proceed.
3667 let authorization = event_stream_rx.expect_authorization().await;
3668 authorization
3669 .response
3670 .send(acp_thread::SelectedPermissionOutcome::new(
3671 agent_client_protocol::schema::v1::PermissionOptionId::new("allow"),
3672 agent_client_protocol::schema::v1::PermissionOptionKind::AllowOnce,
3673 ))
3674 .unwrap();
3675
3676 let output = task.await.unwrap();
3677 cx.run_until_parked();
3678 let expected = format!(
3679 concat!(
3680 "<skill_content name=\"test-skill\">\n",
3681 "<source>project-local</source>\n",
3682 "<worktree>project</worktree>\n",
3683 "<directory>{}</directory>\n",
3684 "Relative paths in this skill resolve against <directory>.\n",
3685 "\n",
3686 "test body\n",
3687 "</skill_content>\n",
3688 ),
3689 path!("/project/.agents/skills/test-skill"),
3690 );
3691 assert_eq!(output, SkillToolOutput::Found { rendered: expected });
3692
3693 fs.create_dir(Path::new(path!("/project/.agents/skills/test-2")))
3694 .await
3695 .unwrap();
3696 fs.insert_file(
3697 path!("/project/.agents/skills/test-2/SKILL.md"),
3698 "---\nname: test-2\ndescription: test description\n---\ntest body"
3699 .as_bytes()
3700 .into(),
3701 )
3702 .await;
3703
3704 cx.run_until_parked();
3705 cx.update(|cx| connection.refresh_skills_for_project(project, cx));
3706 cx.run_until_parked();
3707
3708 let input2 = SkillToolInput {
3709 name: "test-2".into(),
3710 };
3711 let task = cx.update(|cx| {
3712 skill_tool
3713 .clone()
3714 .run(ToolInput::resolved(input2), event_stream.clone(), cx)
3715 });
3716
3717 let authorization = event_stream_rx.expect_authorization().await;
3718 authorization
3719 .response
3720 .send(acp_thread::SelectedPermissionOutcome::new(
3721 agent_client_protocol::schema::v1::PermissionOptionId::new("allow"),
3722 agent_client_protocol::schema::v1::PermissionOptionKind::AllowOnce,
3723 ))
3724 .unwrap();
3725
3726 let output = task.await.unwrap();
3727 let expected2 = format!(
3728 concat!(
3729 "<skill_content name=\"test-2\">\n",
3730 "<source>project-local</source>\n",
3731 "<worktree>project</worktree>\n",
3732 "<directory>{}</directory>\n",
3733 "Relative paths in this skill resolve against <directory>.\n",
3734 "\n",
3735 "test body\n",
3736 "</skill_content>\n",
3737 ),
3738 path!("/project/.agents/skills/test-2"),
3739 );
3740 assert_eq!(
3741 output,
3742 SkillToolOutput::Found {
3743 rendered: expected2
3744 }
3745 );
3746}
3747
3748#[gpui::test]
3749async fn test_remote_external_agent_server(
3750 cx: &mut TestAppContext,
3751 server_cx: &mut TestAppContext,
3752) {
3753 let fs = FakeFs::new(server_cx.executor());
3754 fs.insert_tree(path!("/project"), json!({})).await;
3755
3756 let (project, _headless_project) = init_test(&fs, cx, server_cx).await;
3757 project
3758 .update(cx, |project, cx| {
3759 project.find_or_create_worktree(path!("/project"), true, cx)
3760 })
3761 .await
3762 .unwrap();
3763 let names = project.update(cx, |project, cx| {
3764 project
3765 .agent_server_store()
3766 .read(cx)
3767 .external_agents()
3768 .map(|name| name.to_string())
3769 .collect::<Vec<_>>()
3770 });
3771 pretty_assertions::assert_eq!(names, Vec::<String>::new());
3772 server_cx.update_global::<SettingsStore, _>(|settings_store, cx| {
3773 settings_store
3774 .set_server_settings(
3775 &json!({
3776 "agent_servers": {
3777 "foo": {
3778 "type": "custom",
3779 "command": "foo-cli",
3780 "args": ["--flag"],
3781 "env": {
3782 "VAR": "val"
3783 }
3784 }
3785 }
3786 })
3787 .to_string(),
3788 cx,
3789 )
3790 .unwrap();
3791 });
3792 server_cx.run_until_parked();
3793 cx.run_until_parked();
3794 let names = project.update(cx, |project, cx| {
3795 project
3796 .agent_server_store()
3797 .read(cx)
3798 .external_agents()
3799 .map(|name| name.to_string())
3800 .collect::<Vec<_>>()
3801 });
3802 pretty_assertions::assert_eq!(names, ["foo"]);
3803 let command = project
3804 .update(cx, |project, cx| {
3805 project.agent_server_store().update(cx, |store, cx| {
3806 store
3807 .get_external_agent(&"foo".into())
3808 .unwrap()
3809 .get_command(
3810 vec![],
3811 HashMap::from_iter([("OTHER_VAR".into(), "other-val".into())]),
3812 &mut cx.to_async(),
3813 )
3814 })
3815 })
3816 .await
3817 .unwrap();
3818 assert_eq!(
3819 command,
3820 AgentServerCommand {
3821 path: "foo-cli".into(),
3822 args: vec!["--flag".into()],
3823 env: Some(HashMap::from_iter([
3824 ("NO_BROWSER".into(), "1".into()),
3825 ("VAR".into(), "val".into()),
3826 ("OTHER_VAR".into(), "other-val".into())
3827 ]))
3828 }
3829 );
3830}
3831
3832#[gpui::test]
3833async fn test_remote_apply_code_action_skips_unadvertised_command(
3834 cx: &mut TestAppContext,
3835 server_cx: &mut TestAppContext,
3836) {
3837 let fs = FakeFs::new(server_cx.executor());
3838 fs.insert_tree(
3839 path!("/code"),
3840 json!({
3841 "project1": {
3842 ".git": {},
3843 "README.md": "# project 1",
3844 "src": {
3845 "lib.rs": "fn one() -> usize { 1 }"
3846 }
3847 },
3848 }),
3849 )
3850 .await;
3851
3852 let (project, headless) = init_test(&fs, cx, server_cx).await;
3853
3854 fs.insert_tree(
3855 path!("/code/project1/.zed"),
3856 json!({
3857 "settings.json": r#"
3858 {
3859 "languages": {"Rust":{"language_servers":["rust-analyzer"]}},
3860 "lsp": {
3861 "rust-analyzer": {
3862 "binary": {
3863 "path": "~/.cargo/bin/rust-analyzer"
3864 }
3865 }
3866 }
3867 }"#
3868 }),
3869 )
3870 .await;
3871
3872 cx.update_entity(&project, |project, _| {
3873 project.languages().register_test_language(LanguageConfig {
3874 name: "Rust".into(),
3875 matcher: (LanguageMatcher {
3876 path_suffixes: vec!["rs".into()],
3877 ..Default::default()
3878 })
3879 .into(),
3880 ..Default::default()
3881 });
3882 project.languages().register_fake_lsp_adapter(
3883 "Rust",
3884 FakeLspAdapter {
3885 name: "rust-analyzer",
3886 ..Default::default()
3887 },
3888 )
3889 });
3890
3891 // Register the fake LSP with an empty execute_command_provider and a handler that panics
3892 // if it is ever reached: commands not advertised by the server must be rejected by
3893 // `apply_code_action` before dispatching to the language server.
3894 let mut fake_lsp = server_cx.update(|cx| {
3895 headless.read(cx).languages.register_fake_lsp_server(
3896 LanguageServerName("rust-analyzer".into()),
3897 lsp::ServerCapabilities {
3898 execute_command_provider: Some(lsp::ExecuteCommandOptions {
3899 commands: Vec::new(),
3900 ..Default::default()
3901 }),
3902 ..Default::default()
3903 },
3904 Some(Box::new(|fake| {
3905 fake.set_request_handler::<lsp::request::ExecuteCommand, _, _>(
3906 |params, _| async move {
3907 panic!(
3908 "Unadvertised command {} must not reach the language server",
3909 params.command
3910 );
3911 },
3912 );
3913 })),
3914 )
3915 });
3916
3917 cx.run_until_parked();
3918
3919 let worktree_id = project
3920 .update(cx, |project, cx| {
3921 project.find_or_create_worktree(path!("/code/project1"), true, cx)
3922 })
3923 .await
3924 .unwrap()
3925 .0
3926 .read_with(cx, |worktree, _| worktree.id());
3927
3928 cx.run_until_parked();
3929
3930 let (buffer, _handle) = project
3931 .update(cx, |project, cx| {
3932 project.open_buffer_with_lsp((worktree_id, rel_path("src/lib.rs")), cx)
3933 })
3934 .await
3935 .unwrap();
3936
3937 cx.run_until_parked();
3938
3939 let _fake_lsp = fake_lsp.next().await.unwrap();
3940
3941 let server_id = server_cx.read(|cx| {
3942 *headless
3943 .read(cx)
3944 .lsp_store
3945 .read(cx)
3946 .as_local()
3947 .unwrap()
3948 .language_servers
3949 .keys()
3950 .next()
3951 .unwrap()
3952 });
3953 let buffer_id = cx.read(|cx| buffer.read(cx).remote_id());
3954
3955 let action = project::CodeAction {
3956 server_id,
3957 range: language::Anchor::min_min_range_for_buffer(buffer_id),
3958 lsp_action: project::LspAction::Command(lsp::Command {
3959 title: "\u{25b6}\u{fe0e} Run Tests".into(),
3960 command: "rust-analyzer.runSingle".into(),
3961 arguments: Some(vec![json!({"label": "test-mod tests"})]),
3962 }),
3963 resolved: true,
3964 };
3965
3966 let transaction = project
3967 .update(cx, |project, cx| {
3968 project.apply_code_action(buffer.clone(), action, true, cx)
3969 })
3970 .await
3971 .expect("Unadvertised command must not be forwarded to executeCommand");
3972 assert_eq!(transaction.0.len(), 0);
3973}
3974
3975#[gpui::test]
3976async fn test_remote_restore_unstaged_hunk_clears_diff(
3977 cx: &mut TestAppContext,
3978 server_cx: &mut TestAppContext,
3979) {
3980 cx.update(|cx| {
3981 let settings_store = SettingsStore::test(cx);
3982 cx.set_global(settings_store);
3983 theme_settings::init(theme::LoadThemes::JustBase, cx);
3984 release_channel::init(semver::Version::new(0, 0, 0), cx);
3985 editor::init(cx);
3986 });
3987
3988 use editor::Editor;
3989 use gpui::VisualContext;
3990
3991 let base_text = "
3992 fn one() -> usize {
3993 1
3994 }
3995 "
3996 .unindent();
3997 let modified_text = "
3998 fn one() -> usize {
3999 100
4000 }
4001 "
4002 .unindent();
4003
4004 let fs = FakeFs::new(server_cx.executor());
4005 fs.insert_tree(
4006 path!("/code"),
4007 json!({
4008 "project1": {
4009 ".git": {},
4010 "src": {
4011 "lib.rs": modified_text
4012 },
4013 },
4014 }),
4015 )
4016 .await;
4017 fs.set_index_for_repo(
4018 Path::new(path!("/code/project1/.git")),
4019 &[("src/lib.rs", base_text.clone())],
4020 );
4021 fs.set_head_for_repo(
4022 Path::new(path!("/code/project1/.git")),
4023 &[("src/lib.rs", base_text.clone())],
4024 "deadbeef",
4025 );
4026
4027 let (project, _headless) = init_test(&fs, cx, server_cx).await;
4028 let worktree_id = {
4029 let (worktree, _) = project
4030 .update(cx, |project, cx| {
4031 project.find_or_create_worktree(path!("/code/project1"), true, cx)
4032 })
4033 .await
4034 .unwrap();
4035 cx.update(|cx| worktree.read(cx).id())
4036 };
4037 cx.executor().run_until_parked();
4038
4039 let buffer = project
4040 .update(cx, |project, cx| {
4041 project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
4042 })
4043 .await
4044 .unwrap();
4045
4046 let cx = cx.add_empty_window();
4047 let editor = cx.new_window_entity(|window, cx| {
4048 Editor::for_buffer(buffer, Some(project.clone()), window, cx)
4049 });
4050 cx.executor().run_until_parked();
4051
4052 editor.update_in(cx, |editor, window, cx| {
4053 let snapshot = editor.snapshot(window, cx);
4054 let hunks: Vec<_> = editor
4055 .diff_hunks_in_ranges(
4056 &[editor::Anchor::Min..editor::Anchor::Max],
4057 &snapshot.buffer_snapshot(),
4058 )
4059 .collect();
4060 assert!(!hunks.is_empty(), "should have diff hunks before restore");
4061 });
4062
4063 cx.update_window_entity(&editor, |editor, window, cx| {
4064 editor.select_all(&editor::actions::SelectAll, window, cx);
4065 editor.git_restore(&git::Restore, window, cx);
4066 });
4067 cx.executor().run_until_parked();
4068
4069 editor.update_in(cx, |editor, _window, cx| {
4070 let snapshot = editor.buffer().read(cx).snapshot(cx);
4071 assert_eq!(
4072 snapshot.text(),
4073 base_text,
4074 "buffer text should match base after restoring all hunks"
4075 );
4076
4077 let hunks: Vec<_> = editor
4078 .diff_hunks_in_ranges(&[editor::Anchor::Min..editor::Anchor::Max], &snapshot)
4079 .collect();
4080 assert!(hunks.is_empty(), "should have no diff hunks after restore");
4081 });
4082}
4083
4084#[gpui::test]
4085async fn test_remote_load_commit_template(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
4086 let fs = FakeFs::new(server_cx.executor());
4087
4088 fs.insert_tree(
4089 path!("/root"),
4090 json!({
4091 "project": {
4092 ".git": {},
4093 "README.md": "# Project's README"
4094 }
4095 }),
4096 )
4097 .await;
4098
4099 fs.with_git_state(Path::new(path!("/root/project/.git")), false, |state| {
4100 state.commit_template = Some(GitCommitTemplate {
4101 template: "chore: commit template".to_string(),
4102 })
4103 })
4104 .expect("Should successfully update repository state");
4105
4106 let (project, _) = init_test(&fs, cx, server_cx).await;
4107 project
4108 .update(cx, |project, cx| {
4109 project.find_or_create_worktree(path!("/root/project"), true, cx)
4110 })
4111 .await
4112 .unwrap();
4113 cx.run_until_parked();
4114
4115 let repository = project.update(cx, |project, cx| project.active_repository(cx).unwrap());
4116 let commit_template = repository
4117 .update(cx, |repository, _| repository.load_commit_template_text())
4118 .await
4119 .unwrap()
4120 .unwrap()
4121 .expect("Loading commit template in remote should work");
4122
4123 assert_eq!(commit_template.template, "chore: commit template");
4124}
4125
4126#[gpui::test]
4127async fn test_remote_delete_project_entry_with_trash(
4128 cx: &mut TestAppContext,
4129 server_cx: &mut TestAppContext,
4130) {
4131 let fs = FakeFs::new(server_cx.executor());
4132
4133 fs.insert_tree(
4134 path!("/root"),
4135 json!({
4136 "zed": {
4137 "file_a.txt": "File A"
4138 }
4139 }),
4140 )
4141 .await;
4142
4143 let (project, _headless_project) = init_test(&fs, cx, server_cx).await;
4144 let (worktree, _path) = project
4145 .update(cx, |project, cx| {
4146 project.find_or_create_worktree(path!("/root/zed"), true, cx)
4147 })
4148 .await
4149 .unwrap();
4150 cx.run_until_parked();
4151
4152 let entry_id = worktree.update(cx, |worktree, _cx| {
4153 worktree.entry_for_path(rel_path("file_a.txt")).unwrap().id
4154 });
4155
4156 let remote_client = project.read_with(cx, |project, _cx| {
4157 project
4158 .remote_client()
4159 .expect("project should have a remote client")
4160 });
4161
4162 let proto_client =
4163 remote_client.read_with(cx, |remote_client, _cx| remote_client.proto_client());
4164
4165 proto_client
4166 .request(proto::DeleteProjectEntry {
4167 project_id: proto::REMOTE_SERVER_PROJECT_ID,
4168 entry_id: entry_id.to_proto(),
4169 #[allow(deprecated)]
4170 use_trash: true,
4171 })
4172 .await
4173 .unwrap();
4174
4175 assert_eq!(
4176 fs.trashed_paths(),
4177 vec![PathBuf::from(path!("/root/zed/file_a.txt"))]
4178 );
4179}
4180
4181#[gpui::test]
4182async fn test_remote_trash_restore(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
4183 let fs = FakeFs::new(server_cx.executor());
4184
4185 fs.insert_tree(
4186 path!("/root"),
4187 json!({
4188 "zed": {
4189 "file_a.txt": "File A"
4190 }
4191 }),
4192 )
4193 .await;
4194
4195 let (project, _headless_project) = init_test(&fs, cx, server_cx).await;
4196 let (worktree, _path) = project
4197 .update(cx, |project, cx| {
4198 project.find_or_create_worktree(path!("/root/zed"), true, cx)
4199 })
4200 .await
4201 .unwrap();
4202 cx.run_until_parked();
4203
4204 let worktree_id = worktree.update(cx, |worktree, _cx| worktree.id());
4205 let entry_id = worktree.update(cx, |worktree, _cx| {
4206 worktree.entry_for_path(rel_path("file_a.txt")).unwrap().id
4207 });
4208
4209 let trash_id = project
4210 .update(cx, |project, cx| project.trash_entry(entry_id, cx))
4211 .unwrap()
4212 .await
4213 .unwrap();
4214 cx.run_until_parked();
4215
4216 worktree.update(cx, |worktree, _cx| {
4217 assert!(worktree.entry_for_path(rel_path("file_a.txt")).is_none());
4218 });
4219
4220 project
4221 .update(cx, |project, cx| {
4222 project.restore_entry(worktree_id, trash_id, cx)
4223 })
4224 .await
4225 .unwrap();
4226 cx.run_until_parked();
4227
4228 worktree.update(cx, |worktree, _cx| {
4229 assert!(worktree.entry_for_path(rel_path("file_a.txt")).is_some());
4230 });
4231}
4232
4233pub async fn init_test(
4234 server_fs: &Arc<FakeFs>,
4235 cx: &mut TestAppContext,
4236 server_cx: &mut TestAppContext,
4237) -> (Entity<Project>, Entity<HeadlessProject>) {
4238 let server_fs = server_fs.clone();
4239 cx.update(|cx| {
4240 release_channel::init(semver::Version::new(0, 0, 0), cx);
4241 });
4242 server_cx.update(|cx| {
4243 release_channel::init(semver::Version::new(0, 0, 0), cx);
4244 });
4245 init_logger();
4246
4247 let (opts, ssh_server_client, _) = RemoteClient::fake_server(cx, server_cx);
4248 let http_client = Arc::new(BlockedHttpClient);
4249 let node_runtime = NodeRuntime::unavailable();
4250 let languages = Arc::new(LanguageRegistry::new(cx.executor()));
4251 let proxy = Arc::new(ExtensionHostProxy::new());
4252 server_cx.update(HeadlessProject::init);
4253 let headless = server_cx.new(|cx| {
4254 HeadlessProject::new(
4255 crate::HeadlessAppState {
4256 session: ssh_server_client,
4257 fs: server_fs.clone(),
4258 http_client,
4259 node_runtime,
4260 languages,
4261 extension_host_proxy: proxy,
4262 startup_time: std::time::Instant::now(),
4263 },
4264 false,
4265 cx,
4266 )
4267 });
4268
4269 let ssh = RemoteClient::connect_mock(opts, cx).await;
4270 let project = build_project(ssh, cx);
4271 project
4272 .update(cx, {
4273 let headless = headless.clone();
4274 |_, cx| cx.on_release(|_, _| drop(headless))
4275 })
4276 .detach();
4277 (project, headless)
4278}
4279
4280fn init_logger() {
4281 zlog::init_test();
4282}
4283
4284fn build_project(ssh: Entity<RemoteClient>, cx: &mut TestAppContext) -> Entity<Project> {
4285 cx.update(|cx| {
4286 if !cx.has_global::<SettingsStore>() {
4287 let settings_store = SettingsStore::test(cx);
4288 cx.set_global(settings_store);
4289 }
4290 });
4291
4292 let client = cx.update(|cx| {
4293 Client::new(
4294 Arc::new(FakeSystemClock::new()),
4295 FakeHttpClient::with_404_response(),
4296 cx,
4297 )
4298 });
4299
4300 let node = NodeRuntime::unavailable();
4301 let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
4302 let languages = Arc::new(LanguageRegistry::test(cx.executor()));
4303 let fs = FakeFs::new(cx.executor());
4304
4305 cx.update(|cx| {
4306 Project::init(&client, cx);
4307 });
4308
4309 cx.update(|cx| Project::remote(ssh, client, node, user_store, languages, fs, false, cx))
4310}
4311