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}