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::{AgentTool, ReadFileTool, ReadFileToolInput, ToolCallEventStream, ToolInput};
6use client::{Client, UserStore};
7use clock::FakeSystemClock;
8use collections::{HashMap, HashSet};
9use language_model::LanguageModelToolResultContent;
10use languages::rust_lang;
11
12use extension::ExtensionHostProxy;
13use fs::{FakeFs, Fs};
14use git::repository::Worktree as GitWorktree;
15use gpui::{AppContext as _, Entity, SharedString, TestAppContext};
16use http_client::{BlockedHttpClient, FakeHttpClient};
17use language::{
18 Buffer, FakeLspAdapter, LanguageConfig, LanguageMatcher, LanguageRegistry, LineEnding,
19 language_settings::{AllLanguageSettings, LanguageSettings},
20};
21use lsp::{
22 CompletionContext, CompletionResponse, CompletionTriggerKind, DEFAULT_LSP_REQUEST_TIMEOUT,
23 LanguageServerName,
24};
25use node_runtime::NodeRuntime;
26use project::{
27 ProgressToken, Project,
28 agent_server_store::AgentServerCommand,
29 search::{SearchQuery, SearchResult},
30};
31use remote::RemoteClient;
32use serde_json::json;
33use settings::{Settings, SettingsLocation, SettingsStore, initial_server_settings_content};
34use smol::stream::StreamExt;
35use std::{
36 path::{Path, PathBuf},
37 sync::Arc,
38};
39use unindent::Unindent as _;
40use util::{path, paths::PathMatcher, rel_path::rel_path};
41
42#[gpui::test]
43async fn test_basic_remote_editing(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
44 let fs = FakeFs::new(server_cx.executor());
45 fs.insert_tree(
46 path!("/code"),
47 json!({
48 "project1": {
49 ".git": {},
50 "README.md": "# project 1",
51 "src": {
52 "lib.rs": "fn one() -> usize { 1 }"
53 }
54 },
55 "project2": {
56 "README.md": "# project 2",
57 },
58 }),
59 )
60 .await;
61 fs.set_index_for_repo(
62 Path::new(path!("/code/project1/.git")),
63 &[("src/lib.rs", "fn one() -> usize { 0 }".into())],
64 );
65
66 let (project, _headless) = init_test(&fs, cx, server_cx).await;
67 let (worktree, _) = project
68 .update(cx, |project, cx| {
69 project.find_or_create_worktree(path!("/code/project1"), true, cx)
70 })
71 .await
72 .unwrap();
73
74 // The client sees the worktree's contents.
75 cx.executor().run_until_parked();
76 let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
77 worktree.update(cx, |worktree, _cx| {
78 assert_eq!(
79 worktree.paths().collect::<Vec<_>>(),
80 vec![
81 rel_path("README.md"),
82 rel_path("src"),
83 rel_path("src/lib.rs"),
84 ]
85 );
86 });
87
88 // The user opens a buffer in the remote worktree. The buffer's
89 // contents are loaded from the remote filesystem.
90 let buffer = project
91 .update(cx, |project, cx| {
92 project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
93 })
94 .await
95 .unwrap();
96 let diff = project
97 .update(cx, |project, cx| {
98 project.open_unstaged_diff(buffer.clone(), cx)
99 })
100 .await
101 .unwrap();
102
103 diff.update(cx, |diff, cx| {
104 assert_eq!(
105 diff.base_text_string(cx).unwrap(),
106 "fn one() -> usize { 0 }"
107 );
108 });
109
110 buffer.update(cx, |buffer, cx| {
111 assert_eq!(buffer.text(), "fn one() -> usize { 1 }");
112 let ix = buffer.text().find('1').unwrap();
113 buffer.edit([(ix..ix + 1, "100")], None, cx);
114 });
115
116 // The user saves the buffer. The new contents are written to the
117 // remote filesystem.
118 project
119 .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
120 .await
121 .unwrap();
122 assert_eq!(
123 fs.load("/code/project1/src/lib.rs".as_ref()).await.unwrap(),
124 "fn one() -> usize { 100 }"
125 );
126
127 // A new file is created in the remote filesystem. The user
128 // sees the new file.
129 fs.save(
130 path!("/code/project1/src/main.rs").as_ref(),
131 &"fn main() {}".into(),
132 Default::default(),
133 )
134 .await
135 .unwrap();
136 cx.executor().run_until_parked();
137 worktree.update(cx, |worktree, _cx| {
138 assert_eq!(
139 worktree.paths().collect::<Vec<_>>(),
140 vec![
141 rel_path("README.md"),
142 rel_path("src"),
143 rel_path("src/lib.rs"),
144 rel_path("src/main.rs"),
145 ]
146 );
147 });
148
149 // A file that is currently open in a buffer is renamed.
150 fs.rename(
151 path!("/code/project1/src/lib.rs").as_ref(),
152 path!("/code/project1/src/lib2.rs").as_ref(),
153 Default::default(),
154 )
155 .await
156 .unwrap();
157 cx.executor().run_until_parked();
158 buffer.update(cx, |buffer, _| {
159 assert_eq!(&**buffer.file().unwrap().path(), rel_path("src/lib2.rs"));
160 });
161
162 fs.set_index_for_repo(
163 Path::new(path!("/code/project1/.git")),
164 &[("src/lib2.rs", "fn one() -> usize { 100 }".into())],
165 );
166 cx.executor().run_until_parked();
167 diff.update(cx, |diff, cx| {
168 assert_eq!(
169 diff.base_text_string(cx).unwrap(),
170 "fn one() -> usize { 100 }"
171 );
172 });
173}
174
175async fn do_search_and_assert(
176 project: &Entity<Project>,
177 query: &str,
178 files_to_include: PathMatcher,
179 match_full_paths: bool,
180 expected_paths: &[&str],
181 mut cx: TestAppContext,
182) -> Vec<Entity<Buffer>> {
183 let query = query.to_string();
184 let receiver = project.update(&mut cx, |project, cx| {
185 project.search(
186 SearchQuery::text(
187 query,
188 false,
189 true,
190 false,
191 files_to_include,
192 Default::default(),
193 match_full_paths,
194 None,
195 )
196 .unwrap(),
197 cx,
198 )
199 });
200
201 let mut buffers = Vec::new();
202 for expected_path in expected_paths {
203 let response = receiver.rx.recv().await.unwrap();
204 let SearchResult::Buffer { buffer, .. } = response else {
205 panic!("incorrect result");
206 };
207 buffer.update(&mut cx, |buffer, cx| {
208 assert_eq!(
209 buffer.file().unwrap().full_path(cx).to_string_lossy(),
210 *expected_path
211 )
212 });
213 buffers.push(buffer);
214 }
215
216 assert!(receiver.rx.recv().await.is_err());
217 buffers
218}
219
220#[gpui::test]
221async fn test_remote_project_search(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
222 let fs = FakeFs::new(server_cx.executor());
223 fs.insert_tree(
224 path!("/code"),
225 json!({
226 "project1": {
227 ".git": {},
228 "README.md": "# project 1",
229 "src": {
230 "lib.rs": "fn one() -> usize { 1 }"
231 }
232 },
233 }),
234 )
235 .await;
236
237 let (project, headless) = init_test(&fs, cx, server_cx).await;
238
239 project
240 .update(cx, |project, cx| {
241 project.find_or_create_worktree(path!("/code/project1"), true, cx)
242 })
243 .await
244 .unwrap();
245
246 cx.run_until_parked();
247
248 let buffers = do_search_and_assert(
249 &project,
250 "project",
251 Default::default(),
252 false,
253 &[path!("project1/README.md")],
254 cx.clone(),
255 )
256 .await;
257 let buffer = buffers.into_iter().next().unwrap();
258
259 // test that the headless server is tracking which buffers we have open correctly.
260 cx.run_until_parked();
261 headless.update(server_cx, |headless, cx| {
262 assert!(headless.buffer_store.read(cx).has_shared_buffers())
263 });
264 do_search_and_assert(
265 &project,
266 "project",
267 Default::default(),
268 false,
269 &[path!("project1/README.md")],
270 cx.clone(),
271 )
272 .await;
273 server_cx.run_until_parked();
274 cx.update(|_| {
275 drop(buffer);
276 });
277 cx.run_until_parked();
278 server_cx.run_until_parked();
279 headless.update(server_cx, |headless, cx| {
280 assert!(!headless.buffer_store.read(cx).has_shared_buffers())
281 });
282
283 do_search_and_assert(
284 &project,
285 "project",
286 Default::default(),
287 false,
288 &[path!("project1/README.md")],
289 cx.clone(),
290 )
291 .await;
292}
293
294#[gpui::test]
295async fn test_remote_project_search_single_cpu(
296 cx: &mut TestAppContext,
297 server_cx: &mut TestAppContext,
298) {
299 let fs = FakeFs::new(server_cx.executor());
300 fs.insert_tree(
301 path!("/code"),
302 json!({
303 "project1": {
304 ".git": {},
305 "README.md": "# project 1",
306 "src": {
307 "lib.rs": "fn one() -> usize { 1 }"
308 }
309 },
310 }),
311 )
312 .await;
313
314 // Simulate a single-CPU environment (e.g. a devcontainer with 1 visible CPU).
315 // This causes the worker pool in project search to spawn num_cpus - 1 = 0 workers,
316 // which silently drops all search channels and produces zero results.
317 server_cx.executor().set_num_cpus(1);
318
319 let (project, _) = init_test(&fs, cx, server_cx).await;
320
321 project
322 .update(cx, |project, cx| {
323 project.find_or_create_worktree(path!("/code/project1"), true, cx)
324 })
325 .await
326 .unwrap();
327
328 cx.run_until_parked();
329
330 do_search_and_assert(
331 &project,
332 "project",
333 Default::default(),
334 false,
335 &[path!("project1/README.md")],
336 cx.clone(),
337 )
338 .await;
339}
340
341#[gpui::test]
342async fn test_remote_project_search_inclusion(
343 cx: &mut TestAppContext,
344 server_cx: &mut TestAppContext,
345) {
346 let fs = FakeFs::new(server_cx.executor());
347 fs.insert_tree(
348 path!("/code"),
349 json!({
350 "project1": {
351 "README.md": "# project 1",
352 },
353 "project2": {
354 "README.md": "# project 2",
355 },
356 }),
357 )
358 .await;
359
360 let (project, _) = 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 project
370 .update(cx, |project, cx| {
371 project.find_or_create_worktree(path!("/code/project2"), true, cx)
372 })
373 .await
374 .unwrap();
375
376 cx.run_until_parked();
377
378 // Case 1: Test search with path matcher limiting to only one worktree
379 let path_matcher = PathMatcher::new(
380 &["project1/*.md".to_owned()],
381 util::paths::PathStyle::local(),
382 )
383 .unwrap();
384 do_search_and_assert(
385 &project,
386 "project",
387 path_matcher,
388 true, // should be true in case of multiple worktrees
389 &[path!("project1/README.md")],
390 cx.clone(),
391 )
392 .await;
393
394 // Case 2: Test search without path matcher, matching both worktrees
395 do_search_and_assert(
396 &project,
397 "project",
398 Default::default(),
399 true, // should be true in case of multiple worktrees
400 &[path!("project1/README.md"), path!("project2/README.md")],
401 cx.clone(),
402 )
403 .await;
404}
405
406#[gpui::test]
407async fn test_remote_settings(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
408 let fs = FakeFs::new(server_cx.executor());
409 fs.insert_tree(
410 "/code",
411 json!({
412 "project1": {
413 ".git": {},
414 "README.md": "# project 1",
415 "src": {
416 "lib.rs": "fn one() -> usize { 1 }"
417 }
418 },
419 }),
420 )
421 .await;
422
423 let (project, headless) = init_test(&fs, cx, server_cx).await;
424
425 cx.update_global(|settings_store: &mut SettingsStore, cx| {
426 settings_store.set_user_settings(
427 r#"{"languages":{"Rust":{"language_servers":["from-local-settings"]}}}"#,
428 cx,
429 )
430 })
431 .unwrap();
432
433 cx.run_until_parked();
434
435 server_cx.read(|cx| {
436 assert_eq!(
437 AllLanguageSettings::get_global(cx)
438 .language(None, Some(&"Rust".into()), cx)
439 .language_servers,
440 ["from-local-settings"],
441 "User language settings should be synchronized with the server settings"
442 )
443 });
444
445 server_cx
446 .update_global(|settings_store: &mut SettingsStore, cx| {
447 settings_store.set_server_settings(
448 r#"{"languages":{"Rust":{"language_servers":["from-server-settings"]}}}"#,
449 cx,
450 )
451 })
452 .unwrap();
453
454 cx.run_until_parked();
455
456 server_cx.read(|cx| {
457 assert_eq!(
458 AllLanguageSettings::get_global(cx)
459 .language(None, Some(&"Rust".into()), cx)
460 .language_servers,
461 ["from-server-settings".to_string()],
462 "Server language settings should take precedence over the user settings"
463 )
464 });
465
466 fs.insert_tree(
467 "/code/project1/.zed",
468 json!({
469 "settings.json": r#"
470 {
471 "languages": {"Rust":{"language_servers":["override-rust-analyzer"]}},
472 "lsp": {
473 "override-rust-analyzer": {
474 "binary": {
475 "path": "~/.cargo/bin/rust-analyzer"
476 }
477 }
478 }
479 }"#
480 }),
481 )
482 .await;
483
484 let worktree_id = project
485 .update(cx, |project, cx| {
486 project.languages().add(rust_lang());
487 project.find_or_create_worktree("/code/project1", true, cx)
488 })
489 .await
490 .unwrap()
491 .0
492 .read_with(cx, |worktree, _| worktree.id());
493
494 let buffer = project
495 .update(cx, |project, cx| {
496 project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
497 })
498 .await
499 .unwrap();
500 cx.run_until_parked();
501
502 server_cx.read(|cx| {
503 let worktree_id = headless
504 .read(cx)
505 .worktree_store
506 .read(cx)
507 .worktrees()
508 .next()
509 .unwrap()
510 .read(cx)
511 .id();
512 assert_eq!(
513 AllLanguageSettings::get(
514 Some(SettingsLocation {
515 worktree_id,
516 path: rel_path("src/lib.rs")
517 }),
518 cx
519 )
520 .language(None, Some(&"Rust".into()), cx)
521 .language_servers,
522 ["override-rust-analyzer".to_string()]
523 )
524 });
525
526 cx.read(|cx| {
527 assert_eq!(
528 LanguageSettings::for_buffer(buffer.read(cx), cx).language_servers,
529 ["override-rust-analyzer".to_string()]
530 )
531 });
532}
533
534#[gpui::test]
535async fn test_remote_lsp(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
536 let fs = FakeFs::new(server_cx.executor());
537 fs.insert_tree(
538 path!("/code"),
539 json!({
540 "project1": {
541 ".git": {},
542 "README.md": "# project 1",
543 "src": {
544 "lib.rs": "fn one() -> usize { 1 }"
545 }
546 },
547 }),
548 )
549 .await;
550
551 let (project, headless) = init_test(&fs, cx, server_cx).await;
552
553 fs.insert_tree(
554 path!("/code/project1/.zed"),
555 json!({
556 "settings.json": r#"
557 {
558 "languages": {"Rust":{"language_servers":["rust-analyzer", "fake-analyzer"]}},
559 "lsp": {
560 "rust-analyzer": {
561 "binary": {
562 "path": "~/.cargo/bin/rust-analyzer"
563 }
564 },
565 "fake-analyzer": {
566 "binary": {
567 "path": "~/.cargo/bin/rust-analyzer"
568 }
569 }
570 }
571 }"#
572 }),
573 )
574 .await;
575
576 cx.update_entity(&project, |project, _| {
577 project.languages().register_test_language(LanguageConfig {
578 name: "Rust".into(),
579 matcher: LanguageMatcher {
580 path_suffixes: vec!["rs".into()],
581 ..Default::default()
582 },
583 ..Default::default()
584 });
585 project.languages().register_fake_lsp_adapter(
586 "Rust",
587 FakeLspAdapter {
588 name: "rust-analyzer",
589 capabilities: lsp::ServerCapabilities {
590 completion_provider: Some(lsp::CompletionOptions::default()),
591 rename_provider: Some(lsp::OneOf::Left(true)),
592 ..lsp::ServerCapabilities::default()
593 },
594 ..FakeLspAdapter::default()
595 },
596 );
597 project.languages().register_fake_lsp_adapter(
598 "Rust",
599 FakeLspAdapter {
600 name: "fake-analyzer",
601 capabilities: lsp::ServerCapabilities {
602 completion_provider: Some(lsp::CompletionOptions::default()),
603 rename_provider: Some(lsp::OneOf::Left(true)),
604 ..lsp::ServerCapabilities::default()
605 },
606 ..FakeLspAdapter::default()
607 },
608 )
609 });
610
611 let mut fake_lsp = server_cx.update(|cx| {
612 headless.read(cx).languages.register_fake_lsp_server(
613 LanguageServerName("rust-analyzer".into()),
614 lsp::ServerCapabilities {
615 completion_provider: Some(lsp::CompletionOptions::default()),
616 rename_provider: Some(lsp::OneOf::Left(true)),
617 ..lsp::ServerCapabilities::default()
618 },
619 None,
620 )
621 });
622
623 let mut fake_second_lsp = server_cx.update(|cx| {
624 headless.read(cx).languages.register_fake_lsp_adapter(
625 "Rust",
626 FakeLspAdapter {
627 name: "fake-analyzer",
628 capabilities: lsp::ServerCapabilities {
629 completion_provider: Some(lsp::CompletionOptions::default()),
630 rename_provider: Some(lsp::OneOf::Left(true)),
631 ..lsp::ServerCapabilities::default()
632 },
633 ..FakeLspAdapter::default()
634 },
635 );
636 headless.read(cx).languages.register_fake_lsp_server(
637 LanguageServerName("fake-analyzer".into()),
638 lsp::ServerCapabilities {
639 completion_provider: Some(lsp::CompletionOptions::default()),
640 rename_provider: Some(lsp::OneOf::Left(true)),
641 ..lsp::ServerCapabilities::default()
642 },
643 None,
644 )
645 });
646
647 cx.run_until_parked();
648
649 let worktree_id = project
650 .update(cx, |project, cx| {
651 project.languages().add(rust_lang());
652 project.find_or_create_worktree(path!("/code/project1"), true, cx)
653 })
654 .await
655 .unwrap()
656 .0
657 .read_with(cx, |worktree, _| worktree.id());
658
659 // Wait for the settings to synchronize
660 cx.run_until_parked();
661
662 let (buffer, _handle) = project
663 .update(cx, |project, cx| {
664 project.open_buffer_with_lsp((worktree_id, rel_path("src/lib.rs")), cx)
665 })
666 .await
667 .unwrap();
668 cx.run_until_parked();
669
670 let fake_lsp = fake_lsp.next().await.unwrap();
671 let fake_second_lsp = fake_second_lsp.next().await.unwrap();
672
673 cx.read(|cx| {
674 assert_eq!(
675 LanguageSettings::for_buffer(buffer.read(cx), cx).language_servers,
676 ["rust-analyzer".to_string(), "fake-analyzer".to_string()]
677 )
678 });
679
680 let buffer_id = cx.read(|cx| {
681 let buffer = buffer.read(cx);
682 assert_eq!(buffer.language().unwrap().name(), "Rust");
683 buffer.remote_id()
684 });
685
686 server_cx.read(|cx| {
687 let buffer = headless
688 .read(cx)
689 .buffer_store
690 .read(cx)
691 .get(buffer_id)
692 .unwrap();
693
694 assert_eq!(buffer.read(cx).language().unwrap().name(), "Rust");
695 });
696
697 server_cx.read(|cx| {
698 let lsp_store = headless.read(cx).lsp_store.read(cx);
699 assert_eq!(lsp_store.as_local().unwrap().language_servers.len(), 2);
700 });
701
702 fake_lsp.set_request_handler::<lsp::request::Completion, _, _>(|_, _| async move {
703 Ok(Some(CompletionResponse::Array(vec![lsp::CompletionItem {
704 label: "boop".to_string(),
705 ..Default::default()
706 }])))
707 });
708
709 fake_second_lsp.set_request_handler::<lsp::request::Completion, _, _>(|_, _| async move {
710 Ok(Some(CompletionResponse::Array(vec![lsp::CompletionItem {
711 label: "beep".to_string(),
712 ..Default::default()
713 }])))
714 });
715
716 let result = project
717 .update(cx, |project, cx| {
718 project.completions(
719 &buffer,
720 0,
721 CompletionContext {
722 trigger_kind: CompletionTriggerKind::INVOKED,
723 trigger_character: None,
724 },
725 cx,
726 )
727 })
728 .await
729 .unwrap();
730
731 assert_eq!(
732 result
733 .into_iter()
734 .flat_map(|response| response.completions)
735 .map(|c| c.label.text)
736 .collect::<Vec<_>>(),
737 vec!["boop".to_string(), "beep".to_string()]
738 );
739
740 fake_lsp.set_request_handler::<lsp::request::Rename, _, _>(|_, _| async move {
741 Ok(Some(lsp::WorkspaceEdit {
742 changes: Some(
743 [(
744 lsp::Uri::from_file_path(path!("/code/project1/src/lib.rs")).unwrap(),
745 vec![lsp::TextEdit::new(
746 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 6)),
747 "two".to_string(),
748 )],
749 )]
750 .into_iter()
751 .collect(),
752 ),
753 ..Default::default()
754 }))
755 });
756
757 project
758 .update(cx, |project, cx| {
759 project.perform_rename(buffer.clone(), 3, "two".to_string(), cx)
760 })
761 .await
762 .unwrap();
763
764 cx.run_until_parked();
765 buffer.update(cx, |buffer, _| {
766 assert_eq!(buffer.text(), "fn two() -> usize { 1 }")
767 })
768}
769
770#[gpui::test]
771async fn test_remote_cancel_language_server_work(
772 cx: &mut TestAppContext,
773 server_cx: &mut TestAppContext,
774) {
775 let fs = FakeFs::new(server_cx.executor());
776 fs.insert_tree(
777 path!("/code"),
778 json!({
779 "project1": {
780 ".git": {},
781 "README.md": "# project 1",
782 "src": {
783 "lib.rs": "fn one() -> usize { 1 }"
784 }
785 },
786 }),
787 )
788 .await;
789
790 let (project, headless) = init_test(&fs, cx, server_cx).await;
791
792 fs.insert_tree(
793 path!("/code/project1/.zed"),
794 json!({
795 "settings.json": r#"
796 {
797 "languages": {"Rust":{"language_servers":["rust-analyzer"]}},
798 "lsp": {
799 "rust-analyzer": {
800 "binary": {
801 "path": "~/.cargo/bin/rust-analyzer"
802 }
803 }
804 }
805 }"#
806 }),
807 )
808 .await;
809
810 cx.update_entity(&project, |project, _| {
811 project.languages().register_test_language(LanguageConfig {
812 name: "Rust".into(),
813 matcher: LanguageMatcher {
814 path_suffixes: vec!["rs".into()],
815 ..Default::default()
816 },
817 ..Default::default()
818 });
819 project.languages().register_fake_lsp_adapter(
820 "Rust",
821 FakeLspAdapter {
822 name: "rust-analyzer",
823 ..Default::default()
824 },
825 )
826 });
827
828 let mut fake_lsp = server_cx.update(|cx| {
829 headless.read(cx).languages.register_fake_lsp_server(
830 LanguageServerName("rust-analyzer".into()),
831 Default::default(),
832 None,
833 )
834 });
835
836 cx.run_until_parked();
837
838 let worktree_id = project
839 .update(cx, |project, cx| {
840 project.find_or_create_worktree(path!("/code/project1"), true, cx)
841 })
842 .await
843 .unwrap()
844 .0
845 .read_with(cx, |worktree, _| worktree.id());
846
847 cx.run_until_parked();
848
849 let (buffer, _handle) = project
850 .update(cx, |project, cx| {
851 project.open_buffer_with_lsp((worktree_id, rel_path("src/lib.rs")), cx)
852 })
853 .await
854 .unwrap();
855
856 cx.run_until_parked();
857
858 let mut fake_lsp = fake_lsp.next().await.unwrap();
859
860 // Cancelling all language server work for a given buffer
861 {
862 // Two operations, one cancellable and one not.
863 fake_lsp
864 .start_progress_with(
865 "another-token",
866 lsp::WorkDoneProgressBegin {
867 cancellable: Some(false),
868 ..Default::default()
869 },
870 DEFAULT_LSP_REQUEST_TIMEOUT,
871 )
872 .await;
873
874 let progress_token = "the-progress-token";
875 fake_lsp
876 .start_progress_with(
877 progress_token,
878 lsp::WorkDoneProgressBegin {
879 cancellable: Some(true),
880 ..Default::default()
881 },
882 DEFAULT_LSP_REQUEST_TIMEOUT,
883 )
884 .await;
885
886 cx.executor().run_until_parked();
887
888 project.update(cx, |project, cx| {
889 project.cancel_language_server_work_for_buffers([buffer.clone()], cx)
890 });
891
892 cx.executor().run_until_parked();
893
894 // Verify the cancellation was received on the server side
895 let cancel_notification = fake_lsp
896 .receive_notification::<lsp::notification::WorkDoneProgressCancel>()
897 .await;
898 assert_eq!(
899 cancel_notification.token,
900 lsp::NumberOrString::String(progress_token.into())
901 );
902 }
903
904 // Cancelling work by server_id and token
905 {
906 let server_id = fake_lsp.server.server_id();
907 let progress_token = "the-progress-token";
908
909 fake_lsp
910 .start_progress_with(
911 progress_token,
912 lsp::WorkDoneProgressBegin {
913 cancellable: Some(true),
914 ..Default::default()
915 },
916 DEFAULT_LSP_REQUEST_TIMEOUT,
917 )
918 .await;
919
920 cx.executor().run_until_parked();
921
922 project.update(cx, |project, cx| {
923 project.cancel_language_server_work(
924 server_id,
925 Some(ProgressToken::String(SharedString::from(progress_token))),
926 cx,
927 )
928 });
929
930 cx.executor().run_until_parked();
931
932 // Verify the cancellation was received on the server side
933 let cancel_notification = fake_lsp
934 .receive_notification::<lsp::notification::WorkDoneProgressCancel>()
935 .await;
936 assert_eq!(
937 cancel_notification.token,
938 lsp::NumberOrString::String(progress_token.to_owned())
939 );
940 }
941}
942
943#[gpui::test]
944async fn test_remote_reload(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
945 let fs = FakeFs::new(server_cx.executor());
946 fs.insert_tree(
947 path!("/code"),
948 json!({
949 "project1": {
950 ".git": {},
951 "README.md": "# project 1",
952 "src": {
953 "lib.rs": "fn one() -> usize { 1 }"
954 }
955 },
956 }),
957 )
958 .await;
959
960 let (project, _headless) = init_test(&fs, cx, server_cx).await;
961 let (worktree, _) = project
962 .update(cx, |project, cx| {
963 project.find_or_create_worktree(path!("/code/project1"), true, cx)
964 })
965 .await
966 .unwrap();
967
968 let worktree_id = cx.update(|cx| worktree.read(cx).id());
969
970 let buffer = project
971 .update(cx, |project, cx| {
972 project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
973 })
974 .await
975 .unwrap();
976
977 fs.save(
978 &PathBuf::from(path!("/code/project1/src/lib.rs")),
979 &("bangles".to_string().into()),
980 LineEnding::Unix,
981 )
982 .await
983 .unwrap();
984
985 cx.run_until_parked();
986
987 buffer.update(cx, |buffer, cx| {
988 assert_eq!(buffer.text(), "bangles");
989 buffer.edit([(0..0, "a")], None, cx);
990 });
991
992 fs.save(
993 &PathBuf::from(path!("/code/project1/src/lib.rs")),
994 &("bloop".to_string().into()),
995 LineEnding::Unix,
996 )
997 .await
998 .unwrap();
999
1000 cx.run_until_parked();
1001 cx.update(|cx| {
1002 assert!(buffer.read(cx).has_conflict());
1003 });
1004
1005 project
1006 .update(cx, |project, cx| {
1007 project.reload_buffers([buffer.clone()].into_iter().collect(), false, cx)
1008 })
1009 .await
1010 .unwrap();
1011 cx.run_until_parked();
1012
1013 cx.update(|cx| {
1014 assert!(!buffer.read(cx).has_conflict());
1015 });
1016}
1017
1018#[gpui::test]
1019async fn test_remote_resolve_path_in_buffer(
1020 cx: &mut TestAppContext,
1021 server_cx: &mut TestAppContext,
1022) {
1023 let fs = FakeFs::new(server_cx.executor());
1024 // Even though we are not testing anything from project1, it is necessary to test if project2 is picking up correct worktree
1025 fs.insert_tree(
1026 path!("/code"),
1027 json!({
1028 "project1": {
1029 ".git": {},
1030 "README.md": "# project 1",
1031 "src": {
1032 "lib.rs": "fn one() -> usize { 1 }"
1033 }
1034 },
1035 "project2": {
1036 ".git": {},
1037 "README.md": "# project 2",
1038 "src": {
1039 "lib.rs": "fn two() -> usize { 2 }"
1040 }
1041 }
1042 }),
1043 )
1044 .await;
1045
1046 let (project, _headless) = init_test(&fs, cx, server_cx).await;
1047
1048 let _ = project
1049 .update(cx, |project, cx| {
1050 project.find_or_create_worktree(path!("/code/project1"), true, cx)
1051 })
1052 .await
1053 .unwrap();
1054
1055 let (worktree2, _) = project
1056 .update(cx, |project, cx| {
1057 project.find_or_create_worktree(path!("/code/project2"), true, cx)
1058 })
1059 .await
1060 .unwrap();
1061
1062 let worktree2_id = cx.update(|cx| worktree2.read(cx).id());
1063
1064 cx.run_until_parked();
1065
1066 let buffer2 = project
1067 .update(cx, |project, cx| {
1068 project.open_buffer((worktree2_id, rel_path("src/lib.rs")), cx)
1069 })
1070 .await
1071 .unwrap();
1072
1073 let path = project
1074 .update(cx, |project, cx| {
1075 project.resolve_path_in_buffer(path!("/code/project2/README.md"), &buffer2, cx)
1076 })
1077 .await
1078 .unwrap();
1079 assert!(path.is_file());
1080 assert_eq!(path.abs_path().unwrap(), path!("/code/project2/README.md"));
1081
1082 let path = project
1083 .update(cx, |project, cx| {
1084 project.resolve_path_in_buffer("../README.md", &buffer2, cx)
1085 })
1086 .await
1087 .unwrap();
1088 assert!(path.is_file());
1089 assert_eq!(
1090 path.project_path().unwrap().clone(),
1091 (worktree2_id, rel_path("README.md")).into()
1092 );
1093
1094 let path = project
1095 .update(cx, |project, cx| {
1096 project.resolve_path_in_buffer("../src", &buffer2, cx)
1097 })
1098 .await
1099 .unwrap();
1100 assert_eq!(
1101 path.project_path().unwrap().clone(),
1102 (worktree2_id, rel_path("src")).into()
1103 );
1104 assert!(path.is_dir());
1105}
1106
1107#[gpui::test]
1108async fn test_remote_resolve_abs_path(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1109 let fs = FakeFs::new(server_cx.executor());
1110 fs.insert_tree(
1111 path!("/code"),
1112 json!({
1113 "project1": {
1114 ".git": {},
1115 "README.md": "# project 1",
1116 "src": {
1117 "lib.rs": "fn one() -> usize { 1 }"
1118 }
1119 },
1120 }),
1121 )
1122 .await;
1123
1124 let (project, _headless) = init_test(&fs, cx, server_cx).await;
1125
1126 let path = project
1127 .update(cx, |project, cx| {
1128 project.resolve_abs_path(path!("/code/project1/README.md"), cx)
1129 })
1130 .await
1131 .unwrap();
1132
1133 assert!(path.is_file());
1134 assert_eq!(path.abs_path().unwrap(), path!("/code/project1/README.md"));
1135
1136 let path = project
1137 .update(cx, |project, cx| {
1138 project.resolve_abs_path(path!("/code/project1/src"), cx)
1139 })
1140 .await
1141 .unwrap();
1142
1143 assert!(path.is_dir());
1144 assert_eq!(path.abs_path().unwrap(), path!("/code/project1/src"));
1145
1146 let path = project
1147 .update(cx, |project, cx| {
1148 project.resolve_abs_path(path!("/code/project1/DOESNOTEXIST"), cx)
1149 })
1150 .await;
1151 assert!(path.is_none());
1152}
1153
1154#[gpui::test(iterations = 10)]
1155async fn test_canceling_buffer_opening(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1156 let fs = FakeFs::new(server_cx.executor());
1157 fs.insert_tree(
1158 "/code",
1159 json!({
1160 "project1": {
1161 ".git": {},
1162 "README.md": "# project 1",
1163 "src": {
1164 "lib.rs": "fn one() -> usize { 1 }"
1165 }
1166 },
1167 }),
1168 )
1169 .await;
1170
1171 let (project, _headless) = init_test(&fs, cx, server_cx).await;
1172 let (worktree, _) = project
1173 .update(cx, |project, cx| {
1174 project.find_or_create_worktree("/code/project1", true, cx)
1175 })
1176 .await
1177 .unwrap();
1178 let worktree_id = worktree.read_with(cx, |tree, _| tree.id());
1179
1180 // Open a buffer on the client but cancel after a random amount of time.
1181 let buffer = project.update(cx, |p, cx| {
1182 p.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
1183 });
1184 cx.executor().simulate_random_delay().await;
1185 drop(buffer);
1186
1187 // Try opening the same buffer again as the client, and ensure we can
1188 // still do it despite the cancellation above.
1189 let buffer = project
1190 .update(cx, |p, cx| {
1191 p.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
1192 })
1193 .await
1194 .unwrap();
1195
1196 buffer.read_with(cx, |buf, _| {
1197 assert_eq!(buf.text(), "fn one() -> usize { 1 }")
1198 });
1199}
1200
1201#[gpui::test]
1202async fn test_adding_then_removing_then_adding_worktrees(
1203 cx: &mut TestAppContext,
1204 server_cx: &mut TestAppContext,
1205) {
1206 let fs = FakeFs::new(server_cx.executor());
1207 fs.insert_tree(
1208 path!("/code"),
1209 json!({
1210 "project1": {
1211 ".git": {},
1212 "README.md": "# project 1",
1213 "src": {
1214 "lib.rs": "fn one() -> usize { 1 }"
1215 }
1216 },
1217 "project2": {
1218 "README.md": "# project 2",
1219 },
1220 }),
1221 )
1222 .await;
1223
1224 let (project, _headless) = init_test(&fs, cx, server_cx).await;
1225 let (_worktree, _) = project
1226 .update(cx, |project, cx| {
1227 project.find_or_create_worktree(path!("/code/project1"), true, cx)
1228 })
1229 .await
1230 .unwrap();
1231
1232 let (worktree_2, _) = project
1233 .update(cx, |project, cx| {
1234 project.find_or_create_worktree(path!("/code/project2"), true, cx)
1235 })
1236 .await
1237 .unwrap();
1238 let worktree_id_2 = worktree_2.read_with(cx, |tree, _| tree.id());
1239
1240 project.update(cx, |project, cx| project.remove_worktree(worktree_id_2, cx));
1241
1242 let (worktree_2, _) = project
1243 .update(cx, |project, cx| {
1244 project.find_or_create_worktree(path!("/code/project2"), true, cx)
1245 })
1246 .await
1247 .unwrap();
1248
1249 cx.run_until_parked();
1250 worktree_2.update(cx, |worktree, _cx| {
1251 assert!(worktree.is_visible());
1252 let entries = worktree.entries(true, 0).collect::<Vec<_>>();
1253 assert_eq!(entries.len(), 2);
1254 assert_eq!(entries[1].path.as_unix_str(), "README.md")
1255 })
1256}
1257
1258#[gpui::test]
1259async fn test_open_server_settings(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1260 let fs = FakeFs::new(server_cx.executor());
1261 fs.insert_tree(
1262 path!("/code"),
1263 json!({
1264 "project1": {
1265 ".git": {},
1266 "README.md": "# project 1",
1267 "src": {
1268 "lib.rs": "fn one() -> usize { 1 }"
1269 }
1270 },
1271 }),
1272 )
1273 .await;
1274
1275 let (project, _headless) = init_test(&fs, cx, server_cx).await;
1276 let buffer = project.update(cx, |project, cx| project.open_server_settings(cx));
1277 cx.executor().run_until_parked();
1278
1279 let buffer = buffer.await.unwrap();
1280
1281 cx.update(|cx| {
1282 assert_eq!(
1283 buffer.read(cx).text(),
1284 initial_server_settings_content()
1285 .to_string()
1286 .replace("\r\n", "\n")
1287 )
1288 })
1289}
1290
1291#[gpui::test(iterations = 20)]
1292async fn test_reconnect(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1293 let fs = FakeFs::new(server_cx.executor());
1294 fs.insert_tree(
1295 path!("/code"),
1296 json!({
1297 "project1": {
1298 ".git": {},
1299 "README.md": "# project 1",
1300 "src": {
1301 "lib.rs": "fn one() -> usize { 1 }"
1302 }
1303 },
1304 }),
1305 )
1306 .await;
1307
1308 let (project, _headless) = init_test(&fs, cx, server_cx).await;
1309
1310 let (worktree, _) = project
1311 .update(cx, |project, cx| {
1312 project.find_or_create_worktree(path!("/code/project1"), true, cx)
1313 })
1314 .await
1315 .unwrap();
1316
1317 let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
1318 let buffer = project
1319 .update(cx, |project, cx| {
1320 project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
1321 })
1322 .await
1323 .unwrap();
1324
1325 buffer.update(cx, |buffer, cx| {
1326 assert_eq!(buffer.text(), "fn one() -> usize { 1 }");
1327 let ix = buffer.text().find('1').unwrap();
1328 buffer.edit([(ix..ix + 1, "100")], None, cx);
1329 });
1330
1331 let client = cx.read(|cx| project.read(cx).remote_client().unwrap());
1332 client
1333 .update(cx, |client, cx| client.simulate_disconnect(cx))
1334 .detach();
1335
1336 project
1337 .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
1338 .await
1339 .unwrap();
1340
1341 assert_eq!(
1342 fs.load(path!("/code/project1/src/lib.rs").as_ref())
1343 .await
1344 .unwrap(),
1345 "fn one() -> usize { 100 }"
1346 );
1347}
1348
1349#[gpui::test]
1350async fn test_remote_root_rename(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1351 let fs = FakeFs::new(server_cx.executor());
1352 fs.insert_tree(
1353 "/code",
1354 json!({
1355 "project1": {
1356 ".git": {},
1357 "README.md": "# project 1",
1358 },
1359 }),
1360 )
1361 .await;
1362
1363 let (project, _) = init_test(&fs, cx, server_cx).await;
1364
1365 let (worktree, _) = project
1366 .update(cx, |project, cx| {
1367 project.find_or_create_worktree("/code/project1", true, cx)
1368 })
1369 .await
1370 .unwrap();
1371
1372 cx.run_until_parked();
1373
1374 fs.rename(
1375 &PathBuf::from("/code/project1"),
1376 &PathBuf::from("/code/project2"),
1377 Default::default(),
1378 )
1379 .await
1380 .unwrap();
1381
1382 cx.run_until_parked();
1383 worktree.update(cx, |worktree, _| {
1384 assert_eq!(worktree.root_name(), "project2")
1385 })
1386}
1387
1388#[gpui::test]
1389async fn test_remote_rename_entry(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1390 let fs = FakeFs::new(server_cx.executor());
1391 fs.insert_tree(
1392 "/code",
1393 json!({
1394 "project1": {
1395 ".git": {},
1396 "README.md": "# project 1",
1397 },
1398 }),
1399 )
1400 .await;
1401
1402 let (project, _) = init_test(&fs, cx, server_cx).await;
1403 let (worktree, _) = project
1404 .update(cx, |project, cx| {
1405 project.find_or_create_worktree("/code/project1", true, cx)
1406 })
1407 .await
1408 .unwrap();
1409
1410 cx.run_until_parked();
1411
1412 let entry = project
1413 .update(cx, |project, cx| {
1414 let worktree = worktree.read(cx);
1415 let entry = worktree.entry_for_path(rel_path("README.md")).unwrap();
1416 project.rename_entry(entry.id, (worktree.id(), rel_path("README.rst")).into(), cx)
1417 })
1418 .await
1419 .unwrap()
1420 .into_included()
1421 .unwrap();
1422
1423 cx.run_until_parked();
1424
1425 worktree.update(cx, |worktree, _| {
1426 assert_eq!(
1427 worktree.entry_for_path(rel_path("README.rst")).unwrap().id,
1428 entry.id
1429 )
1430 });
1431}
1432
1433#[gpui::test]
1434async fn test_copy_file_into_remote_project(
1435 cx: &mut TestAppContext,
1436 server_cx: &mut TestAppContext,
1437) {
1438 let remote_fs = FakeFs::new(server_cx.executor());
1439 remote_fs
1440 .insert_tree(
1441 path!("/code"),
1442 json!({
1443 "project1": {
1444 ".git": {},
1445 "README.md": "# project 1",
1446 "src": {
1447 "main.rs": ""
1448 }
1449 },
1450 }),
1451 )
1452 .await;
1453
1454 let (project, _) = init_test(&remote_fs, cx, server_cx).await;
1455 let (worktree, _) = project
1456 .update(cx, |project, cx| {
1457 project.find_or_create_worktree(path!("/code/project1"), true, cx)
1458 })
1459 .await
1460 .unwrap();
1461
1462 cx.run_until_parked();
1463
1464 let local_fs = project
1465 .read_with(cx, |project, _| project.fs().clone())
1466 .as_fake();
1467 local_fs
1468 .insert_tree(
1469 path!("/local-code"),
1470 json!({
1471 "dir1": {
1472 "file1": "file 1 content",
1473 "dir2": {
1474 "file2": "file 2 content",
1475 "dir3": {
1476 "file3": ""
1477 },
1478 "dir4": {}
1479 },
1480 "dir5": {}
1481 },
1482 "file4": "file 4 content"
1483 }),
1484 )
1485 .await;
1486
1487 worktree
1488 .update(cx, |worktree, cx| {
1489 worktree.copy_external_entries(
1490 rel_path("src").into(),
1491 vec![
1492 Path::new(path!("/local-code/dir1/file1")).into(),
1493 Path::new(path!("/local-code/dir1/dir2")).into(),
1494 ],
1495 local_fs.clone(),
1496 cx,
1497 )
1498 })
1499 .await
1500 .unwrap();
1501
1502 assert_eq!(
1503 remote_fs.paths(true),
1504 vec![
1505 PathBuf::from(path!("/")),
1506 PathBuf::from(path!("/code")),
1507 PathBuf::from(path!("/code/project1")),
1508 PathBuf::from(path!("/code/project1/.git")),
1509 PathBuf::from(path!("/code/project1/README.md")),
1510 PathBuf::from(path!("/code/project1/src")),
1511 PathBuf::from(path!("/code/project1/src/dir2")),
1512 PathBuf::from(path!("/code/project1/src/file1")),
1513 PathBuf::from(path!("/code/project1/src/main.rs")),
1514 PathBuf::from(path!("/code/project1/src/dir2/dir3")),
1515 PathBuf::from(path!("/code/project1/src/dir2/dir4")),
1516 PathBuf::from(path!("/code/project1/src/dir2/file2")),
1517 PathBuf::from(path!("/code/project1/src/dir2/dir3/file3")),
1518 ]
1519 );
1520 assert_eq!(
1521 remote_fs
1522 .load(path!("/code/project1/src/file1").as_ref())
1523 .await
1524 .unwrap(),
1525 "file 1 content"
1526 );
1527 assert_eq!(
1528 remote_fs
1529 .load(path!("/code/project1/src/dir2/file2").as_ref())
1530 .await
1531 .unwrap(),
1532 "file 2 content"
1533 );
1534 assert_eq!(
1535 remote_fs
1536 .load(path!("/code/project1/src/dir2/dir3/file3").as_ref())
1537 .await
1538 .unwrap(),
1539 ""
1540 );
1541}
1542
1543#[gpui::test]
1544async fn test_remote_root_repo_common_dir(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1545 let fs = FakeFs::new(server_cx.executor());
1546 fs.insert_tree(
1547 "/code",
1548 json!({
1549 "main_repo": {
1550 ".git": {},
1551 "file.txt": "content",
1552 },
1553 "no_git": {
1554 "file.txt": "content",
1555 },
1556 }),
1557 )
1558 .await;
1559
1560 // Create a linked worktree that points back to main_repo's .git.
1561 fs.add_linked_worktree_for_repo(
1562 Path::new("/code/main_repo/.git"),
1563 false,
1564 GitWorktree {
1565 path: PathBuf::from("/code/linked_worktree"),
1566 ref_name: Some("refs/heads/feature-branch".into()),
1567 sha: "abc123".into(),
1568 is_main: false,
1569 },
1570 )
1571 .await;
1572
1573 let (project, _headless) = init_test(&fs, cx, server_cx).await;
1574
1575 // Main repo: root_repo_common_dir should be the .git directory itself.
1576 let (worktree_main, _) = project
1577 .update(cx, |project, cx| {
1578 project.find_or_create_worktree("/code/main_repo", true, cx)
1579 })
1580 .await
1581 .unwrap();
1582 cx.executor().run_until_parked();
1583
1584 let common_dir = worktree_main.read_with(cx, |worktree, _| {
1585 worktree.snapshot().root_repo_common_dir().cloned()
1586 });
1587 assert_eq!(
1588 common_dir.as_deref(),
1589 Some(Path::new("/code/main_repo/.git")),
1590 );
1591
1592 // Linked worktree: root_repo_common_dir should point to the main repo's .git.
1593 let (worktree_linked, _) = project
1594 .update(cx, |project, cx| {
1595 project.find_or_create_worktree("/code/linked_worktree", true, cx)
1596 })
1597 .await
1598 .unwrap();
1599 cx.executor().run_until_parked();
1600
1601 let common_dir = worktree_linked.read_with(cx, |worktree, _| {
1602 worktree.snapshot().root_repo_common_dir().cloned()
1603 });
1604 assert_eq!(
1605 common_dir.as_deref(),
1606 Some(Path::new("/code/main_repo/.git")),
1607 );
1608
1609 // No git repo: root_repo_common_dir should be None.
1610 let (worktree_no_git, _) = project
1611 .update(cx, |project, cx| {
1612 project.find_or_create_worktree("/code/no_git", true, cx)
1613 })
1614 .await
1615 .unwrap();
1616 cx.executor().run_until_parked();
1617
1618 let common_dir = worktree_no_git.read_with(cx, |worktree, _| {
1619 worktree.snapshot().root_repo_common_dir().cloned()
1620 });
1621 assert_eq!(common_dir, None);
1622}
1623
1624#[gpui::test]
1625async fn test_remote_git_diffs(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1626 let text_2 = "
1627 fn one() -> usize {
1628 1
1629 }
1630 "
1631 .unindent();
1632 let text_1 = "
1633 fn one() -> usize {
1634 0
1635 }
1636 "
1637 .unindent();
1638
1639 let fs = FakeFs::new(server_cx.executor());
1640 fs.insert_tree(
1641 "/code",
1642 json!({
1643 "project1": {
1644 ".git": {},
1645 "src": {
1646 "lib.rs": text_2
1647 },
1648 "README.md": "# project 1",
1649 },
1650 }),
1651 )
1652 .await;
1653 fs.set_index_for_repo(
1654 Path::new("/code/project1/.git"),
1655 &[("src/lib.rs", text_1.clone())],
1656 );
1657 fs.set_head_for_repo(
1658 Path::new("/code/project1/.git"),
1659 &[("src/lib.rs", text_1.clone())],
1660 "deadbeef",
1661 );
1662
1663 let (project, _headless) = init_test(&fs, cx, server_cx).await;
1664 let (worktree, _) = project
1665 .update(cx, |project, cx| {
1666 project.find_or_create_worktree("/code/project1", true, cx)
1667 })
1668 .await
1669 .unwrap();
1670 let worktree_id = cx.update(|cx| worktree.read(cx).id());
1671 cx.executor().run_until_parked();
1672
1673 let buffer = project
1674 .update(cx, |project, cx| {
1675 project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
1676 })
1677 .await
1678 .unwrap();
1679 let diff = project
1680 .update(cx, |project, cx| {
1681 project.open_uncommitted_diff(buffer.clone(), cx)
1682 })
1683 .await
1684 .unwrap();
1685
1686 diff.read_with(cx, |diff, cx| {
1687 assert_eq!(diff.base_text_string(cx).unwrap(), text_1);
1688 assert_eq!(
1689 diff.secondary_diff()
1690 .unwrap()
1691 .read(cx)
1692 .base_text_string(cx)
1693 .unwrap(),
1694 text_1
1695 );
1696 });
1697
1698 // stage the current buffer's contents
1699 fs.set_index_for_repo(
1700 Path::new("/code/project1/.git"),
1701 &[("src/lib.rs", text_2.clone())],
1702 );
1703
1704 cx.executor().run_until_parked();
1705 diff.read_with(cx, |diff, cx| {
1706 assert_eq!(diff.base_text_string(cx).unwrap(), text_1);
1707 assert_eq!(
1708 diff.secondary_diff()
1709 .unwrap()
1710 .read(cx)
1711 .base_text_string(cx)
1712 .unwrap(),
1713 text_2
1714 );
1715 });
1716
1717 // commit the current buffer's contents
1718 fs.set_head_for_repo(
1719 Path::new("/code/project1/.git"),
1720 &[("src/lib.rs", text_2.clone())],
1721 "deadbeef",
1722 );
1723
1724 cx.executor().run_until_parked();
1725 diff.read_with(cx, |diff, cx| {
1726 assert_eq!(diff.base_text_string(cx).unwrap(), text_2);
1727 assert_eq!(
1728 diff.secondary_diff()
1729 .unwrap()
1730 .read(cx)
1731 .base_text_string(cx)
1732 .unwrap(),
1733 text_2
1734 );
1735 });
1736}
1737
1738#[gpui::test]
1739async fn test_remote_git_diffs_when_recv_update_repository_delay(
1740 cx: &mut TestAppContext,
1741 server_cx: &mut TestAppContext,
1742) {
1743 cx.update(|cx| {
1744 let settings_store = SettingsStore::test(cx);
1745 cx.set_global(settings_store);
1746 theme_settings::init(theme::LoadThemes::JustBase, cx);
1747 release_channel::init(semver::Version::new(0, 0, 0), cx);
1748 editor::init(cx);
1749 });
1750
1751 use editor::Editor;
1752 use gpui::VisualContext;
1753 let text_2 = "
1754 fn one() -> usize {
1755 1
1756 }
1757 "
1758 .unindent();
1759 let text_1 = "
1760 fn one() -> usize {
1761 0
1762 }
1763 "
1764 .unindent();
1765
1766 let fs = FakeFs::new(server_cx.executor());
1767 fs.insert_tree(
1768 path!("/code"),
1769 json!({
1770 "project1": {
1771 "src": {
1772 "lib.rs": text_2
1773 },
1774 "README.md": "# project 1",
1775 },
1776 }),
1777 )
1778 .await;
1779
1780 let (project, _headless) = init_test(&fs, cx, server_cx).await;
1781 let (worktree, _) = project
1782 .update(cx, |project, cx| {
1783 project.find_or_create_worktree(path!("/code/project1"), true, cx)
1784 })
1785 .await
1786 .unwrap();
1787 let worktree_id = cx.update(|cx| worktree.read(cx).id());
1788 let buffer = project
1789 .update(cx, |project, cx| {
1790 project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx)
1791 })
1792 .await
1793 .unwrap();
1794 let buffer_id = cx.update(|cx| buffer.read(cx).remote_id());
1795
1796 let cx = cx.add_empty_window();
1797 let editor = cx.new_window_entity(|window, cx| {
1798 Editor::for_buffer(buffer, Some(project.clone()), window, cx)
1799 });
1800
1801 // Remote server will send proto::UpdateRepository after the instance of Editor create.
1802 fs.insert_tree(
1803 path!("/code"),
1804 json!({
1805 "project1": {
1806 ".git": {},
1807 },
1808 }),
1809 )
1810 .await;
1811
1812 fs.set_index_for_repo(
1813 Path::new(path!("/code/project1/.git")),
1814 &[("src/lib.rs", text_1.clone())],
1815 );
1816 fs.set_head_for_repo(
1817 Path::new(path!("/code/project1/.git")),
1818 &[("src/lib.rs", text_1.clone())],
1819 "sha",
1820 );
1821
1822 cx.executor().run_until_parked();
1823 let diff = editor
1824 .read_with(cx, |editor, cx| {
1825 editor
1826 .buffer()
1827 .read_with(cx, |buffer, _| buffer.diff_for(buffer_id))
1828 })
1829 .unwrap();
1830
1831 diff.read_with(cx, |diff, cx| {
1832 assert_eq!(diff.base_text_string(cx).unwrap(), text_1);
1833 assert_eq!(
1834 diff.secondary_diff()
1835 .unwrap()
1836 .read(cx)
1837 .base_text_string(cx)
1838 .unwrap(),
1839 text_1
1840 );
1841 });
1842
1843 // stage the current buffer's contents
1844 fs.set_index_for_repo(
1845 Path::new(path!("/code/project1/.git")),
1846 &[("src/lib.rs", text_2.clone())],
1847 );
1848
1849 cx.executor().run_until_parked();
1850 diff.read_with(cx, |diff, cx| {
1851 assert_eq!(diff.base_text_string(cx).unwrap(), text_1);
1852 assert_eq!(
1853 diff.secondary_diff()
1854 .unwrap()
1855 .read(cx)
1856 .base_text_string(cx)
1857 .unwrap(),
1858 text_2
1859 );
1860 });
1861
1862 // commit the current buffer's contents
1863 fs.set_head_for_repo(
1864 Path::new(path!("/code/project1/.git")),
1865 &[("src/lib.rs", text_2.clone())],
1866 "sha",
1867 );
1868
1869 cx.executor().run_until_parked();
1870 diff.read_with(cx, |diff, cx| {
1871 assert_eq!(diff.base_text_string(cx).unwrap(), text_2);
1872 assert_eq!(
1873 diff.secondary_diff()
1874 .unwrap()
1875 .read(cx)
1876 .base_text_string(cx)
1877 .unwrap(),
1878 text_2
1879 );
1880 });
1881}
1882
1883#[gpui::test]
1884async fn test_remote_git_branches(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
1885 let fs = FakeFs::new(server_cx.executor());
1886 fs.insert_tree(
1887 path!("/code"),
1888 json!({
1889 "project1": {
1890 ".git": {},
1891 "README.md": "# project 1",
1892 },
1893 }),
1894 )
1895 .await;
1896
1897 let (project, headless_project) = init_test(&fs, cx, server_cx).await;
1898 let branches = ["main", "dev", "feature-1"];
1899 let branches_set = branches
1900 .iter()
1901 .map(ToString::to_string)
1902 .collect::<HashSet<_>>();
1903 fs.insert_branches(Path::new(path!("/code/project1/.git")), &branches);
1904
1905 let (_worktree, _) = project
1906 .update(cx, |project, cx| {
1907 project.find_or_create_worktree(path!("/code/project1"), true, cx)
1908 })
1909 .await
1910 .unwrap();
1911 // Give the worktree a bit of time to index the file system
1912 cx.run_until_parked();
1913
1914 let repository = project.update(cx, |project, cx| project.active_repository(cx).unwrap());
1915
1916 let remote_branches = repository
1917 .update(cx, |repository, _| repository.branches())
1918 .await
1919 .unwrap()
1920 .unwrap();
1921
1922 let new_branch = branches[2];
1923
1924 let remote_branches = remote_branches
1925 .into_iter()
1926 .map(|branch| branch.name().to_string())
1927 .collect::<HashSet<_>>();
1928
1929 assert_eq!(&remote_branches, &branches_set);
1930
1931 cx.update(|cx| {
1932 repository.update(cx, |repository, _cx| {
1933 repository.change_branch(new_branch.to_string())
1934 })
1935 })
1936 .await
1937 .unwrap()
1938 .unwrap();
1939
1940 cx.run_until_parked();
1941
1942 let server_branch = server_cx.update(|cx| {
1943 headless_project.update(cx, |headless_project, cx| {
1944 headless_project.git_store.update(cx, |git_store, cx| {
1945 git_store
1946 .repositories()
1947 .values()
1948 .next()
1949 .unwrap()
1950 .read(cx)
1951 .branch
1952 .as_ref()
1953 .unwrap()
1954 .clone()
1955 })
1956 })
1957 });
1958
1959 assert_eq!(server_branch.name(), branches[2]);
1960
1961 // Also try creating a new branch
1962 cx.update(|cx| {
1963 repository.update(cx, |repo, _cx| {
1964 repo.create_branch("totally-new-branch".to_string(), None)
1965 })
1966 })
1967 .await
1968 .unwrap()
1969 .unwrap();
1970
1971 cx.update(|cx| {
1972 repository.update(cx, |repo, _cx| {
1973 repo.change_branch("totally-new-branch".to_string())
1974 })
1975 })
1976 .await
1977 .unwrap()
1978 .unwrap();
1979
1980 cx.run_until_parked();
1981
1982 let server_branch = server_cx.update(|cx| {
1983 headless_project.update(cx, |headless_project, cx| {
1984 headless_project.git_store.update(cx, |git_store, cx| {
1985 git_store
1986 .repositories()
1987 .values()
1988 .next()
1989 .unwrap()
1990 .read(cx)
1991 .branch
1992 .as_ref()
1993 .unwrap()
1994 .clone()
1995 })
1996 })
1997 });
1998
1999 assert_eq!(server_branch.name(), "totally-new-branch");
2000}
2001
2002#[gpui::test]
2003async fn test_remote_git_checkpoints(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
2004 let fs = FakeFs::new(server_cx.executor());
2005 fs.insert_tree(
2006 path!("/code"),
2007 json!({
2008 "project1": {
2009 ".git": {},
2010 "file.txt": "original content",
2011 },
2012 }),
2013 )
2014 .await;
2015
2016 let (project, _headless) = init_test(&fs, cx, server_cx).await;
2017
2018 let (_worktree, _) = project
2019 .update(cx, |project, cx| {
2020 project.find_or_create_worktree(path!("/code/project1"), true, cx)
2021 })
2022 .await
2023 .unwrap();
2024 cx.run_until_parked();
2025
2026 let repository = project.update(cx, |project, cx| project.active_repository(cx).unwrap());
2027
2028 // 1. Create a checkpoint of the original state
2029 let checkpoint_1 = repository
2030 .update(cx, |repository, _| repository.checkpoint())
2031 .await
2032 .unwrap()
2033 .unwrap();
2034
2035 // 2. Modify a file on the server-side fs
2036 fs.write(
2037 Path::new(path!("/code/project1/file.txt")),
2038 b"modified content",
2039 )
2040 .await
2041 .unwrap();
2042
2043 // 3. Create a second checkpoint with the modified state
2044 let checkpoint_2 = repository
2045 .update(cx, |repository, _| repository.checkpoint())
2046 .await
2047 .unwrap()
2048 .unwrap();
2049
2050 // 4. compare_checkpoints: same checkpoint with itself => equal
2051 let equal = repository
2052 .update(cx, |repository, _| {
2053 repository.compare_checkpoints(checkpoint_1.clone(), checkpoint_1.clone())
2054 })
2055 .await
2056 .unwrap()
2057 .unwrap();
2058 assert!(equal, "a checkpoint compared with itself should be equal");
2059
2060 // 5. compare_checkpoints: different states => not equal
2061 let equal = repository
2062 .update(cx, |repository, _| {
2063 repository.compare_checkpoints(checkpoint_1.clone(), checkpoint_2.clone())
2064 })
2065 .await
2066 .unwrap()
2067 .unwrap();
2068 assert!(
2069 !equal,
2070 "checkpoints of different states should not be equal"
2071 );
2072
2073 // 6. diff_checkpoints: same checkpoint => empty diff
2074 let diff = repository
2075 .update(cx, |repository, _| {
2076 repository.diff_checkpoints(checkpoint_1.clone(), checkpoint_1.clone())
2077 })
2078 .await
2079 .unwrap()
2080 .unwrap();
2081 assert!(
2082 diff.is_empty(),
2083 "diff of identical checkpoints should be empty"
2084 );
2085
2086 // 7. diff_checkpoints: different checkpoints => non-empty diff mentioning the changed file
2087 let diff = repository
2088 .update(cx, |repository, _| {
2089 repository.diff_checkpoints(checkpoint_1.clone(), checkpoint_2.clone())
2090 })
2091 .await
2092 .unwrap()
2093 .unwrap();
2094 assert!(
2095 !diff.is_empty(),
2096 "diff of different checkpoints should be non-empty"
2097 );
2098 assert!(
2099 diff.contains("file.txt"),
2100 "diff should mention the changed file"
2101 );
2102 assert!(
2103 diff.contains("original content"),
2104 "diff should contain removed content"
2105 );
2106 assert!(
2107 diff.contains("modified content"),
2108 "diff should contain added content"
2109 );
2110
2111 // 8. restore_checkpoint: restore to original state
2112 repository
2113 .update(cx, |repository, _| {
2114 repository.restore_checkpoint(checkpoint_1.clone())
2115 })
2116 .await
2117 .unwrap()
2118 .unwrap();
2119 cx.run_until_parked();
2120
2121 // 9. Create a checkpoint after restore
2122 let checkpoint_3 = repository
2123 .update(cx, |repository, _| repository.checkpoint())
2124 .await
2125 .unwrap()
2126 .unwrap();
2127
2128 // 10. compare_checkpoints: restored state matches original
2129 let equal = repository
2130 .update(cx, |repository, _| {
2131 repository.compare_checkpoints(checkpoint_1.clone(), checkpoint_3.clone())
2132 })
2133 .await
2134 .unwrap()
2135 .unwrap();
2136 assert!(equal, "restored state should match original checkpoint");
2137
2138 // 11. diff_checkpoints: restored state vs original => empty diff
2139 let diff = repository
2140 .update(cx, |repository, _| {
2141 repository.diff_checkpoints(checkpoint_1.clone(), checkpoint_3.clone())
2142 })
2143 .await
2144 .unwrap()
2145 .unwrap();
2146 assert!(diff.is_empty(), "diff after restore should be empty");
2147}
2148
2149#[gpui::test]
2150async fn test_remote_agent_fs_tool_calls(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
2151 let fs = FakeFs::new(server_cx.executor());
2152 fs.insert_tree(
2153 path!("/project"),
2154 json!({
2155 "a.txt": "A",
2156 "b.txt": "B",
2157 }),
2158 )
2159 .await;
2160
2161 let (project, _headless_project) = init_test(&fs, cx, server_cx).await;
2162 project
2163 .update(cx, |project, cx| {
2164 project.find_or_create_worktree(path!("/project"), true, cx)
2165 })
2166 .await
2167 .unwrap();
2168
2169 let action_log = cx.new(|_| action_log::ActionLog::new(project.clone()));
2170
2171 let input = ReadFileToolInput {
2172 path: "project/b.txt".into(),
2173 start_line: None,
2174 end_line: None,
2175 };
2176 let read_tool = Arc::new(ReadFileTool::new(project, action_log, true));
2177 let (event_stream, _) = ToolCallEventStream::test();
2178
2179 let exists_result = cx.update(|cx| {
2180 read_tool
2181 .clone()
2182 .run(ToolInput::resolved(input), event_stream.clone(), cx)
2183 });
2184 let output = exists_result.await.unwrap();
2185 assert_eq!(output, LanguageModelToolResultContent::Text("B".into()));
2186
2187 let input = ReadFileToolInput {
2188 path: "project/c.txt".into(),
2189 start_line: None,
2190 end_line: None,
2191 };
2192 let does_not_exist_result =
2193 cx.update(|cx| read_tool.run(ToolInput::resolved(input), event_stream, cx));
2194 does_not_exist_result.await.unwrap_err();
2195}
2196
2197#[gpui::test]
2198async fn test_remote_external_agent_server(
2199 cx: &mut TestAppContext,
2200 server_cx: &mut TestAppContext,
2201) {
2202 let fs = FakeFs::new(server_cx.executor());
2203 fs.insert_tree(path!("/project"), json!({})).await;
2204
2205 let (project, _headless_project) = init_test(&fs, cx, server_cx).await;
2206 project
2207 .update(cx, |project, cx| {
2208 project.find_or_create_worktree(path!("/project"), true, cx)
2209 })
2210 .await
2211 .unwrap();
2212 let names = project.update(cx, |project, cx| {
2213 project
2214 .agent_server_store()
2215 .read(cx)
2216 .external_agents()
2217 .map(|name| name.to_string())
2218 .collect::<Vec<_>>()
2219 });
2220 pretty_assertions::assert_eq!(names, Vec::<String>::new());
2221 server_cx.update_global::<SettingsStore, _>(|settings_store, cx| {
2222 settings_store
2223 .set_server_settings(
2224 &json!({
2225 "agent_servers": {
2226 "foo": {
2227 "type": "custom",
2228 "command": "foo-cli",
2229 "args": ["--flag"],
2230 "env": {
2231 "VAR": "val"
2232 }
2233 }
2234 }
2235 })
2236 .to_string(),
2237 cx,
2238 )
2239 .unwrap();
2240 });
2241 server_cx.run_until_parked();
2242 cx.run_until_parked();
2243 let names = project.update(cx, |project, cx| {
2244 project
2245 .agent_server_store()
2246 .read(cx)
2247 .external_agents()
2248 .map(|name| name.to_string())
2249 .collect::<Vec<_>>()
2250 });
2251 pretty_assertions::assert_eq!(names, ["foo"]);
2252 let command = project
2253 .update(cx, |project, cx| {
2254 project.agent_server_store().update(cx, |store, cx| {
2255 store
2256 .get_external_agent(&"foo".into())
2257 .unwrap()
2258 .get_command(
2259 HashMap::from_iter([("OTHER_VAR".into(), "other-val".into())]),
2260 None,
2261 &mut cx.to_async(),
2262 )
2263 })
2264 })
2265 .await
2266 .unwrap();
2267 assert_eq!(
2268 command,
2269 AgentServerCommand {
2270 path: "mock".into(),
2271 args: vec!["foo-cli".into(), "--flag".into()],
2272 env: Some(HashMap::from_iter([
2273 ("NO_BROWSER".into(), "1".into()),
2274 ("VAR".into(), "val".into()),
2275 ("OTHER_VAR".into(), "other-val".into())
2276 ]))
2277 }
2278 );
2279}
2280
2281pub async fn init_test(
2282 server_fs: &Arc<FakeFs>,
2283 cx: &mut TestAppContext,
2284 server_cx: &mut TestAppContext,
2285) -> (Entity<Project>, Entity<HeadlessProject>) {
2286 let server_fs = server_fs.clone();
2287 cx.update(|cx| {
2288 release_channel::init(semver::Version::new(0, 0, 0), cx);
2289 });
2290 server_cx.update(|cx| {
2291 release_channel::init(semver::Version::new(0, 0, 0), cx);
2292 });
2293 init_logger();
2294
2295 let (opts, ssh_server_client, _) = RemoteClient::fake_server(cx, server_cx);
2296 let http_client = Arc::new(BlockedHttpClient);
2297 let node_runtime = NodeRuntime::unavailable();
2298 let languages = Arc::new(LanguageRegistry::new(cx.executor()));
2299 let proxy = Arc::new(ExtensionHostProxy::new());
2300 server_cx.update(HeadlessProject::init);
2301 let headless = server_cx.new(|cx| {
2302 HeadlessProject::new(
2303 crate::HeadlessAppState {
2304 session: ssh_server_client,
2305 fs: server_fs.clone(),
2306 http_client,
2307 node_runtime,
2308 languages,
2309 extension_host_proxy: proxy,
2310 startup_time: std::time::Instant::now(),
2311 },
2312 false,
2313 cx,
2314 )
2315 });
2316
2317 let ssh = RemoteClient::connect_mock(opts, cx).await;
2318 let project = build_project(ssh, cx);
2319 project
2320 .update(cx, {
2321 let headless = headless.clone();
2322 |_, cx| cx.on_release(|_, _| drop(headless))
2323 })
2324 .detach();
2325 (project, headless)
2326}
2327
2328fn init_logger() {
2329 zlog::init_test();
2330}
2331
2332fn build_project(ssh: Entity<RemoteClient>, cx: &mut TestAppContext) -> Entity<Project> {
2333 cx.update(|cx| {
2334 if !cx.has_global::<SettingsStore>() {
2335 let settings_store = SettingsStore::test(cx);
2336 cx.set_global(settings_store);
2337 }
2338 });
2339
2340 let client = cx.update(|cx| {
2341 Client::new(
2342 Arc::new(FakeSystemClock::new()),
2343 FakeHttpClient::with_404_response(),
2344 cx,
2345 )
2346 });
2347
2348 let node = NodeRuntime::unavailable();
2349 let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
2350 let languages = Arc::new(LanguageRegistry::test(cx.executor()));
2351 let fs = FakeFs::new(cx.executor());
2352
2353 cx.update(|cx| {
2354 Project::init(&client, cx);
2355 });
2356
2357 cx.update(|cx| Project::remote(ssh, client, node, user_store, languages, fs, false, cx))
2358}