remote_editing_tests.rs

  1use crate::headless_project::HeadlessProject;
  2use client::{Client, UserStore};
  3use clock::FakeSystemClock;
  4use fs::{FakeFs, Fs};
  5use gpui::{Context, Model, TestAppContext};
  6use http_client::{BlockedHttpClient, FakeHttpClient};
  7use language::{
  8    language_settings::{language_settings, AllLanguageSettings},
  9    Buffer, FakeLspAdapter, LanguageConfig, LanguageMatcher, LanguageRegistry, LanguageServerName,
 10    LineEnding,
 11};
 12use lsp::{CompletionContext, CompletionResponse, CompletionTriggerKind};
 13use node_runtime::NodeRuntime;
 14use project::{
 15    search::{SearchQuery, SearchResult},
 16    Project, ProjectPath,
 17};
 18use remote::SshRemoteClient;
 19use serde_json::json;
 20use settings::{initial_server_settings_content, Settings, SettingsLocation, SettingsStore};
 21use smol::stream::StreamExt;
 22use std::{
 23    path::{Path, PathBuf},
 24    sync::Arc,
 25};
 26
 27#[gpui::test]
 28async fn test_basic_remote_editing(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
 29    let (project, _headless, fs) = init_test(cx, server_cx).await;
 30    let (worktree, _) = project
 31        .update(cx, |project, cx| {
 32            project.find_or_create_worktree("/code/project1", true, cx)
 33        })
 34        .await
 35        .unwrap();
 36
 37    // The client sees the worktree's contents.
 38    cx.executor().run_until_parked();
 39    let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
 40    worktree.update(cx, |worktree, _cx| {
 41        assert_eq!(
 42            worktree.paths().map(Arc::as_ref).collect::<Vec<_>>(),
 43            vec![
 44                Path::new("README.md"),
 45                Path::new("src"),
 46                Path::new("src/lib.rs"),
 47            ]
 48        );
 49    });
 50
 51    // The user opens a buffer in the remote worktree. The buffer's
 52    // contents are loaded from the remote filesystem.
 53    let buffer = project
 54        .update(cx, |project, cx| {
 55            project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
 56        })
 57        .await
 58        .unwrap();
 59
 60    buffer.update(cx, |buffer, cx| {
 61        assert_eq!(buffer.text(), "fn one() -> usize { 1 }");
 62        assert_eq!(
 63            buffer.diff_base().unwrap().to_string(),
 64            "fn one() -> usize { 0 }"
 65        );
 66        let ix = buffer.text().find('1').unwrap();
 67        buffer.edit([(ix..ix + 1, "100")], None, cx);
 68    });
 69
 70    // The user saves the buffer. The new contents are written to the
 71    // remote filesystem.
 72    project
 73        .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
 74        .await
 75        .unwrap();
 76    assert_eq!(
 77        fs.load("/code/project1/src/lib.rs".as_ref()).await.unwrap(),
 78        "fn one() -> usize { 100 }"
 79    );
 80
 81    // A new file is created in the remote filesystem. The user
 82    // sees the new file.
 83    fs.save(
 84        "/code/project1/src/main.rs".as_ref(),
 85        &"fn main() {}".into(),
 86        Default::default(),
 87    )
 88    .await
 89    .unwrap();
 90    cx.executor().run_until_parked();
 91    worktree.update(cx, |worktree, _cx| {
 92        assert_eq!(
 93            worktree.paths().map(Arc::as_ref).collect::<Vec<_>>(),
 94            vec![
 95                Path::new("README.md"),
 96                Path::new("src"),
 97                Path::new("src/lib.rs"),
 98                Path::new("src/main.rs"),
 99            ]
100        );
101    });
102
103    // A file that is currently open in a buffer is renamed.
104    fs.rename(
105        "/code/project1/src/lib.rs".as_ref(),
106        "/code/project1/src/lib2.rs".as_ref(),
107        Default::default(),
108    )
109    .await
110    .unwrap();
111    cx.executor().run_until_parked();
112    buffer.update(cx, |buffer, _| {
113        assert_eq!(&**buffer.file().unwrap().path(), Path::new("src/lib2.rs"));
114    });
115
116    fs.set_index_for_repo(
117        Path::new("/code/project1/.git"),
118        &[(Path::new("src/lib2.rs"), "fn one() -> usize { 100 }".into())],
119    );
120    cx.executor().run_until_parked();
121    buffer.update(cx, |buffer, _| {
122        assert_eq!(
123            buffer.diff_base().unwrap().to_string(),
124            "fn one() -> usize { 100 }"
125        );
126    });
127}
128
129#[gpui::test]
130async fn test_remote_project_search(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
131    let (project, headless, _) = init_test(cx, server_cx).await;
132
133    project
134        .update(cx, |project, cx| {
135            project.find_or_create_worktree("/code/project1", true, cx)
136        })
137        .await
138        .unwrap();
139
140    cx.run_until_parked();
141
142    async fn do_search(project: &Model<Project>, mut cx: TestAppContext) -> Model<Buffer> {
143        let mut receiver = project.update(&mut cx, |project, cx| {
144            project.search(
145                SearchQuery::text(
146                    "project",
147                    false,
148                    true,
149                    false,
150                    Default::default(),
151                    Default::default(),
152                    None,
153                )
154                .unwrap(),
155                cx,
156            )
157        });
158
159        let first_response = receiver.next().await.unwrap();
160        let SearchResult::Buffer { buffer, .. } = first_response else {
161            panic!("incorrect result");
162        };
163        buffer.update(&mut cx, |buffer, cx| {
164            assert_eq!(
165                buffer.file().unwrap().full_path(cx).to_string_lossy(),
166                "project1/README.md"
167            )
168        });
169
170        assert!(receiver.next().await.is_none());
171        buffer
172    }
173
174    let buffer = do_search(&project, cx.clone()).await;
175
176    // test that the headless server is tracking which buffers we have open correctly.
177    cx.run_until_parked();
178    headless.update(server_cx, |headless, cx| {
179        assert!(!headless.buffer_store.read(cx).shared_buffers().is_empty())
180    });
181    do_search(&project, cx.clone()).await;
182
183    cx.update(|_| {
184        drop(buffer);
185    });
186    cx.run_until_parked();
187    headless.update(server_cx, |headless, cx| {
188        assert!(headless.buffer_store.read(cx).shared_buffers().is_empty())
189    });
190
191    do_search(&project, cx.clone()).await;
192}
193
194#[gpui::test]
195async fn test_remote_settings(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
196    let (project, headless, fs) = init_test(cx, server_cx).await;
197
198    cx.update_global(|settings_store: &mut SettingsStore, cx| {
199        settings_store.set_user_settings(
200            r#"{"languages":{"Rust":{"language_servers":["from-local-settings"]}}}"#,
201            cx,
202        )
203    })
204    .unwrap();
205
206    cx.run_until_parked();
207
208    server_cx.read(|cx| {
209        assert_eq!(
210            AllLanguageSettings::get_global(cx)
211                .language(None, Some(&"Rust".into()), cx)
212                .language_servers,
213            ["..."] // local settings are ignored
214        )
215    });
216
217    server_cx
218        .update_global(|settings_store: &mut SettingsStore, cx| {
219            settings_store.set_server_settings(
220                r#"{"languages":{"Rust":{"language_servers":["from-server-settings"]}}}"#,
221                cx,
222            )
223        })
224        .unwrap();
225
226    cx.run_until_parked();
227
228    server_cx.read(|cx| {
229        assert_eq!(
230            AllLanguageSettings::get_global(cx)
231                .language(None, Some(&"Rust".into()), cx)
232                .language_servers,
233            ["from-server-settings".to_string()]
234        )
235    });
236
237    fs.insert_tree(
238        "/code/project1/.zed",
239        json!({
240            "settings.json": r#"
241                  {
242                    "languages": {"Rust":{"language_servers":["override-rust-analyzer"]}},
243                    "lsp": {
244                      "override-rust-analyzer": {
245                        "binary": {
246                          "path": "~/.cargo/bin/rust-analyzer"
247                        }
248                      }
249                    }
250                  }"#
251        }),
252    )
253    .await;
254
255    let worktree_id = project
256        .update(cx, |project, cx| {
257            project.find_or_create_worktree("/code/project1", true, cx)
258        })
259        .await
260        .unwrap()
261        .0
262        .read_with(cx, |worktree, _| worktree.id());
263
264    let buffer = project
265        .update(cx, |project, cx| {
266            project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
267        })
268        .await
269        .unwrap();
270    cx.run_until_parked();
271
272    server_cx.read(|cx| {
273        let worktree_id = headless
274            .read(cx)
275            .worktree_store
276            .read(cx)
277            .worktrees()
278            .next()
279            .unwrap()
280            .read(cx)
281            .id();
282        assert_eq!(
283            AllLanguageSettings::get(
284                Some(SettingsLocation {
285                    worktree_id,
286                    path: Path::new("src/lib.rs")
287                }),
288                cx
289            )
290            .language(None, Some(&"Rust".into()), cx)
291            .language_servers,
292            ["override-rust-analyzer".to_string()]
293        )
294    });
295
296    cx.read(|cx| {
297        let file = buffer.read(cx).file();
298        assert_eq!(
299            language_settings(Some("Rust".into()), file, cx).language_servers,
300            ["override-rust-analyzer".to_string()]
301        )
302    });
303}
304
305#[gpui::test]
306async fn test_remote_lsp(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
307    let (project, headless, fs) = init_test(cx, server_cx).await;
308
309    fs.insert_tree(
310        "/code/project1/.zed",
311        json!({
312            "settings.json": r#"
313          {
314            "languages": {"Rust":{"language_servers":["rust-analyzer"]}},
315            "lsp": {
316              "rust-analyzer": {
317                "binary": {
318                  "path": "~/.cargo/bin/rust-analyzer"
319                }
320              }
321            }
322          }"#
323        }),
324    )
325    .await;
326
327    cx.update_model(&project, |project, _| {
328        project.languages().register_test_language(LanguageConfig {
329            name: "Rust".into(),
330            matcher: LanguageMatcher {
331                path_suffixes: vec!["rs".into()],
332                ..Default::default()
333            },
334            ..Default::default()
335        });
336        project.languages().register_fake_lsp_adapter(
337            "Rust",
338            FakeLspAdapter {
339                name: "rust-analyzer",
340                ..Default::default()
341            },
342        )
343    });
344
345    let mut fake_lsp = server_cx.update(|cx| {
346        headless.read(cx).languages.register_fake_language_server(
347            LanguageServerName("rust-analyzer".into()),
348            Default::default(),
349            None,
350        )
351    });
352
353    cx.run_until_parked();
354
355    let worktree_id = project
356        .update(cx, |project, cx| {
357            project.find_or_create_worktree("/code/project1", true, cx)
358        })
359        .await
360        .unwrap()
361        .0
362        .read_with(cx, |worktree, _| worktree.id());
363
364    // Wait for the settings to synchronize
365    cx.run_until_parked();
366
367    let buffer = project
368        .update(cx, |project, cx| {
369            project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
370        })
371        .await
372        .unwrap();
373    cx.run_until_parked();
374
375    let fake_lsp = fake_lsp.next().await.unwrap();
376
377    cx.read(|cx| {
378        let file = buffer.read(cx).file();
379        assert_eq!(
380            language_settings(Some("Rust".into()), file, cx).language_servers,
381            ["rust-analyzer".to_string()]
382        )
383    });
384
385    let buffer_id = cx.read(|cx| {
386        let buffer = buffer.read(cx);
387        assert_eq!(buffer.language().unwrap().name(), "Rust".into());
388        buffer.remote_id()
389    });
390
391    server_cx.read(|cx| {
392        let buffer = headless
393            .read(cx)
394            .buffer_store
395            .read(cx)
396            .get(buffer_id)
397            .unwrap();
398
399        assert_eq!(buffer.read(cx).language().unwrap().name(), "Rust".into());
400    });
401
402    server_cx.read(|cx| {
403        let lsp_store = headless.read(cx).lsp_store.read(cx);
404        assert_eq!(lsp_store.as_local().unwrap().language_servers.len(), 1);
405    });
406
407    fake_lsp.handle_request::<lsp::request::Completion, _, _>(|_, _| async move {
408        Ok(Some(CompletionResponse::Array(vec![lsp::CompletionItem {
409            label: "boop".to_string(),
410            ..Default::default()
411        }])))
412    });
413
414    let result = project
415        .update(cx, |project, cx| {
416            project.completions(
417                &buffer,
418                0,
419                CompletionContext {
420                    trigger_kind: CompletionTriggerKind::INVOKED,
421                    trigger_character: None,
422                },
423                cx,
424            )
425        })
426        .await
427        .unwrap();
428
429    assert_eq!(
430        result.into_iter().map(|c| c.label.text).collect::<Vec<_>>(),
431        vec!["boop".to_string()]
432    );
433
434    fake_lsp.handle_request::<lsp::request::Rename, _, _>(|_, _| async move {
435        Ok(Some(lsp::WorkspaceEdit {
436            changes: Some(
437                [(
438                    lsp::Url::from_file_path("/code/project1/src/lib.rs").unwrap(),
439                    vec![lsp::TextEdit::new(
440                        lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 6)),
441                        "two".to_string(),
442                    )],
443                )]
444                .into_iter()
445                .collect(),
446            ),
447            ..Default::default()
448        }))
449    });
450
451    project
452        .update(cx, |project, cx| {
453            project.perform_rename(buffer.clone(), 3, "two".to_string(), cx)
454        })
455        .await
456        .unwrap();
457
458    cx.run_until_parked();
459    buffer.update(cx, |buffer, _| {
460        assert_eq!(buffer.text(), "fn two() -> usize { 1 }")
461    })
462}
463
464#[gpui::test]
465async fn test_remote_reload(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
466    let (project, _headless, fs) = init_test(cx, server_cx).await;
467    let (worktree, _) = project
468        .update(cx, |project, cx| {
469            project.find_or_create_worktree("/code/project1", true, cx)
470        })
471        .await
472        .unwrap();
473
474    let worktree_id = cx.update(|cx| worktree.read(cx).id());
475
476    let buffer = project
477        .update(cx, |project, cx| {
478            project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
479        })
480        .await
481        .unwrap();
482
483    fs.save(
484        &PathBuf::from("/code/project1/src/lib.rs"),
485        &("bangles".to_string().into()),
486        LineEnding::Unix,
487    )
488    .await
489    .unwrap();
490
491    cx.run_until_parked();
492
493    buffer.update(cx, |buffer, cx| {
494        assert_eq!(buffer.text(), "bangles");
495        buffer.edit([(0..0, "a")], None, cx);
496    });
497
498    fs.save(
499        &PathBuf::from("/code/project1/src/lib.rs"),
500        &("bloop".to_string().into()),
501        LineEnding::Unix,
502    )
503    .await
504    .unwrap();
505
506    cx.run_until_parked();
507    cx.update(|cx| {
508        assert!(buffer.read(cx).has_conflict());
509    });
510
511    project
512        .update(cx, |project, cx| {
513            project.reload_buffers([buffer.clone()].into_iter().collect(), false, cx)
514        })
515        .await
516        .unwrap();
517    cx.run_until_parked();
518
519    cx.update(|cx| {
520        assert!(!buffer.read(cx).has_conflict());
521    });
522}
523
524#[gpui::test]
525async fn test_remote_resolve_file_path(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
526    let (project, _headless, _fs) = init_test(cx, server_cx).await;
527    let (worktree, _) = project
528        .update(cx, |project, cx| {
529            project.find_or_create_worktree("/code/project1", true, cx)
530        })
531        .await
532        .unwrap();
533
534    let worktree_id = cx.update(|cx| worktree.read(cx).id());
535
536    let buffer = project
537        .update(cx, |project, cx| {
538            project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
539        })
540        .await
541        .unwrap();
542
543    let path = project
544        .update(cx, |project, cx| {
545            project.resolve_existing_file_path("/code/project1/README.md", &buffer, cx)
546        })
547        .await
548        .unwrap();
549    assert_eq!(
550        path.abs_path().unwrap().to_string_lossy(),
551        "/code/project1/README.md"
552    );
553
554    let path = project
555        .update(cx, |project, cx| {
556            project.resolve_existing_file_path("../README.md", &buffer, cx)
557        })
558        .await
559        .unwrap();
560
561    assert_eq!(
562        path.project_path().unwrap().clone(),
563        ProjectPath::from((worktree_id, "README.md"))
564    );
565}
566
567#[gpui::test(iterations = 10)]
568async fn test_canceling_buffer_opening(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
569    let (project, _headless, _fs) = init_test(cx, server_cx).await;
570    let (worktree, _) = project
571        .update(cx, |project, cx| {
572            project.find_or_create_worktree("/code/project1", true, cx)
573        })
574        .await
575        .unwrap();
576    let worktree_id = worktree.read_with(cx, |tree, _| tree.id());
577
578    // Open a buffer on the client but cancel after a random amount of time.
579    let buffer = project.update(cx, |p, cx| p.open_buffer((worktree_id, "src/lib.rs"), cx));
580    cx.executor().simulate_random_delay().await;
581    drop(buffer);
582
583    // Try opening the same buffer again as the client, and ensure we can
584    // still do it despite the cancellation above.
585    let buffer = project
586        .update(cx, |p, cx| p.open_buffer((worktree_id, "src/lib.rs"), cx))
587        .await
588        .unwrap();
589
590    buffer.read_with(cx, |buf, _| {
591        assert_eq!(buf.text(), "fn one() -> usize { 1 }")
592    });
593}
594
595#[gpui::test]
596async fn test_adding_then_removing_then_adding_worktrees(
597    cx: &mut TestAppContext,
598    server_cx: &mut TestAppContext,
599) {
600    let (project, _headless, _fs) = init_test(cx, server_cx).await;
601    let (_worktree, _) = project
602        .update(cx, |project, cx| {
603            project.find_or_create_worktree("/code/project1", true, cx)
604        })
605        .await
606        .unwrap();
607
608    let (worktree_2, _) = project
609        .update(cx, |project, cx| {
610            project.find_or_create_worktree("/code/project2", true, cx)
611        })
612        .await
613        .unwrap();
614    let worktree_id_2 = worktree_2.read_with(cx, |tree, _| tree.id());
615
616    project.update(cx, |project, cx| project.remove_worktree(worktree_id_2, cx));
617
618    let (worktree_2, _) = project
619        .update(cx, |project, cx| {
620            project.find_or_create_worktree("/code/project2", true, cx)
621        })
622        .await
623        .unwrap();
624
625    cx.run_until_parked();
626    worktree_2.update(cx, |worktree, _cx| {
627        assert!(worktree.is_visible());
628        let entries = worktree.entries(true, 0).collect::<Vec<_>>();
629        assert_eq!(entries.len(), 2);
630        assert_eq!(
631            entries[1].path.to_string_lossy().to_string(),
632            "README.md".to_string()
633        )
634    })
635}
636
637#[gpui::test]
638async fn test_open_server_settings(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
639    let (project, _headless, _fs) = init_test(cx, server_cx).await;
640    let buffer = project.update(cx, |project, cx| project.open_server_settings(cx));
641    cx.executor().run_until_parked();
642    let buffer = buffer.await.unwrap();
643
644    cx.update(|cx| {
645        assert_eq!(
646            buffer.read(cx).text(),
647            initial_server_settings_content().to_string()
648        )
649    })
650}
651
652#[gpui::test(iterations = 20)]
653async fn test_reconnect(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
654    let (project, _headless, fs) = init_test(cx, server_cx).await;
655
656    let (worktree, _) = project
657        .update(cx, |project, cx| {
658            project.find_or_create_worktree("/code/project1", true, cx)
659        })
660        .await
661        .unwrap();
662
663    let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
664    let buffer = project
665        .update(cx, |project, cx| {
666            project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
667        })
668        .await
669        .unwrap();
670
671    buffer.update(cx, |buffer, cx| {
672        assert_eq!(buffer.text(), "fn one() -> usize { 1 }");
673        let ix = buffer.text().find('1').unwrap();
674        buffer.edit([(ix..ix + 1, "100")], None, cx);
675    });
676
677    let client = cx.read(|cx| project.read(cx).ssh_client().unwrap());
678    client
679        .update(cx, |client, cx| client.simulate_disconnect(cx))
680        .detach();
681
682    project
683        .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
684        .await
685        .unwrap();
686
687    assert_eq!(
688        fs.load("/code/project1/src/lib.rs".as_ref()).await.unwrap(),
689        "fn one() -> usize { 100 }"
690    );
691}
692
693fn init_logger() {
694    if std::env::var("RUST_LOG").is_ok() {
695        env_logger::try_init().ok();
696    }
697}
698
699async fn init_test(
700    cx: &mut TestAppContext,
701    server_cx: &mut TestAppContext,
702) -> (Model<Project>, Model<HeadlessProject>, Arc<FakeFs>) {
703    init_logger();
704
705    let (forwarder, ssh_server_client) = SshRemoteClient::fake_server(cx, server_cx);
706    let fs = FakeFs::new(server_cx.executor());
707    fs.insert_tree(
708        "/code",
709        json!({
710            "project1": {
711                ".git": {},
712                "README.md": "# project 1",
713                "src": {
714                    "lib.rs": "fn one() -> usize { 1 }"
715                }
716            },
717            "project2": {
718                "README.md": "# project 2",
719            },
720        }),
721    )
722    .await;
723    fs.set_index_for_repo(
724        Path::new("/code/project1/.git"),
725        &[(Path::new("src/lib.rs"), "fn one() -> usize { 0 }".into())],
726    );
727
728    server_cx.update(HeadlessProject::init);
729    let http_client = Arc::new(BlockedHttpClient);
730    let node_runtime = NodeRuntime::unavailable();
731    let languages = Arc::new(LanguageRegistry::new(cx.executor()));
732    let headless = server_cx.new_model(|cx| {
733        client::init_settings(cx);
734
735        HeadlessProject::new(
736            crate::HeadlessAppState {
737                session: ssh_server_client,
738                fs: fs.clone(),
739                http_client,
740                node_runtime,
741                languages,
742            },
743            cx,
744        )
745    });
746
747    let ssh = SshRemoteClient::fake_client(forwarder, cx).await;
748    let project = build_project(ssh, cx);
749    project
750        .update(cx, {
751            let headless = headless.clone();
752            |_, cx| cx.on_release(|_, _| drop(headless))
753        })
754        .detach();
755    (project, headless, fs)
756}
757
758fn build_project(ssh: Model<SshRemoteClient>, cx: &mut TestAppContext) -> Model<Project> {
759    cx.update(|cx| {
760        let settings_store = SettingsStore::test(cx);
761        cx.set_global(settings_store);
762    });
763
764    let client = cx.update(|cx| {
765        Client::new(
766            Arc::new(FakeSystemClock::default()),
767            FakeHttpClient::with_404_response(),
768            cx,
769        )
770    });
771
772    let node = NodeRuntime::unavailable();
773    let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
774    let languages = Arc::new(LanguageRegistry::test(cx.executor()));
775    let fs = FakeFs::new(cx.executor());
776    cx.update(|cx| {
777        Project::init(&client, cx);
778        language::init(cx);
779    });
780
781    cx.update(|cx| Project::ssh(ssh, client, node, user_store, languages, fs, cx))
782}