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