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, 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"))
206                .language_servers,
207            ["custom-rust-analyzer".into()]
208        )
209    });
210
211    fs.insert_tree("/code/project1/.zed", json!({
212        "settings.json": r#"{"languages":{"Rust":{"language_servers":["override-rust-analyzer"]}}}"#
213    })).await;
214
215    let worktree_id = project
216        .update(cx, |project, cx| {
217            project.find_or_create_worktree("/code/project1", true, cx)
218        })
219        .await
220        .unwrap()
221        .0
222        .read_with(cx, |worktree, _| worktree.id());
223
224    let buffer = project
225        .update(cx, |project, cx| {
226            project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
227        })
228        .await
229        .unwrap();
230    cx.run_until_parked();
231
232    server_cx.read(|cx| {
233        let worktree_id = headless
234            .read(cx)
235            .worktree_store
236            .read(cx)
237            .worktrees()
238            .next()
239            .unwrap()
240            .read(cx)
241            .id();
242        assert_eq!(
243            AllLanguageSettings::get(
244                Some(SettingsLocation {
245                    worktree_id,
246                    path: Path::new("src/lib.rs")
247                }),
248                cx
249            )
250            .language(Some("Rust"))
251            .language_servers,
252            ["override-rust-analyzer".into()]
253        )
254    });
255
256    cx.read(|cx| {
257        let file = buffer.read(cx).file();
258        assert_eq!(
259            all_language_settings(file, cx)
260                .language(Some("Rust"))
261                .language_servers,
262            ["override-rust-analyzer".into()]
263        )
264    });
265}
266
267fn init_logger() {
268    if std::env::var("RUST_LOG").is_ok() {
269        env_logger::try_init().ok();
270    }
271}
272
273async fn init_test(
274    cx: &mut TestAppContext,
275    server_cx: &mut TestAppContext,
276) -> (Model<Project>, Model<HeadlessProject>, Arc<FakeFs>) {
277    let (client_ssh, server_ssh) = SshSession::fake(cx, server_cx);
278    init_logger();
279
280    let fs = FakeFs::new(server_cx.executor());
281    fs.insert_tree(
282        "/code",
283        json!({
284            "project1": {
285                ".git": {},
286                "README.md": "# project 1",
287                "src": {
288                    "lib.rs": "fn one() -> usize { 1 }"
289                }
290            },
291            "project2": {
292                "README.md": "# project 2",
293            },
294        }),
295    )
296    .await;
297    fs.set_index_for_repo(
298        Path::new("/code/project1/.git"),
299        &[(Path::new("src/lib.rs"), "fn one() -> usize { 0 }".into())],
300    );
301
302    server_cx.update(HeadlessProject::init);
303    let headless = server_cx.new_model(|cx| HeadlessProject::new(server_ssh, fs.clone(), cx));
304    let project = build_project(client_ssh, cx);
305
306    project
307        .update(cx, {
308            let headless = headless.clone();
309            |_, cx| cx.on_release(|_, _| drop(headless))
310        })
311        .detach();
312    (project, headless, fs)
313}
314
315fn build_project(ssh: Arc<SshSession>, cx: &mut TestAppContext) -> Model<Project> {
316    cx.update(|cx| {
317        let settings_store = SettingsStore::test(cx);
318        cx.set_global(settings_store);
319    });
320
321    let client = cx.update(|cx| {
322        Client::new(
323            Arc::new(FakeSystemClock::default()),
324            FakeHttpClient::with_404_response(),
325            cx,
326        )
327    });
328
329    let node = FakeNodeRuntime::new();
330    let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
331    let languages = Arc::new(LanguageRegistry::test(cx.executor()));
332    let fs = FakeFs::new(cx.executor());
333    cx.update(|cx| {
334        Project::init(&client, cx);
335        language::init(cx);
336    });
337
338    cx.update(|cx| Project::ssh(ssh, client, node, user_store, languages, fs, cx))
339}