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::{all_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(Some(&"Rust".into()))
212                .language_servers,
213            ["from-local-settings".to_string()]
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(Some(&"Rust".into()))
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(Some(&"Rust".into()))
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            all_language_settings(file, cx)
300                .language(Some(&"Rust".into()))
301                .language_servers,
302            ["override-rust-analyzer".to_string()]
303        )
304    });
305}
306
307#[gpui::test]
308async fn test_remote_lsp(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
309    let (project, headless, fs) = init_test(cx, server_cx).await;
310
311    fs.insert_tree(
312        "/code/project1/.zed",
313        json!({
314            "settings.json": r#"
315          {
316            "languages": {"Rust":{"language_servers":["rust-analyzer"]}},
317            "lsp": {
318              "rust-analyzer": {
319                "binary": {
320                  "path": "~/.cargo/bin/rust-analyzer"
321                }
322              }
323            }
324          }"#
325        }),
326    )
327    .await;
328
329    cx.update_model(&project, |project, _| {
330        project.languages().register_test_language(LanguageConfig {
331            name: "Rust".into(),
332            matcher: LanguageMatcher {
333                path_suffixes: vec!["rs".into()],
334                ..Default::default()
335            },
336            ..Default::default()
337        });
338        project.languages().register_fake_lsp_adapter(
339            "Rust",
340            FakeLspAdapter {
341                name: "rust-analyzer",
342                ..Default::default()
343            },
344        )
345    });
346
347    let mut fake_lsp = server_cx.update(|cx| {
348        headless.read(cx).languages.register_fake_language_server(
349            LanguageServerName("rust-analyzer".into()),
350            Default::default(),
351            None,
352        )
353    });
354
355    cx.run_until_parked();
356
357    let worktree_id = project
358        .update(cx, |project, cx| {
359            project.find_or_create_worktree("/code/project1", true, cx)
360        })
361        .await
362        .unwrap()
363        .0
364        .read_with(cx, |worktree, _| worktree.id());
365
366    // Wait for the settings to synchronize
367    cx.run_until_parked();
368
369    let buffer = project
370        .update(cx, |project, cx| {
371            project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
372        })
373        .await
374        .unwrap();
375    cx.run_until_parked();
376
377    let fake_lsp = fake_lsp.next().await.unwrap();
378
379    cx.read(|cx| {
380        let file = buffer.read(cx).file();
381        assert_eq!(
382            all_language_settings(file, cx)
383                .language(Some(&"Rust".into()))
384                .language_servers,
385            ["rust-analyzer".to_string()]
386        )
387    });
388
389    let buffer_id = cx.read(|cx| {
390        let buffer = buffer.read(cx);
391        assert_eq!(buffer.language().unwrap().name(), "Rust".into());
392        buffer.remote_id()
393    });
394
395    server_cx.read(|cx| {
396        let buffer = headless
397            .read(cx)
398            .buffer_store
399            .read(cx)
400            .get(buffer_id)
401            .unwrap();
402
403        assert_eq!(buffer.read(cx).language().unwrap().name(), "Rust".into());
404    });
405
406    server_cx.read(|cx| {
407        let lsp_store = headless.read(cx).lsp_store.read(cx);
408        assert_eq!(lsp_store.as_local().unwrap().language_servers.len(), 1);
409    });
410
411    fake_lsp.handle_request::<lsp::request::Completion, _, _>(|_, _| async move {
412        Ok(Some(CompletionResponse::Array(vec![lsp::CompletionItem {
413            label: "boop".to_string(),
414            ..Default::default()
415        }])))
416    });
417
418    let result = project
419        .update(cx, |project, cx| {
420            project.completions(
421                &buffer,
422                0,
423                CompletionContext {
424                    trigger_kind: CompletionTriggerKind::INVOKED,
425                    trigger_character: None,
426                },
427                cx,
428            )
429        })
430        .await
431        .unwrap();
432
433    assert_eq!(
434        result.into_iter().map(|c| c.label.text).collect::<Vec<_>>(),
435        vec!["boop".to_string()]
436    );
437
438    fake_lsp.handle_request::<lsp::request::Rename, _, _>(|_, _| async move {
439        Ok(Some(lsp::WorkspaceEdit {
440            changes: Some(
441                [(
442                    lsp::Url::from_file_path("/code/project1/src/lib.rs").unwrap(),
443                    vec![lsp::TextEdit::new(
444                        lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 6)),
445                        "two".to_string(),
446                    )],
447                )]
448                .into_iter()
449                .collect(),
450            ),
451            ..Default::default()
452        }))
453    });
454
455    project
456        .update(cx, |project, cx| {
457            project.perform_rename(buffer.clone(), 3, "two".to_string(), cx)
458        })
459        .await
460        .unwrap();
461
462    cx.run_until_parked();
463    buffer.update(cx, |buffer, _| {
464        assert_eq!(buffer.text(), "fn two() -> usize { 1 }")
465    })
466}
467
468#[gpui::test]
469async fn test_remote_reload(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
470    let (project, _headless, fs) = init_test(cx, server_cx).await;
471    let (worktree, _) = project
472        .update(cx, |project, cx| {
473            project.find_or_create_worktree("/code/project1", true, cx)
474        })
475        .await
476        .unwrap();
477
478    let worktree_id = cx.update(|cx| worktree.read(cx).id());
479
480    let buffer = project
481        .update(cx, |project, cx| {
482            project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
483        })
484        .await
485        .unwrap();
486    buffer.update(cx, |buffer, cx| {
487        buffer.edit([(0..0, "a")], None, cx);
488    });
489
490    fs.save(
491        &PathBuf::from("/code/project1/src/lib.rs"),
492        &("bloop".to_string().into()),
493        LineEnding::Unix,
494    )
495    .await
496    .unwrap();
497
498    cx.run_until_parked();
499    cx.update(|cx| {
500        assert!(buffer.read(cx).has_conflict());
501    });
502
503    project
504        .update(cx, |project, cx| {
505            project.reload_buffers([buffer.clone()].into_iter().collect(), false, cx)
506        })
507        .await
508        .unwrap();
509    cx.run_until_parked();
510
511    cx.update(|cx| {
512        assert!(!buffer.read(cx).has_conflict());
513    });
514}
515
516#[gpui::test]
517async fn test_remote_resolve_file_path(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
518    let (project, _headless, _fs) = init_test(cx, server_cx).await;
519    let (worktree, _) = project
520        .update(cx, |project, cx| {
521            project.find_or_create_worktree("/code/project1", true, cx)
522        })
523        .await
524        .unwrap();
525
526    let worktree_id = cx.update(|cx| worktree.read(cx).id());
527
528    let buffer = project
529        .update(cx, |project, cx| {
530            project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
531        })
532        .await
533        .unwrap();
534
535    let path = project
536        .update(cx, |project, cx| {
537            project.resolve_existing_file_path("/code/project1/README.md", &buffer, cx)
538        })
539        .await
540        .unwrap();
541    assert_eq!(
542        path.abs_path().unwrap().to_string_lossy(),
543        "/code/project1/README.md"
544    );
545
546    let path = project
547        .update(cx, |project, cx| {
548            project.resolve_existing_file_path("../README.md", &buffer, cx)
549        })
550        .await
551        .unwrap();
552
553    assert_eq!(
554        path.project_path().unwrap().clone(),
555        ProjectPath::from((worktree_id, "README.md"))
556    );
557}
558
559#[gpui::test(iterations = 10)]
560async fn test_canceling_buffer_opening(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
561    let (project, _headless, _fs) = init_test(cx, server_cx).await;
562    let (worktree, _) = project
563        .update(cx, |project, cx| {
564            project.find_or_create_worktree("/code/project1", true, cx)
565        })
566        .await
567        .unwrap();
568    let worktree_id = worktree.read_with(cx, |tree, _| tree.id());
569
570    // Open a buffer on the client but cancel after a random amount of time.
571    let buffer = project.update(cx, |p, cx| p.open_buffer((worktree_id, "src/lib.rs"), cx));
572    cx.executor().simulate_random_delay().await;
573    drop(buffer);
574
575    // Try opening the same buffer again as the client, and ensure we can
576    // still do it despite the cancellation above.
577    let buffer = project
578        .update(cx, |p, cx| p.open_buffer((worktree_id, "src/lib.rs"), cx))
579        .await
580        .unwrap();
581
582    buffer.read_with(cx, |buf, _| {
583        assert_eq!(buf.text(), "fn one() -> usize { 1 }")
584    });
585}
586
587#[gpui::test]
588async fn test_adding_then_removing_then_adding_worktrees(
589    cx: &mut TestAppContext,
590    server_cx: &mut TestAppContext,
591) {
592    let (project, _headless, _fs) = init_test(cx, server_cx).await;
593    let (_worktree, _) = project
594        .update(cx, |project, cx| {
595            project.find_or_create_worktree("/code/project1", true, cx)
596        })
597        .await
598        .unwrap();
599
600    let (worktree_2, _) = project
601        .update(cx, |project, cx| {
602            project.find_or_create_worktree("/code/project2", true, cx)
603        })
604        .await
605        .unwrap();
606    let worktree_id_2 = worktree_2.read_with(cx, |tree, _| tree.id());
607
608    project.update(cx, |project, cx| project.remove_worktree(worktree_id_2, cx));
609
610    let (worktree_2, _) = project
611        .update(cx, |project, cx| {
612            project.find_or_create_worktree("/code/project2", true, cx)
613        })
614        .await
615        .unwrap();
616
617    cx.run_until_parked();
618    worktree_2.update(cx, |worktree, _cx| {
619        assert!(worktree.is_visible());
620        let entries = worktree.entries(true, 0).collect::<Vec<_>>();
621        assert_eq!(entries.len(), 2);
622        assert_eq!(
623            entries[1].path.to_string_lossy().to_string(),
624            "README.md".to_string()
625        )
626    })
627}
628
629#[gpui::test]
630async fn test_open_server_settings(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
631    let (project, _headless, _fs) = init_test(cx, server_cx).await;
632    let buffer = project.update(cx, |project, cx| project.open_server_settings(cx));
633    cx.executor().run_until_parked();
634    let buffer = buffer.await.unwrap();
635
636    cx.update(|cx| {
637        assert_eq!(
638            buffer.read(cx).text(),
639            initial_server_settings_content().to_string()
640        )
641    })
642}
643
644fn init_logger() {
645    if std::env::var("RUST_LOG").is_ok() {
646        env_logger::try_init().ok();
647    }
648}
649
650async fn init_test(
651    cx: &mut TestAppContext,
652    server_cx: &mut TestAppContext,
653) -> (Model<Project>, Model<HeadlessProject>, Arc<FakeFs>) {
654    let (ssh_remote_client, ssh_server_client) = SshRemoteClient::fake(cx, server_cx);
655    init_logger();
656
657    let fs = FakeFs::new(server_cx.executor());
658    fs.insert_tree(
659        "/code",
660        json!({
661            "project1": {
662                ".git": {},
663                "README.md": "# project 1",
664                "src": {
665                    "lib.rs": "fn one() -> usize { 1 }"
666                }
667            },
668            "project2": {
669                "README.md": "# project 2",
670            },
671        }),
672    )
673    .await;
674    fs.set_index_for_repo(
675        Path::new("/code/project1/.git"),
676        &[(Path::new("src/lib.rs"), "fn one() -> usize { 0 }".into())],
677    );
678
679    server_cx.update(HeadlessProject::init);
680    let http_client = Arc::new(BlockedHttpClient);
681    let node_runtime = NodeRuntime::unavailable();
682    let languages = Arc::new(LanguageRegistry::new(cx.executor()));
683    let headless = server_cx.new_model(|cx| {
684        client::init_settings(cx);
685
686        HeadlessProject::new(
687            crate::HeadlessAppState {
688                session: ssh_server_client,
689                fs: fs.clone(),
690                http_client,
691                node_runtime,
692                languages,
693            },
694            cx,
695        )
696    });
697    let project = build_project(ssh_remote_client, cx);
698
699    project
700        .update(cx, {
701            let headless = headless.clone();
702            |_, cx| cx.on_release(|_, _| drop(headless))
703        })
704        .detach();
705    (project, headless, fs)
706}
707
708fn build_project(ssh: Model<SshRemoteClient>, cx: &mut TestAppContext) -> Model<Project> {
709    cx.update(|cx| {
710        let settings_store = SettingsStore::test(cx);
711        cx.set_global(settings_store);
712    });
713
714    let client = cx.update(|cx| {
715        Client::new(
716            Arc::new(FakeSystemClock::default()),
717            FakeHttpClient::with_404_response(),
718            cx,
719        )
720    });
721
722    let node = NodeRuntime::unavailable();
723    let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
724    let languages = Arc::new(LanguageRegistry::test(cx.executor()));
725    let fs = FakeFs::new(cx.executor());
726    cx.update(|cx| {
727        Project::init(&client, cx);
728        language::init(cx);
729    });
730
731    cx.update(|cx| Project::ssh(ssh, client, node, user_store, languages, fs, cx))
732}