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::FakeHttpClient;
  7use language::{
  8    language_settings::{all_language_settings, AllLanguageSettings},
  9    Buffer, FakeLspAdapter, LanguageConfig, LanguageMatcher, LanguageRegistry,
 10};
 11use node_runtime::FakeNodeRuntime;
 12use project::{
 13    search::{SearchQuery, SearchResult},
 14    Project,
 15};
 16use remote::SshSession;
 17use serde_json::json;
 18use settings::{Settings, SettingsLocation, SettingsStore};
 19use smol::stream::StreamExt;
 20use std::{path::Path, sync::Arc};
 21
 22#[gpui::test]
 23async fn test_basic_remote_editing(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
 24    let (project, _headless, fs) = init_test(cx, server_cx).await;
 25    let (worktree, _) = project
 26        .update(cx, |project, cx| {
 27            project.find_or_create_worktree("/code/project1", true, cx)
 28        })
 29        .await
 30        .unwrap();
 31
 32    // The client sees the worktree's contents.
 33    cx.executor().run_until_parked();
 34    let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
 35    worktree.update(cx, |worktree, _cx| {
 36        assert_eq!(
 37            worktree.paths().map(Arc::as_ref).collect::<Vec<_>>(),
 38            vec![
 39                Path::new("README.md"),
 40                Path::new("src"),
 41                Path::new("src/lib.rs"),
 42            ]
 43        );
 44    });
 45
 46    // The user opens a buffer in the remote worktree. The buffer's
 47    // contents are loaded from the remote filesystem.
 48    let buffer = project
 49        .update(cx, |project, cx| {
 50            project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
 51        })
 52        .await
 53        .unwrap();
 54    buffer.update(cx, |buffer, cx| {
 55        assert_eq!(buffer.text(), "fn one() -> usize { 1 }");
 56        assert_eq!(
 57            buffer.diff_base().unwrap().to_string(),
 58            "fn one() -> usize { 0 }"
 59        );
 60        let ix = buffer.text().find('1').unwrap();
 61        buffer.edit([(ix..ix + 1, "100")], None, cx);
 62    });
 63
 64    // The user saves the buffer. The new contents are written to the
 65    // remote filesystem.
 66    project
 67        .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
 68        .await
 69        .unwrap();
 70    assert_eq!(
 71        fs.load("/code/project1/src/lib.rs".as_ref()).await.unwrap(),
 72        "fn one() -> usize { 100 }"
 73    );
 74
 75    // A new file is created in the remote filesystem. The user
 76    // sees the new file.
 77    fs.save(
 78        "/code/project1/src/main.rs".as_ref(),
 79        &"fn main() {}".into(),
 80        Default::default(),
 81    )
 82    .await
 83    .unwrap();
 84    cx.executor().run_until_parked();
 85    worktree.update(cx, |worktree, _cx| {
 86        assert_eq!(
 87            worktree.paths().map(Arc::as_ref).collect::<Vec<_>>(),
 88            vec![
 89                Path::new("README.md"),
 90                Path::new("src"),
 91                Path::new("src/lib.rs"),
 92                Path::new("src/main.rs"),
 93            ]
 94        );
 95    });
 96
 97    // A file that is currently open in a buffer is renamed.
 98    fs.rename(
 99        "/code/project1/src/lib.rs".as_ref(),
100        "/code/project1/src/lib2.rs".as_ref(),
101        Default::default(),
102    )
103    .await
104    .unwrap();
105    cx.executor().run_until_parked();
106    buffer.update(cx, |buffer, _| {
107        assert_eq!(&**buffer.file().unwrap().path(), Path::new("src/lib2.rs"));
108    });
109
110    fs.set_index_for_repo(
111        Path::new("/code/project1/.git"),
112        &[(Path::new("src/lib2.rs"), "fn one() -> usize { 100 }".into())],
113    );
114    cx.executor().run_until_parked();
115    buffer.update(cx, |buffer, _| {
116        assert_eq!(
117            buffer.diff_base().unwrap().to_string(),
118            "fn one() -> usize { 100 }"
119        );
120    });
121}
122
123#[gpui::test]
124async fn test_remote_project_search(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
125    let (project, headless, _) = init_test(cx, server_cx).await;
126
127    project
128        .update(cx, |project, cx| {
129            project.find_or_create_worktree("/code/project1", true, cx)
130        })
131        .await
132        .unwrap();
133
134    cx.run_until_parked();
135
136    async fn do_search(project: &Model<Project>, mut cx: TestAppContext) -> Model<Buffer> {
137        let mut receiver = project.update(&mut cx, |project, cx| {
138            project.search(
139                SearchQuery::text(
140                    "project",
141                    false,
142                    true,
143                    false,
144                    Default::default(),
145                    Default::default(),
146                    None,
147                )
148                .unwrap(),
149                cx,
150            )
151        });
152
153        let first_response = receiver.next().await.unwrap();
154        let SearchResult::Buffer { buffer, .. } = first_response else {
155            panic!("incorrect result");
156        };
157        buffer.update(&mut cx, |buffer, cx| {
158            assert_eq!(
159                buffer.file().unwrap().full_path(cx).to_string_lossy(),
160                "project1/README.md"
161            )
162        });
163
164        assert!(receiver.next().await.is_none());
165        buffer
166    }
167
168    let buffer = do_search(&project, cx.clone()).await;
169
170    // test that the headless server is tracking which buffers we have open correctly.
171    cx.run_until_parked();
172    headless.update(server_cx, |headless, cx| {
173        assert!(!headless.buffer_store.read(cx).shared_buffers().is_empty())
174    });
175    do_search(&project, cx.clone()).await;
176
177    cx.update(|_| {
178        drop(buffer);
179    });
180    cx.run_until_parked();
181    headless.update(server_cx, |headless, cx| {
182        assert!(headless.buffer_store.read(cx).shared_buffers().is_empty())
183    });
184
185    do_search(&project, cx.clone()).await;
186}
187
188#[gpui::test]
189async fn test_remote_settings(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
190    let (project, headless, fs) = init_test(cx, server_cx).await;
191
192    cx.update_global(|settings_store: &mut SettingsStore, cx| {
193        settings_store.set_user_settings(
194            r#"{"languages":{"Rust":{"language_servers":["custom-rust-analyzer"]}}}"#,
195            cx,
196        )
197    })
198    .unwrap();
199
200    cx.run_until_parked();
201
202    server_cx.read(|cx| {
203        assert_eq!(
204            AllLanguageSettings::get_global(cx)
205                .language(Some(&"Rust".into()))
206                .language_servers,
207            ["custom-rust-analyzer".into()]
208        )
209    });
210
211    fs.insert_tree(
212        "/code/project1/.zed",
213        json!({
214            "settings.json": r#"
215                  {
216                    "languages": {"Rust":{"language_servers":["override-rust-analyzer"]}},
217                    "lsp": {
218                      "override-rust-analyzer": {
219                        "binary": {
220                          "path": "~/.cargo/bin/rust-analyzer"
221                        }
222                      }
223                    }
224                  }"#
225        }),
226    )
227    .await;
228
229    let worktree_id = project
230        .update(cx, |project, cx| {
231            project.find_or_create_worktree("/code/project1", true, cx)
232        })
233        .await
234        .unwrap()
235        .0
236        .read_with(cx, |worktree, _| worktree.id());
237
238    let buffer = project
239        .update(cx, |project, cx| {
240            project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
241        })
242        .await
243        .unwrap();
244    cx.run_until_parked();
245
246    server_cx.read(|cx| {
247        let worktree_id = headless
248            .read(cx)
249            .worktree_store
250            .read(cx)
251            .worktrees()
252            .next()
253            .unwrap()
254            .read(cx)
255            .id();
256        assert_eq!(
257            AllLanguageSettings::get(
258                Some(SettingsLocation {
259                    worktree_id,
260                    path: Path::new("src/lib.rs")
261                }),
262                cx
263            )
264            .language(Some(&"Rust".into()))
265            .language_servers,
266            ["override-rust-analyzer".into()]
267        )
268    });
269
270    cx.read(|cx| {
271        let file = buffer.read(cx).file();
272        assert_eq!(
273            all_language_settings(file, cx)
274                .language(Some(&"Rust".into()))
275                .language_servers,
276            ["override-rust-analyzer".into()]
277        )
278    });
279}
280
281#[gpui::test]
282async fn test_remote_lsp(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
283    let (project, headless, fs) = init_test(cx, server_cx).await;
284
285    fs.insert_tree(
286        "/code/project1/.zed",
287        json!({
288            "settings.json": r#"
289          {
290            "languages": {"Rust":{"language_servers":["rust-analyzer"]}},
291            "lsp": {
292              "rust-analyzer": {
293                "binary": {
294                  "path": "~/.cargo/bin/rust-analyzer"
295                }
296              }
297            }
298          }"#
299        }),
300    )
301    .await;
302
303    cx.update_model(&project, |project, _| {
304        project.languages().register_test_language(LanguageConfig {
305            name: "Rust".into(),
306            matcher: LanguageMatcher {
307                path_suffixes: vec!["rs".into()],
308                ..Default::default()
309            },
310            ..Default::default()
311        });
312        project.languages().register_fake_lsp_adapter(
313            "Rust",
314            FakeLspAdapter {
315                name: "rust-analyzer",
316                ..Default::default()
317            },
318        )
319    });
320    cx.run_until_parked();
321
322    let worktree_id = project
323        .update(cx, |project, cx| {
324            project.find_or_create_worktree("/code/project1", true, cx)
325        })
326        .await
327        .unwrap()
328        .0
329        .read_with(cx, |worktree, _| worktree.id());
330
331    // Wait for the settings to synchronize
332    cx.run_until_parked();
333
334    let buffer = project
335        .update(cx, |project, cx| {
336            project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
337        })
338        .await
339        .unwrap();
340    cx.run_until_parked();
341
342    cx.read(|cx| {
343        let file = buffer.read(cx).file();
344        assert_eq!(
345            all_language_settings(file, cx)
346                .language(Some(&"Rust".into()))
347                .language_servers,
348            ["rust-analyzer".into()]
349        )
350    });
351
352    let buffer_id = cx.read(|cx| {
353        let buffer = buffer.read(cx);
354        assert_eq!(buffer.language().unwrap().name(), "Rust".into());
355        buffer.remote_id()
356    });
357
358    server_cx.read(|cx| {
359        let buffer = headless
360            .read(cx)
361            .buffer_store
362            .read(cx)
363            .get(buffer_id)
364            .unwrap();
365
366        assert_eq!(buffer.read(cx).language().unwrap().name(), "Rust".into());
367    });
368
369    server_cx.read(|cx| {
370        let lsp_store = headless.read(cx).lsp_store.read(cx);
371        assert_eq!(lsp_store.as_local().unwrap().language_servers.len(), 1);
372    });
373}
374
375fn init_logger() {
376    if std::env::var("RUST_LOG").is_ok() {
377        env_logger::try_init().ok();
378    }
379}
380
381async fn init_test(
382    cx: &mut TestAppContext,
383    server_cx: &mut TestAppContext,
384) -> (Model<Project>, Model<HeadlessProject>, Arc<FakeFs>) {
385    let (client_ssh, server_ssh) = SshSession::fake(cx, server_cx);
386    init_logger();
387
388    let fs = FakeFs::new(server_cx.executor());
389    fs.insert_tree(
390        "/code",
391        json!({
392            "project1": {
393                ".git": {},
394                "README.md": "# project 1",
395                "src": {
396                    "lib.rs": "fn one() -> usize { 1 }"
397                }
398            },
399            "project2": {
400                "README.md": "# project 2",
401            },
402        }),
403    )
404    .await;
405    fs.set_index_for_repo(
406        Path::new("/code/project1/.git"),
407        &[(Path::new("src/lib.rs"), "fn one() -> usize { 0 }".into())],
408    );
409
410    server_cx.update(HeadlessProject::init);
411    let headless = server_cx.new_model(|cx| HeadlessProject::new(server_ssh, fs.clone(), cx));
412    let project = build_project(client_ssh, cx);
413
414    project
415        .update(cx, {
416            let headless = headless.clone();
417            |_, cx| cx.on_release(|_, _| drop(headless))
418        })
419        .detach();
420    (project, headless, fs)
421}
422
423fn build_project(ssh: Arc<SshSession>, cx: &mut TestAppContext) -> Model<Project> {
424    cx.update(|cx| {
425        let settings_store = SettingsStore::test(cx);
426        cx.set_global(settings_store);
427    });
428
429    let client = cx.update(|cx| {
430        Client::new(
431            Arc::new(FakeSystemClock::default()),
432            FakeHttpClient::with_404_response(),
433            cx,
434        )
435    });
436
437    let node = FakeNodeRuntime::new();
438    let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
439    let languages = Arc::new(LanguageRegistry::test(cx.executor()));
440    let fs = FakeFs::new(cx.executor());
441    cx.update(|cx| {
442        Project::init(&client, cx);
443        language::init(cx);
444    });
445
446    cx.update(|cx| Project::ssh(ssh, client, node, user_store, languages, fs, cx))
447}