1use crate::{
2 db::{tests::TestDb, ProjectId, UserId},
3 rpc::{Executor, Server, Store},
4 AppState,
5};
6use ::rpc::Peer;
7use anyhow::anyhow;
8use call::Room;
9use client::{
10 self, proto, test::FakeHttpClient, Channel, ChannelDetails, ChannelList, Client, Connection,
11 Credentials, EstablishConnectionError, UserStore, RECEIVE_TIMEOUT,
12};
13use collections::{BTreeMap, HashMap, HashSet};
14use editor::{
15 self, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Redo, Rename, ToOffset,
16 ToggleCodeActions, Undo,
17};
18use futures::{channel::mpsc, Future, StreamExt as _};
19use gpui::{
20 executor::{self, Deterministic},
21 geometry::vector::vec2f,
22 test::EmptyView,
23 ModelHandle, Task, TestAppContext, ViewHandle,
24};
25use language::{
26 range_to_lsp, tree_sitter_rust, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language,
27 LanguageConfig, LanguageRegistry, LineEnding, OffsetRangeExt, Point, Rope,
28};
29use lsp::{self, FakeLanguageServer};
30use parking_lot::Mutex;
31use project::{
32 fs::{FakeFs, Fs as _},
33 search::SearchQuery,
34 worktree::WorktreeHandle,
35 DiagnosticSummary, Project, ProjectPath, ProjectStore, WorktreeId,
36};
37use rand::prelude::*;
38use rpc::PeerId;
39use serde_json::json;
40use settings::{Formatter, Settings};
41use sqlx::types::time::OffsetDateTime;
42use std::{
43 cell::RefCell,
44 env,
45 ops::Deref,
46 path::{Path, PathBuf},
47 rc::Rc,
48 sync::{
49 atomic::{AtomicBool, Ordering::SeqCst},
50 Arc,
51 },
52 time::Duration,
53};
54use theme::ThemeRegistry;
55use workspace::{Item, SplitDirection, ToggleFollow, Workspace};
56
57#[ctor::ctor]
58fn init_logger() {
59 if std::env::var("RUST_LOG").is_ok() {
60 env_logger::init();
61 }
62}
63
64#[gpui::test(iterations = 10)]
65async fn test_basic_calls(
66 deterministic: Arc<Deterministic>,
67 cx_a: &mut TestAppContext,
68 cx_b: &mut TestAppContext,
69 cx_b2: &mut TestAppContext,
70 cx_c: &mut TestAppContext,
71) {
72 deterministic.forbid_parking();
73 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
74 let client_a = server.create_client(cx_a, "user_a").await;
75 let client_b = server.create_client(cx_b, "user_b").await;
76 let client_c = server.create_client(cx_c, "user_c").await;
77 server
78 .make_contacts(vec![
79 (&client_a, cx_a),
80 (&client_b, cx_b),
81 (&client_c, cx_c),
82 ])
83 .await;
84
85 let room_a = cx_a
86 .update(|cx| Room::create(client_a.clone(), client_a.user_store.clone(), cx))
87 .await
88 .unwrap();
89 assert_eq!(
90 room_participants(&room_a, &client_a, cx_a).await,
91 RoomParticipants {
92 remote: Default::default(),
93 pending: Default::default()
94 }
95 );
96
97 // Call user B from client A.
98 let mut incoming_call_b = client_b
99 .user_store
100 .update(cx_b, |user, _| user.incoming_call());
101 room_a
102 .update(cx_a, |room, cx| room.call(client_b.user_id().unwrap(), cx))
103 .await
104 .unwrap();
105
106 deterministic.run_until_parked();
107 assert_eq!(
108 room_participants(&room_a, &client_a, cx_a).await,
109 RoomParticipants {
110 remote: Default::default(),
111 pending: vec!["user_b".to_string()]
112 }
113 );
114
115 // User B receives the call.
116 let call_b = incoming_call_b.next().await.unwrap().unwrap();
117
118 // User B connects via another client and also receives a ring on the newly-connected client.
119 let client_b2 = server.create_client(cx_b2, "user_b").await;
120 let mut incoming_call_b2 = client_b2
121 .user_store
122 .update(cx_b2, |user, _| user.incoming_call());
123 deterministic.run_until_parked();
124 let _call_b2 = incoming_call_b2.next().await.unwrap().unwrap();
125
126 // User B joins the room using the first client.
127 let room_b = cx_b
128 .update(|cx| Room::join(&call_b, client_b.clone(), client_b.user_store.clone(), cx))
129 .await
130 .unwrap();
131 assert!(incoming_call_b.next().await.unwrap().is_none());
132
133 deterministic.run_until_parked();
134 assert_eq!(
135 room_participants(&room_a, &client_a, cx_a).await,
136 RoomParticipants {
137 remote: vec!["user_b".to_string()],
138 pending: Default::default()
139 }
140 );
141 assert_eq!(
142 room_participants(&room_b, &client_b, cx_b).await,
143 RoomParticipants {
144 remote: vec!["user_a".to_string()],
145 pending: Default::default()
146 }
147 );
148
149 // Call user C from client B.
150 let mut incoming_call_c = client_c
151 .user_store
152 .update(cx_c, |user, _| user.incoming_call());
153 room_b
154 .update(cx_b, |room, cx| room.call(client_c.user_id().unwrap(), cx))
155 .await
156 .unwrap();
157
158 deterministic.run_until_parked();
159 assert_eq!(
160 room_participants(&room_a, &client_a, cx_a).await,
161 RoomParticipants {
162 remote: vec!["user_b".to_string()],
163 pending: vec!["user_c".to_string()]
164 }
165 );
166 assert_eq!(
167 room_participants(&room_b, &client_b, cx_b).await,
168 RoomParticipants {
169 remote: vec!["user_a".to_string()],
170 pending: vec!["user_c".to_string()]
171 }
172 );
173
174 // User C receives the call, but declines it.
175 let _call_c = incoming_call_c.next().await.unwrap().unwrap();
176 client_c
177 .user_store
178 .update(cx_c, |user, _| user.decline_call())
179 .unwrap();
180 assert!(incoming_call_c.next().await.unwrap().is_none());
181
182 deterministic.run_until_parked();
183 assert_eq!(
184 room_participants(&room_a, &client_a, cx_a).await,
185 RoomParticipants {
186 remote: vec!["user_b".to_string()],
187 pending: Default::default()
188 }
189 );
190 assert_eq!(
191 room_participants(&room_b, &client_b, cx_b).await,
192 RoomParticipants {
193 remote: vec!["user_a".to_string()],
194 pending: Default::default()
195 }
196 );
197
198 // User A leaves the room.
199 room_a.update(cx_a, |room, cx| room.leave(cx)).unwrap();
200 deterministic.run_until_parked();
201 assert_eq!(
202 room_participants(&room_a, &client_a, cx_a).await,
203 RoomParticipants {
204 remote: Default::default(),
205 pending: Default::default()
206 }
207 );
208 assert_eq!(
209 room_participants(&room_b, &client_b, cx_b).await,
210 RoomParticipants {
211 remote: Default::default(),
212 pending: Default::default()
213 }
214 );
215}
216
217#[gpui::test(iterations = 10)]
218async fn test_leaving_room_on_disconnection(
219 deterministic: Arc<Deterministic>,
220 cx_a: &mut TestAppContext,
221 cx_b: &mut TestAppContext,
222) {
223 deterministic.forbid_parking();
224 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
225 let client_a = server.create_client(cx_a, "user_a").await;
226 let client_b = server.create_client(cx_b, "user_b").await;
227 server
228 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
229 .await;
230
231 let room_a = cx_a
232 .update(|cx| Room::create(client_a.clone(), client_a.user_store.clone(), cx))
233 .await
234 .unwrap();
235
236 // Call user B from client A.
237 let mut incoming_call_b = client_b
238 .user_store
239 .update(cx_b, |user, _| user.incoming_call());
240 room_a
241 .update(cx_a, |room, cx| room.call(client_b.user_id().unwrap(), cx))
242 .await
243 .unwrap();
244
245 // User B receives the call and joins the room.
246 let call_b = incoming_call_b.next().await.unwrap().unwrap();
247 let room_b = cx_b
248 .update(|cx| Room::join(&call_b, client_b.clone(), client_b.user_store.clone(), cx))
249 .await
250 .unwrap();
251 deterministic.run_until_parked();
252 assert_eq!(
253 room_participants(&room_a, &client_a, cx_a).await,
254 RoomParticipants {
255 remote: vec!["user_b".to_string()],
256 pending: Default::default()
257 }
258 );
259 assert_eq!(
260 room_participants(&room_b, &client_b, cx_b).await,
261 RoomParticipants {
262 remote: vec!["user_a".to_string()],
263 pending: Default::default()
264 }
265 );
266
267 server.disconnect_client(client_a.current_user_id(cx_a));
268 cx_a.foreground().advance_clock(rpc::RECEIVE_TIMEOUT);
269 assert_eq!(
270 room_participants(&room_a, &client_a, cx_a).await,
271 RoomParticipants {
272 remote: Default::default(),
273 pending: Default::default()
274 }
275 );
276 assert_eq!(
277 room_participants(&room_b, &client_b, cx_b).await,
278 RoomParticipants {
279 remote: Default::default(),
280 pending: Default::default()
281 }
282 );
283}
284
285#[gpui::test(iterations = 10)]
286async fn test_share_project(
287 deterministic: Arc<Deterministic>,
288 cx_a: &mut TestAppContext,
289 cx_b: &mut TestAppContext,
290 cx_b2: &mut TestAppContext,
291) {
292 cx_a.foreground().forbid_parking();
293 let (_, window_b) = cx_b.add_window(|_| EmptyView);
294 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
295 let client_a = server.create_client(cx_a, "user_a").await;
296 let client_b = server.create_client(cx_b, "user_b").await;
297 server
298 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
299 .await;
300
301 client_a
302 .fs
303 .insert_tree(
304 "/a",
305 json!({
306 ".gitignore": "ignored-dir",
307 "a.txt": "a-contents",
308 "b.txt": "b-contents",
309 "ignored-dir": {
310 "c.txt": "",
311 "d.txt": "",
312 }
313 }),
314 )
315 .await;
316
317 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
318 let project_id = project_a.read_with(cx_a, |project, _| project.remote_id().unwrap());
319
320 // Join that project as client B
321 let client_b_peer_id = client_b.peer_id;
322 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
323 let replica_id_b = project_b.read_with(cx_b, |project, _| {
324 assert_eq!(
325 project
326 .collaborators()
327 .get(&client_a.peer_id)
328 .unwrap()
329 .user
330 .github_login,
331 "user_a"
332 );
333 project.replica_id()
334 });
335
336 deterministic.run_until_parked();
337 project_a.read_with(cx_a, |project, _| {
338 let client_b_collaborator = project.collaborators().get(&client_b_peer_id).unwrap();
339 assert_eq!(client_b_collaborator.replica_id, replica_id_b);
340 assert_eq!(client_b_collaborator.user.github_login, "user_b");
341 });
342 project_b.read_with(cx_b, |project, cx| {
343 let worktree = project.worktrees(cx).next().unwrap().read(cx);
344 assert_eq!(
345 worktree.paths().map(AsRef::as_ref).collect::<Vec<_>>(),
346 [
347 Path::new(".gitignore"),
348 Path::new("a.txt"),
349 Path::new("b.txt"),
350 Path::new("ignored-dir"),
351 Path::new("ignored-dir/c.txt"),
352 Path::new("ignored-dir/d.txt"),
353 ]
354 );
355 });
356
357 // Open the same file as client B and client A.
358 let buffer_b = project_b
359 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx))
360 .await
361 .unwrap();
362 buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "b-contents"));
363 project_a.read_with(cx_a, |project, cx| {
364 assert!(project.has_open_buffer((worktree_id, "b.txt"), cx))
365 });
366 let buffer_a = project_a
367 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx))
368 .await
369 .unwrap();
370
371 let editor_b = cx_b.add_view(&window_b, |cx| Editor::for_buffer(buffer_b, None, cx));
372
373 // TODO
374 // // Create a selection set as client B and see that selection set as client A.
375 // buffer_a
376 // .condition(&cx_a, |buffer, _| buffer.selection_sets().count() == 1)
377 // .await;
378
379 // Edit the buffer as client B and see that edit as client A.
380 editor_b.update(cx_b, |editor, cx| editor.handle_input("ok, ", cx));
381 buffer_a
382 .condition(cx_a, |buffer, _| buffer.text() == "ok, b-contents")
383 .await;
384
385 // TODO
386 // // Remove the selection set as client B, see those selections disappear as client A.
387 cx_b.update(move |_| drop(editor_b));
388 // buffer_a
389 // .condition(&cx_a, |buffer, _| buffer.selection_sets().count() == 0)
390 // .await;
391
392 // Client B can join again on a different window because they are already a participant.
393 let client_b2 = server.create_client(cx_b2, "user_b").await;
394 let project_b2 = Project::remote(
395 project_id,
396 client_b2.client.clone(),
397 client_b2.user_store.clone(),
398 client_b2.project_store.clone(),
399 client_b2.language_registry.clone(),
400 FakeFs::new(cx_b2.background()),
401 cx_b2.to_async(),
402 )
403 .await
404 .unwrap();
405 deterministic.run_until_parked();
406 project_a.read_with(cx_a, |project, _| {
407 assert_eq!(project.collaborators().len(), 2);
408 });
409 project_b.read_with(cx_b, |project, _| {
410 assert_eq!(project.collaborators().len(), 2);
411 });
412 project_b2.read_with(cx_b2, |project, _| {
413 assert_eq!(project.collaborators().len(), 2);
414 });
415
416 // Dropping client B's first project removes only that from client A's collaborators.
417 cx_b.update(move |_| drop(project_b));
418 deterministic.run_until_parked();
419 project_a.read_with(cx_a, |project, _| {
420 assert_eq!(project.collaborators().len(), 1);
421 });
422 project_b2.read_with(cx_b2, |project, _| {
423 assert_eq!(project.collaborators().len(), 1);
424 });
425}
426
427#[gpui::test(iterations = 10)]
428async fn test_unshare_project(
429 deterministic: Arc<Deterministic>,
430 cx_a: &mut TestAppContext,
431 cx_b: &mut TestAppContext,
432) {
433 cx_a.foreground().forbid_parking();
434 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
435 let client_a = server.create_client(cx_a, "user_a").await;
436 let client_b = server.create_client(cx_b, "user_b").await;
437 server
438 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
439 .await;
440
441 client_a
442 .fs
443 .insert_tree(
444 "/a",
445 json!({
446 "a.txt": "a-contents",
447 "b.txt": "b-contents",
448 }),
449 )
450 .await;
451
452 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
453 let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap());
454 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
455 assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
456
457 project_b
458 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
459 .await
460 .unwrap();
461
462 // When client B leaves the project, it gets automatically unshared.
463 cx_b.update(|_| drop(project_b));
464 deterministic.run_until_parked();
465 assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared()));
466
467 // When client B joins again, the project gets re-shared.
468 let project_b2 = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
469 assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
470 project_b2
471 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
472 .await
473 .unwrap();
474
475 // When client A (the host) leaves, the project gets unshared and guests are notified.
476 cx_a.update(|_| drop(project_a));
477 deterministic.run_until_parked();
478 project_b2.read_with(cx_b, |project, _| {
479 assert!(project.is_read_only());
480 assert!(project.collaborators().is_empty());
481 });
482}
483
484#[gpui::test(iterations = 10)]
485async fn test_host_disconnect(
486 deterministic: Arc<Deterministic>,
487 cx_a: &mut TestAppContext,
488 cx_b: &mut TestAppContext,
489 cx_c: &mut TestAppContext,
490) {
491 cx_b.update(editor::init);
492 deterministic.forbid_parking();
493 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
494 let client_a = server.create_client(cx_a, "user_a").await;
495 let client_b = server.create_client(cx_b, "user_b").await;
496 let client_c = server.create_client(cx_c, "user_c").await;
497 server
498 .make_contacts(vec![
499 (&client_a, cx_a),
500 (&client_b, cx_b),
501 (&client_c, cx_c),
502 ])
503 .await;
504
505 client_a
506 .fs
507 .insert_tree(
508 "/a",
509 json!({
510 "a.txt": "a-contents",
511 "b.txt": "b-contents",
512 }),
513 )
514 .await;
515
516 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
517 let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap());
518 let project_id = project_a.read_with(cx_a, |project, _| project.remote_id().unwrap());
519
520 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
521 assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
522
523 let (_, workspace_b) =
524 cx_b.add_window(|cx| Workspace::new(project_b.clone(), |_, _| unimplemented!(), cx));
525 let editor_b = workspace_b
526 .update(cx_b, |workspace, cx| {
527 workspace.open_path((worktree_id, "b.txt"), true, cx)
528 })
529 .await
530 .unwrap()
531 .downcast::<Editor>()
532 .unwrap();
533 cx_b.read(|cx| {
534 assert_eq!(
535 cx.focused_view_id(workspace_b.window_id()),
536 Some(editor_b.id())
537 );
538 });
539 editor_b.update(cx_b, |editor, cx| editor.insert("X", cx));
540 assert!(cx_b.is_window_edited(workspace_b.window_id()));
541
542 // Request to join that project as client C
543 let project_c = cx_c.spawn(|cx| {
544 Project::remote(
545 project_id,
546 client_c.client.clone(),
547 client_c.user_store.clone(),
548 client_c.project_store.clone(),
549 client_c.language_registry.clone(),
550 FakeFs::new(cx.background()),
551 cx,
552 )
553 });
554 deterministic.run_until_parked();
555
556 // Drop client A's connection. Collaborators should disappear and the project should not be shown as shared.
557 server.disconnect_client(client_a.current_user_id(cx_a));
558 cx_a.foreground().advance_clock(rpc::RECEIVE_TIMEOUT);
559 project_a
560 .condition(cx_a, |project, _| project.collaborators().is_empty())
561 .await;
562 project_a.read_with(cx_a, |project, _| assert!(!project.is_shared()));
563 project_b
564 .condition(cx_b, |project, _| project.is_read_only())
565 .await;
566 assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared()));
567 assert!(matches!(
568 project_c.await.unwrap_err(),
569 project::JoinProjectError::HostWentOffline
570 ));
571
572 // Ensure client B's edited state is reset and that the whole window is blurred.
573 cx_b.read(|cx| {
574 assert_eq!(cx.focused_view_id(workspace_b.window_id()), None);
575 });
576 assert!(!cx_b.is_window_edited(workspace_b.window_id()));
577
578 // Ensure client B is not prompted to save edits when closing window after disconnecting.
579 workspace_b
580 .update(cx_b, |workspace, cx| {
581 workspace.close(&Default::default(), cx)
582 })
583 .unwrap()
584 .await
585 .unwrap();
586 assert_eq!(cx_b.window_ids().len(), 0);
587 cx_b.update(|_| {
588 drop(workspace_b);
589 drop(project_b);
590 });
591
592 // Ensure guests can still join.
593 let project_b2 = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
594 assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
595 project_b2
596 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
597 .await
598 .unwrap();
599}
600
601#[gpui::test(iterations = 10)]
602async fn test_decline_join_request(
603 deterministic: Arc<Deterministic>,
604 cx_a: &mut TestAppContext,
605 cx_b: &mut TestAppContext,
606) {
607 cx_a.foreground().forbid_parking();
608 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
609 let client_a = server.create_client(cx_a, "user_a").await;
610 let client_b = server.create_client(cx_b, "user_b").await;
611 server
612 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
613 .await;
614
615 client_a.fs.insert_tree("/a", json!({})).await;
616
617 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
618 let project_id = project_a.read_with(cx_a, |project, _| project.remote_id().unwrap());
619
620 // Request to join that project as client B
621 let project_b = cx_b.spawn(|cx| {
622 Project::remote(
623 project_id,
624 client_b.client.clone(),
625 client_b.user_store.clone(),
626 client_b.project_store.clone(),
627 client_b.language_registry.clone(),
628 FakeFs::new(cx.background()),
629 cx,
630 )
631 });
632 deterministic.run_until_parked();
633 project_a.update(cx_a, |project, cx| {
634 project.respond_to_join_request(client_b.user_id().unwrap(), false, cx)
635 });
636 assert!(matches!(
637 project_b.await.unwrap_err(),
638 project::JoinProjectError::HostDeclined
639 ));
640
641 // Request to join the project again as client B
642 let project_b = cx_b.spawn(|cx| {
643 Project::remote(
644 project_id,
645 client_b.client.clone(),
646 client_b.user_store.clone(),
647 client_b.project_store.clone(),
648 client_b.language_registry.clone(),
649 FakeFs::new(cx.background()),
650 cx,
651 )
652 });
653
654 // Close the project on the host
655 deterministic.run_until_parked();
656 cx_a.update(|_| drop(project_a));
657 deterministic.run_until_parked();
658 assert!(matches!(
659 project_b.await.unwrap_err(),
660 project::JoinProjectError::HostClosedProject
661 ));
662}
663
664#[gpui::test(iterations = 10)]
665async fn test_cancel_join_request(
666 deterministic: Arc<Deterministic>,
667 cx_a: &mut TestAppContext,
668 cx_b: &mut TestAppContext,
669) {
670 cx_a.foreground().forbid_parking();
671 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
672 let client_a = server.create_client(cx_a, "user_a").await;
673 let client_b = server.create_client(cx_b, "user_b").await;
674 server
675 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
676 .await;
677
678 client_a.fs.insert_tree("/a", json!({})).await;
679 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
680 let project_id = project_a.read_with(cx_a, |project, _| project.remote_id().unwrap());
681
682 let user_b = client_a
683 .user_store
684 .update(cx_a, |store, cx| {
685 store.get_user(client_b.user_id().unwrap(), cx)
686 })
687 .await
688 .unwrap();
689
690 let project_a_events = Rc::new(RefCell::new(Vec::new()));
691 project_a.update(cx_a, {
692 let project_a_events = project_a_events.clone();
693 move |_, cx| {
694 cx.subscribe(&cx.handle(), move |_, _, event, _| {
695 project_a_events.borrow_mut().push(event.clone());
696 })
697 .detach();
698 }
699 });
700
701 // Request to join that project as client B
702 let project_b = cx_b.spawn(|cx| {
703 Project::remote(
704 project_id,
705 client_b.client.clone(),
706 client_b.user_store.clone(),
707 client_b.project_store.clone(),
708 client_b.language_registry.clone(),
709 FakeFs::new(cx.background()),
710 cx,
711 )
712 });
713 deterministic.run_until_parked();
714 assert_eq!(
715 &*project_a_events.borrow(),
716 &[project::Event::ContactRequestedJoin(user_b.clone())]
717 );
718 project_a_events.borrow_mut().clear();
719
720 // Cancel the join request by leaving the project
721 client_b
722 .client
723 .send(proto::LeaveProject { project_id })
724 .unwrap();
725 drop(project_b);
726
727 deterministic.run_until_parked();
728 assert_eq!(
729 &*project_a_events.borrow(),
730 &[project::Event::ContactCancelledJoinRequest(user_b)]
731 );
732}
733
734#[gpui::test(iterations = 10)]
735async fn test_propagate_saves_and_fs_changes(
736 cx_a: &mut TestAppContext,
737 cx_b: &mut TestAppContext,
738 cx_c: &mut TestAppContext,
739) {
740 cx_a.foreground().forbid_parking();
741 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
742 let client_a = server.create_client(cx_a, "user_a").await;
743 let client_b = server.create_client(cx_b, "user_b").await;
744 let client_c = server.create_client(cx_c, "user_c").await;
745 server
746 .make_contacts(vec![
747 (&client_a, cx_a),
748 (&client_b, cx_b),
749 (&client_c, cx_c),
750 ])
751 .await;
752
753 client_a
754 .fs
755 .insert_tree(
756 "/a",
757 json!({
758 "file1": "",
759 "file2": ""
760 }),
761 )
762 .await;
763 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
764 let worktree_a = project_a.read_with(cx_a, |p, cx| p.worktrees(cx).next().unwrap());
765
766 // Join that worktree as clients B and C.
767 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
768 let project_c = client_c.build_remote_project(&project_a, cx_a, cx_c).await;
769 let worktree_b = project_b.read_with(cx_b, |p, cx| p.worktrees(cx).next().unwrap());
770 let worktree_c = project_c.read_with(cx_c, |p, cx| p.worktrees(cx).next().unwrap());
771
772 // Open and edit a buffer as both guests B and C.
773 let buffer_b = project_b
774 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "file1"), cx))
775 .await
776 .unwrap();
777 let buffer_c = project_c
778 .update(cx_c, |p, cx| p.open_buffer((worktree_id, "file1"), cx))
779 .await
780 .unwrap();
781 buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "i-am-b, ")], None, cx));
782 buffer_c.update(cx_c, |buf, cx| buf.edit([(0..0, "i-am-c, ")], None, cx));
783
784 // Open and edit that buffer as the host.
785 let buffer_a = project_a
786 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "file1"), cx))
787 .await
788 .unwrap();
789
790 buffer_a
791 .condition(cx_a, |buf, _| buf.text() == "i-am-c, i-am-b, ")
792 .await;
793 buffer_a.update(cx_a, |buf, cx| {
794 buf.edit([(buf.len()..buf.len(), "i-am-a")], None, cx)
795 });
796
797 // Wait for edits to propagate
798 buffer_a
799 .condition(cx_a, |buf, _| buf.text() == "i-am-c, i-am-b, i-am-a")
800 .await;
801 buffer_b
802 .condition(cx_b, |buf, _| buf.text() == "i-am-c, i-am-b, i-am-a")
803 .await;
804 buffer_c
805 .condition(cx_c, |buf, _| buf.text() == "i-am-c, i-am-b, i-am-a")
806 .await;
807
808 // Edit the buffer as the host and concurrently save as guest B.
809 let save_b = buffer_b.update(cx_b, |buf, cx| buf.save(cx));
810 buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "hi-a, ")], None, cx));
811 save_b.await.unwrap();
812 assert_eq!(
813 client_a.fs.load("/a/file1".as_ref()).await.unwrap(),
814 "hi-a, i-am-c, i-am-b, i-am-a"
815 );
816 buffer_a.read_with(cx_a, |buf, _| assert!(!buf.is_dirty()));
817 buffer_b.read_with(cx_b, |buf, _| assert!(!buf.is_dirty()));
818 buffer_c.condition(cx_c, |buf, _| !buf.is_dirty()).await;
819
820 worktree_a.flush_fs_events(cx_a).await;
821
822 // Make changes on host's file system, see those changes on guest worktrees.
823 client_a
824 .fs
825 .rename(
826 "/a/file1".as_ref(),
827 "/a/file1-renamed".as_ref(),
828 Default::default(),
829 )
830 .await
831 .unwrap();
832
833 client_a
834 .fs
835 .rename("/a/file2".as_ref(), "/a/file3".as_ref(), Default::default())
836 .await
837 .unwrap();
838 client_a.fs.insert_file("/a/file4", "4".into()).await;
839
840 worktree_a
841 .condition(cx_a, |tree, _| {
842 tree.paths()
843 .map(|p| p.to_string_lossy())
844 .collect::<Vec<_>>()
845 == ["file1-renamed", "file3", "file4"]
846 })
847 .await;
848 worktree_b
849 .condition(cx_b, |tree, _| {
850 tree.paths()
851 .map(|p| p.to_string_lossy())
852 .collect::<Vec<_>>()
853 == ["file1-renamed", "file3", "file4"]
854 })
855 .await;
856 worktree_c
857 .condition(cx_c, |tree, _| {
858 tree.paths()
859 .map(|p| p.to_string_lossy())
860 .collect::<Vec<_>>()
861 == ["file1-renamed", "file3", "file4"]
862 })
863 .await;
864
865 // Ensure buffer files are updated as well.
866 buffer_a
867 .condition(cx_a, |buf, _| {
868 buf.file().unwrap().path().to_str() == Some("file1-renamed")
869 })
870 .await;
871 buffer_b
872 .condition(cx_b, |buf, _| {
873 buf.file().unwrap().path().to_str() == Some("file1-renamed")
874 })
875 .await;
876 buffer_c
877 .condition(cx_c, |buf, _| {
878 buf.file().unwrap().path().to_str() == Some("file1-renamed")
879 })
880 .await;
881}
882
883#[gpui::test(iterations = 10)]
884async fn test_fs_operations(
885 executor: Arc<Deterministic>,
886 cx_a: &mut TestAppContext,
887 cx_b: &mut TestAppContext,
888) {
889 executor.forbid_parking();
890 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
891 let client_a = server.create_client(cx_a, "user_a").await;
892 let client_b = server.create_client(cx_b, "user_b").await;
893 server
894 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
895 .await;
896
897 client_a
898 .fs
899 .insert_tree(
900 "/dir",
901 json!({
902 "a.txt": "a-contents",
903 "b.txt": "b-contents",
904 }),
905 )
906 .await;
907 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
908 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
909
910 let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap());
911 let worktree_b = project_b.read_with(cx_b, |project, cx| project.worktrees(cx).next().unwrap());
912
913 let entry = project_b
914 .update(cx_b, |project, cx| {
915 project
916 .create_entry((worktree_id, "c.txt"), false, cx)
917 .unwrap()
918 })
919 .await
920 .unwrap();
921 worktree_a.read_with(cx_a, |worktree, _| {
922 assert_eq!(
923 worktree
924 .paths()
925 .map(|p| p.to_string_lossy())
926 .collect::<Vec<_>>(),
927 ["a.txt", "b.txt", "c.txt"]
928 );
929 });
930 worktree_b.read_with(cx_b, |worktree, _| {
931 assert_eq!(
932 worktree
933 .paths()
934 .map(|p| p.to_string_lossy())
935 .collect::<Vec<_>>(),
936 ["a.txt", "b.txt", "c.txt"]
937 );
938 });
939
940 project_b
941 .update(cx_b, |project, cx| {
942 project.rename_entry(entry.id, Path::new("d.txt"), cx)
943 })
944 .unwrap()
945 .await
946 .unwrap();
947 worktree_a.read_with(cx_a, |worktree, _| {
948 assert_eq!(
949 worktree
950 .paths()
951 .map(|p| p.to_string_lossy())
952 .collect::<Vec<_>>(),
953 ["a.txt", "b.txt", "d.txt"]
954 );
955 });
956 worktree_b.read_with(cx_b, |worktree, _| {
957 assert_eq!(
958 worktree
959 .paths()
960 .map(|p| p.to_string_lossy())
961 .collect::<Vec<_>>(),
962 ["a.txt", "b.txt", "d.txt"]
963 );
964 });
965
966 let dir_entry = project_b
967 .update(cx_b, |project, cx| {
968 project
969 .create_entry((worktree_id, "DIR"), true, cx)
970 .unwrap()
971 })
972 .await
973 .unwrap();
974 worktree_a.read_with(cx_a, |worktree, _| {
975 assert_eq!(
976 worktree
977 .paths()
978 .map(|p| p.to_string_lossy())
979 .collect::<Vec<_>>(),
980 ["DIR", "a.txt", "b.txt", "d.txt"]
981 );
982 });
983 worktree_b.read_with(cx_b, |worktree, _| {
984 assert_eq!(
985 worktree
986 .paths()
987 .map(|p| p.to_string_lossy())
988 .collect::<Vec<_>>(),
989 ["DIR", "a.txt", "b.txt", "d.txt"]
990 );
991 });
992
993 project_b
994 .update(cx_b, |project, cx| {
995 project
996 .create_entry((worktree_id, "DIR/e.txt"), false, cx)
997 .unwrap()
998 })
999 .await
1000 .unwrap();
1001 project_b
1002 .update(cx_b, |project, cx| {
1003 project
1004 .create_entry((worktree_id, "DIR/SUBDIR"), true, cx)
1005 .unwrap()
1006 })
1007 .await
1008 .unwrap();
1009 project_b
1010 .update(cx_b, |project, cx| {
1011 project
1012 .create_entry((worktree_id, "DIR/SUBDIR/f.txt"), false, cx)
1013 .unwrap()
1014 })
1015 .await
1016 .unwrap();
1017 worktree_a.read_with(cx_a, |worktree, _| {
1018 assert_eq!(
1019 worktree
1020 .paths()
1021 .map(|p| p.to_string_lossy())
1022 .collect::<Vec<_>>(),
1023 [
1024 "DIR",
1025 "DIR/SUBDIR",
1026 "DIR/SUBDIR/f.txt",
1027 "DIR/e.txt",
1028 "a.txt",
1029 "b.txt",
1030 "d.txt"
1031 ]
1032 );
1033 });
1034 worktree_b.read_with(cx_b, |worktree, _| {
1035 assert_eq!(
1036 worktree
1037 .paths()
1038 .map(|p| p.to_string_lossy())
1039 .collect::<Vec<_>>(),
1040 [
1041 "DIR",
1042 "DIR/SUBDIR",
1043 "DIR/SUBDIR/f.txt",
1044 "DIR/e.txt",
1045 "a.txt",
1046 "b.txt",
1047 "d.txt"
1048 ]
1049 );
1050 });
1051
1052 project_b
1053 .update(cx_b, |project, cx| {
1054 project
1055 .copy_entry(entry.id, Path::new("f.txt"), cx)
1056 .unwrap()
1057 })
1058 .await
1059 .unwrap();
1060 worktree_a.read_with(cx_a, |worktree, _| {
1061 assert_eq!(
1062 worktree
1063 .paths()
1064 .map(|p| p.to_string_lossy())
1065 .collect::<Vec<_>>(),
1066 [
1067 "DIR",
1068 "DIR/SUBDIR",
1069 "DIR/SUBDIR/f.txt",
1070 "DIR/e.txt",
1071 "a.txt",
1072 "b.txt",
1073 "d.txt",
1074 "f.txt"
1075 ]
1076 );
1077 });
1078 worktree_b.read_with(cx_b, |worktree, _| {
1079 assert_eq!(
1080 worktree
1081 .paths()
1082 .map(|p| p.to_string_lossy())
1083 .collect::<Vec<_>>(),
1084 [
1085 "DIR",
1086 "DIR/SUBDIR",
1087 "DIR/SUBDIR/f.txt",
1088 "DIR/e.txt",
1089 "a.txt",
1090 "b.txt",
1091 "d.txt",
1092 "f.txt"
1093 ]
1094 );
1095 });
1096
1097 project_b
1098 .update(cx_b, |project, cx| {
1099 project.delete_entry(dir_entry.id, cx).unwrap()
1100 })
1101 .await
1102 .unwrap();
1103 worktree_a.read_with(cx_a, |worktree, _| {
1104 assert_eq!(
1105 worktree
1106 .paths()
1107 .map(|p| p.to_string_lossy())
1108 .collect::<Vec<_>>(),
1109 ["a.txt", "b.txt", "d.txt", "f.txt"]
1110 );
1111 });
1112 worktree_b.read_with(cx_b, |worktree, _| {
1113 assert_eq!(
1114 worktree
1115 .paths()
1116 .map(|p| p.to_string_lossy())
1117 .collect::<Vec<_>>(),
1118 ["a.txt", "b.txt", "d.txt", "f.txt"]
1119 );
1120 });
1121
1122 project_b
1123 .update(cx_b, |project, cx| {
1124 project.delete_entry(entry.id, cx).unwrap()
1125 })
1126 .await
1127 .unwrap();
1128 worktree_a.read_with(cx_a, |worktree, _| {
1129 assert_eq!(
1130 worktree
1131 .paths()
1132 .map(|p| p.to_string_lossy())
1133 .collect::<Vec<_>>(),
1134 ["a.txt", "b.txt", "f.txt"]
1135 );
1136 });
1137 worktree_b.read_with(cx_b, |worktree, _| {
1138 assert_eq!(
1139 worktree
1140 .paths()
1141 .map(|p| p.to_string_lossy())
1142 .collect::<Vec<_>>(),
1143 ["a.txt", "b.txt", "f.txt"]
1144 );
1145 });
1146}
1147
1148#[gpui::test(iterations = 10)]
1149async fn test_buffer_conflict_after_save(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
1150 cx_a.foreground().forbid_parking();
1151 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
1152 let client_a = server.create_client(cx_a, "user_a").await;
1153 let client_b = server.create_client(cx_b, "user_b").await;
1154 server
1155 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
1156 .await;
1157
1158 client_a
1159 .fs
1160 .insert_tree(
1161 "/dir",
1162 json!({
1163 "a.txt": "a-contents",
1164 }),
1165 )
1166 .await;
1167 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
1168 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
1169
1170 // Open a buffer as client B
1171 let buffer_b = project_b
1172 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
1173 .await
1174 .unwrap();
1175
1176 buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "world ")], None, cx));
1177 buffer_b.read_with(cx_b, |buf, _| {
1178 assert!(buf.is_dirty());
1179 assert!(!buf.has_conflict());
1180 });
1181
1182 buffer_b.update(cx_b, |buf, cx| buf.save(cx)).await.unwrap();
1183 buffer_b
1184 .condition(cx_b, |buffer_b, _| !buffer_b.is_dirty())
1185 .await;
1186 buffer_b.read_with(cx_b, |buf, _| {
1187 assert!(!buf.has_conflict());
1188 });
1189
1190 buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "hello ")], None, cx));
1191 buffer_b.read_with(cx_b, |buf, _| {
1192 assert!(buf.is_dirty());
1193 assert!(!buf.has_conflict());
1194 });
1195}
1196
1197#[gpui::test(iterations = 10)]
1198async fn test_buffer_reloading(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
1199 cx_a.foreground().forbid_parking();
1200 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
1201 let client_a = server.create_client(cx_a, "user_a").await;
1202 let client_b = server.create_client(cx_b, "user_b").await;
1203 server
1204 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
1205 .await;
1206
1207 client_a
1208 .fs
1209 .insert_tree(
1210 "/dir",
1211 json!({
1212 "a.txt": "a\nb\nc",
1213 }),
1214 )
1215 .await;
1216 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
1217 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
1218
1219 // Open a buffer as client B
1220 let buffer_b = project_b
1221 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
1222 .await
1223 .unwrap();
1224 buffer_b.read_with(cx_b, |buf, _| {
1225 assert!(!buf.is_dirty());
1226 assert!(!buf.has_conflict());
1227 assert_eq!(buf.line_ending(), LineEnding::Unix);
1228 });
1229
1230 let new_contents = Rope::from("d\ne\nf");
1231 client_a
1232 .fs
1233 .save("/dir/a.txt".as_ref(), &new_contents, LineEnding::Windows)
1234 .await
1235 .unwrap();
1236 buffer_b
1237 .condition(cx_b, |buf, _| {
1238 buf.text() == new_contents.to_string() && !buf.is_dirty()
1239 })
1240 .await;
1241 buffer_b.read_with(cx_b, |buf, _| {
1242 assert!(!buf.is_dirty());
1243 assert!(!buf.has_conflict());
1244 assert_eq!(buf.line_ending(), LineEnding::Windows);
1245 });
1246}
1247
1248#[gpui::test(iterations = 10)]
1249async fn test_editing_while_guest_opens_buffer(
1250 cx_a: &mut TestAppContext,
1251 cx_b: &mut TestAppContext,
1252) {
1253 cx_a.foreground().forbid_parking();
1254 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
1255 let client_a = server.create_client(cx_a, "user_a").await;
1256 let client_b = server.create_client(cx_b, "user_b").await;
1257 server
1258 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
1259 .await;
1260
1261 client_a
1262 .fs
1263 .insert_tree("/dir", json!({ "a.txt": "a-contents" }))
1264 .await;
1265 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
1266 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
1267
1268 // Open a buffer as client A
1269 let buffer_a = project_a
1270 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
1271 .await
1272 .unwrap();
1273
1274 // Start opening the same buffer as client B
1275 let buffer_b = cx_b
1276 .background()
1277 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)));
1278
1279 // Edit the buffer as client A while client B is still opening it.
1280 cx_b.background().simulate_random_delay().await;
1281 buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "X")], None, cx));
1282 cx_b.background().simulate_random_delay().await;
1283 buffer_a.update(cx_a, |buf, cx| buf.edit([(1..1, "Y")], None, cx));
1284
1285 let text = buffer_a.read_with(cx_a, |buf, _| buf.text());
1286 let buffer_b = buffer_b.await.unwrap();
1287 buffer_b.condition(cx_b, |buf, _| buf.text() == text).await;
1288}
1289
1290#[gpui::test(iterations = 10)]
1291async fn test_leaving_worktree_while_opening_buffer(
1292 cx_a: &mut TestAppContext,
1293 cx_b: &mut TestAppContext,
1294) {
1295 cx_a.foreground().forbid_parking();
1296 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
1297 let client_a = server.create_client(cx_a, "user_a").await;
1298 let client_b = server.create_client(cx_b, "user_b").await;
1299 server
1300 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
1301 .await;
1302
1303 client_a
1304 .fs
1305 .insert_tree("/dir", json!({ "a.txt": "a-contents" }))
1306 .await;
1307 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
1308 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
1309
1310 // See that a guest has joined as client A.
1311 project_a
1312 .condition(cx_a, |p, _| p.collaborators().len() == 1)
1313 .await;
1314
1315 // Begin opening a buffer as client B, but leave the project before the open completes.
1316 let buffer_b = cx_b
1317 .background()
1318 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)));
1319 cx_b.update(|_| drop(project_b));
1320 drop(buffer_b);
1321
1322 // See that the guest has left.
1323 project_a
1324 .condition(cx_a, |p, _| p.collaborators().is_empty())
1325 .await;
1326}
1327
1328#[gpui::test(iterations = 10)]
1329async fn test_canceling_buffer_opening(
1330 deterministic: Arc<Deterministic>,
1331 cx_a: &mut TestAppContext,
1332 cx_b: &mut TestAppContext,
1333) {
1334 deterministic.forbid_parking();
1335
1336 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
1337 let client_a = server.create_client(cx_a, "user_a").await;
1338 let client_b = server.create_client(cx_b, "user_b").await;
1339 server
1340 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
1341 .await;
1342
1343 client_a
1344 .fs
1345 .insert_tree(
1346 "/dir",
1347 json!({
1348 "a.txt": "abc",
1349 }),
1350 )
1351 .await;
1352 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
1353 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
1354
1355 let buffer_a = project_a
1356 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
1357 .await
1358 .unwrap();
1359
1360 // Open a buffer as client B but cancel after a random amount of time.
1361 let buffer_b = project_b.update(cx_b, |p, cx| p.open_buffer_by_id(buffer_a.id() as u64, cx));
1362 deterministic.simulate_random_delay().await;
1363 drop(buffer_b);
1364
1365 // Try opening the same buffer again as client B, and ensure we can
1366 // still do it despite the cancellation above.
1367 let buffer_b = project_b
1368 .update(cx_b, |p, cx| p.open_buffer_by_id(buffer_a.id() as u64, cx))
1369 .await
1370 .unwrap();
1371 buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "abc"));
1372}
1373
1374#[gpui::test(iterations = 10)]
1375async fn test_leaving_project(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
1376 cx_a.foreground().forbid_parking();
1377 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
1378 let client_a = server.create_client(cx_a, "user_a").await;
1379 let client_b = server.create_client(cx_b, "user_b").await;
1380 server
1381 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
1382 .await;
1383
1384 client_a
1385 .fs
1386 .insert_tree(
1387 "/a",
1388 json!({
1389 "a.txt": "a-contents",
1390 "b.txt": "b-contents",
1391 }),
1392 )
1393 .await;
1394 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
1395 let _project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
1396
1397 // Client A sees that a guest has joined.
1398 project_a
1399 .condition(cx_a, |p, _| p.collaborators().len() == 1)
1400 .await;
1401
1402 // Drop client B's connection and ensure client A observes client B leaving the project.
1403 client_b.disconnect(&cx_b.to_async()).unwrap();
1404 project_a
1405 .condition(cx_a, |p, _| p.collaborators().is_empty())
1406 .await;
1407
1408 // Rejoin the project as client B
1409 let _project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
1410
1411 // Client A sees that a guest has re-joined.
1412 project_a
1413 .condition(cx_a, |p, _| p.collaborators().len() == 1)
1414 .await;
1415
1416 // Simulate connection loss for client B and ensure client A observes client B leaving the project.
1417 client_b.wait_for_current_user(cx_b).await;
1418 server.disconnect_client(client_b.current_user_id(cx_b));
1419 cx_a.foreground().advance_clock(rpc::RECEIVE_TIMEOUT);
1420 project_a
1421 .condition(cx_a, |p, _| p.collaborators().is_empty())
1422 .await;
1423}
1424
1425#[gpui::test(iterations = 10)]
1426async fn test_collaborating_with_diagnostics(
1427 deterministic: Arc<Deterministic>,
1428 cx_a: &mut TestAppContext,
1429 cx_b: &mut TestAppContext,
1430 cx_c: &mut TestAppContext,
1431) {
1432 deterministic.forbid_parking();
1433 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
1434 let client_a = server.create_client(cx_a, "user_a").await;
1435 let client_b = server.create_client(cx_b, "user_b").await;
1436 let client_c = server.create_client(cx_c, "user_c").await;
1437 server
1438 .make_contacts(vec![
1439 (&client_a, cx_a),
1440 (&client_b, cx_b),
1441 (&client_c, cx_c),
1442 ])
1443 .await;
1444
1445 // Set up a fake language server.
1446 let mut language = Language::new(
1447 LanguageConfig {
1448 name: "Rust".into(),
1449 path_suffixes: vec!["rs".to_string()],
1450 ..Default::default()
1451 },
1452 Some(tree_sitter_rust::language()),
1453 );
1454 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
1455 client_a.language_registry.add(Arc::new(language));
1456
1457 // Share a project as client A
1458 client_a
1459 .fs
1460 .insert_tree(
1461 "/a",
1462 json!({
1463 "a.rs": "let one = two",
1464 "other.rs": "",
1465 }),
1466 )
1467 .await;
1468 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1469 let project_id = project_a.update(cx_a, |p, _| p.next_remote_id()).await;
1470
1471 // Cause the language server to start.
1472 let _buffer = cx_a
1473 .background()
1474 .spawn(project_a.update(cx_a, |project, cx| {
1475 project.open_buffer(
1476 ProjectPath {
1477 worktree_id,
1478 path: Path::new("other.rs").into(),
1479 },
1480 cx,
1481 )
1482 }))
1483 .await
1484 .unwrap();
1485
1486 // Join the worktree as client B.
1487 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
1488
1489 // Simulate a language server reporting errors for a file.
1490 let mut fake_language_server = fake_language_servers.next().await.unwrap();
1491 fake_language_server
1492 .receive_notification::<lsp::notification::DidOpenTextDocument>()
1493 .await;
1494 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
1495 lsp::PublishDiagnosticsParams {
1496 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
1497 version: None,
1498 diagnostics: vec![lsp::Diagnostic {
1499 severity: Some(lsp::DiagnosticSeverity::ERROR),
1500 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
1501 message: "message 1".to_string(),
1502 ..Default::default()
1503 }],
1504 },
1505 );
1506
1507 // Wait for server to see the diagnostics update.
1508 deterministic.run_until_parked();
1509 {
1510 let store = server.store.lock().await;
1511 let project = store.project(ProjectId::from_proto(project_id)).unwrap();
1512 let worktree = project.worktrees.get(&worktree_id.to_proto()).unwrap();
1513 assert!(!worktree.diagnostic_summaries.is_empty());
1514 }
1515
1516 // Ensure client B observes the new diagnostics.
1517 project_b.read_with(cx_b, |project, cx| {
1518 assert_eq!(
1519 project.diagnostic_summaries(cx).collect::<Vec<_>>(),
1520 &[(
1521 ProjectPath {
1522 worktree_id,
1523 path: Arc::from(Path::new("a.rs")),
1524 },
1525 DiagnosticSummary {
1526 error_count: 1,
1527 warning_count: 0,
1528 ..Default::default()
1529 },
1530 )]
1531 )
1532 });
1533
1534 // Join project as client C and observe the diagnostics.
1535 let project_c = client_c.build_remote_project(&project_a, cx_a, cx_c).await;
1536 deterministic.run_until_parked();
1537 project_c.read_with(cx_c, |project, cx| {
1538 assert_eq!(
1539 project.diagnostic_summaries(cx).collect::<Vec<_>>(),
1540 &[(
1541 ProjectPath {
1542 worktree_id,
1543 path: Arc::from(Path::new("a.rs")),
1544 },
1545 DiagnosticSummary {
1546 error_count: 1,
1547 warning_count: 0,
1548 ..Default::default()
1549 },
1550 )]
1551 )
1552 });
1553
1554 // Simulate a language server reporting more errors for a file.
1555 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
1556 lsp::PublishDiagnosticsParams {
1557 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
1558 version: None,
1559 diagnostics: vec![
1560 lsp::Diagnostic {
1561 severity: Some(lsp::DiagnosticSeverity::ERROR),
1562 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
1563 message: "message 1".to_string(),
1564 ..Default::default()
1565 },
1566 lsp::Diagnostic {
1567 severity: Some(lsp::DiagnosticSeverity::WARNING),
1568 range: lsp::Range::new(lsp::Position::new(0, 10), lsp::Position::new(0, 13)),
1569 message: "message 2".to_string(),
1570 ..Default::default()
1571 },
1572 ],
1573 },
1574 );
1575
1576 // Clients B and C get the updated summaries
1577 deterministic.run_until_parked();
1578 project_b.read_with(cx_b, |project, cx| {
1579 assert_eq!(
1580 project.diagnostic_summaries(cx).collect::<Vec<_>>(),
1581 [(
1582 ProjectPath {
1583 worktree_id,
1584 path: Arc::from(Path::new("a.rs")),
1585 },
1586 DiagnosticSummary {
1587 error_count: 1,
1588 warning_count: 1,
1589 ..Default::default()
1590 },
1591 )]
1592 );
1593 });
1594 project_c.read_with(cx_c, |project, cx| {
1595 assert_eq!(
1596 project.diagnostic_summaries(cx).collect::<Vec<_>>(),
1597 [(
1598 ProjectPath {
1599 worktree_id,
1600 path: Arc::from(Path::new("a.rs")),
1601 },
1602 DiagnosticSummary {
1603 error_count: 1,
1604 warning_count: 1,
1605 ..Default::default()
1606 },
1607 )]
1608 );
1609 });
1610
1611 // Open the file with the errors on client B. They should be present.
1612 let buffer_b = cx_b
1613 .background()
1614 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
1615 .await
1616 .unwrap();
1617
1618 buffer_b.read_with(cx_b, |buffer, _| {
1619 assert_eq!(
1620 buffer
1621 .snapshot()
1622 .diagnostics_in_range::<_, Point>(0..buffer.len(), false)
1623 .collect::<Vec<_>>(),
1624 &[
1625 DiagnosticEntry {
1626 range: Point::new(0, 4)..Point::new(0, 7),
1627 diagnostic: Diagnostic {
1628 group_id: 1,
1629 message: "message 1".to_string(),
1630 severity: lsp::DiagnosticSeverity::ERROR,
1631 is_primary: true,
1632 ..Default::default()
1633 }
1634 },
1635 DiagnosticEntry {
1636 range: Point::new(0, 10)..Point::new(0, 13),
1637 diagnostic: Diagnostic {
1638 group_id: 2,
1639 severity: lsp::DiagnosticSeverity::WARNING,
1640 message: "message 2".to_string(),
1641 is_primary: true,
1642 ..Default::default()
1643 }
1644 }
1645 ]
1646 );
1647 });
1648
1649 // Simulate a language server reporting no errors for a file.
1650 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
1651 lsp::PublishDiagnosticsParams {
1652 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
1653 version: None,
1654 diagnostics: vec![],
1655 },
1656 );
1657 deterministic.run_until_parked();
1658 project_a.read_with(cx_a, |project, cx| {
1659 assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
1660 });
1661 project_b.read_with(cx_b, |project, cx| {
1662 assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
1663 });
1664 project_c.read_with(cx_c, |project, cx| {
1665 assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
1666 });
1667}
1668
1669#[gpui::test(iterations = 10)]
1670async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
1671 cx_a.foreground().forbid_parking();
1672 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
1673 let client_a = server.create_client(cx_a, "user_a").await;
1674 let client_b = server.create_client(cx_b, "user_b").await;
1675 server
1676 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
1677 .await;
1678
1679 // Set up a fake language server.
1680 let mut language = Language::new(
1681 LanguageConfig {
1682 name: "Rust".into(),
1683 path_suffixes: vec!["rs".to_string()],
1684 ..Default::default()
1685 },
1686 Some(tree_sitter_rust::language()),
1687 );
1688 let mut fake_language_servers = language
1689 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
1690 capabilities: lsp::ServerCapabilities {
1691 completion_provider: Some(lsp::CompletionOptions {
1692 trigger_characters: Some(vec![".".to_string()]),
1693 ..Default::default()
1694 }),
1695 ..Default::default()
1696 },
1697 ..Default::default()
1698 }))
1699 .await;
1700 client_a.language_registry.add(Arc::new(language));
1701
1702 client_a
1703 .fs
1704 .insert_tree(
1705 "/a",
1706 json!({
1707 "main.rs": "fn main() { a }",
1708 "other.rs": "",
1709 }),
1710 )
1711 .await;
1712 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1713 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
1714
1715 // Open a file in an editor as the guest.
1716 let buffer_b = project_b
1717 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1718 .await
1719 .unwrap();
1720 let (_, window_b) = cx_b.add_window(|_| EmptyView);
1721 let editor_b = cx_b.add_view(&window_b, |cx| {
1722 Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), cx)
1723 });
1724
1725 let fake_language_server = fake_language_servers.next().await.unwrap();
1726 buffer_b
1727 .condition(cx_b, |buffer, _| !buffer.completion_triggers().is_empty())
1728 .await;
1729
1730 // Type a completion trigger character as the guest.
1731 editor_b.update(cx_b, |editor, cx| {
1732 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1733 editor.handle_input(".", cx);
1734 cx.focus(&editor_b);
1735 });
1736
1737 // Receive a completion request as the host's language server.
1738 // Return some completions from the host's language server.
1739 cx_a.foreground().start_waiting();
1740 fake_language_server
1741 .handle_request::<lsp::request::Completion, _, _>(|params, _| async move {
1742 assert_eq!(
1743 params.text_document_position.text_document.uri,
1744 lsp::Url::from_file_path("/a/main.rs").unwrap(),
1745 );
1746 assert_eq!(
1747 params.text_document_position.position,
1748 lsp::Position::new(0, 14),
1749 );
1750
1751 Ok(Some(lsp::CompletionResponse::Array(vec![
1752 lsp::CompletionItem {
1753 label: "first_method(…)".into(),
1754 detail: Some("fn(&mut self, B) -> C".into()),
1755 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
1756 new_text: "first_method($1)".to_string(),
1757 range: lsp::Range::new(
1758 lsp::Position::new(0, 14),
1759 lsp::Position::new(0, 14),
1760 ),
1761 })),
1762 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
1763 ..Default::default()
1764 },
1765 lsp::CompletionItem {
1766 label: "second_method(…)".into(),
1767 detail: Some("fn(&mut self, C) -> D<E>".into()),
1768 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
1769 new_text: "second_method()".to_string(),
1770 range: lsp::Range::new(
1771 lsp::Position::new(0, 14),
1772 lsp::Position::new(0, 14),
1773 ),
1774 })),
1775 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
1776 ..Default::default()
1777 },
1778 ])))
1779 })
1780 .next()
1781 .await
1782 .unwrap();
1783 cx_a.foreground().finish_waiting();
1784
1785 // Open the buffer on the host.
1786 let buffer_a = project_a
1787 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1788 .await
1789 .unwrap();
1790 buffer_a
1791 .condition(cx_a, |buffer, _| buffer.text() == "fn main() { a. }")
1792 .await;
1793
1794 // Confirm a completion on the guest.
1795 editor_b
1796 .condition(cx_b, |editor, _| editor.context_menu_visible())
1797 .await;
1798 editor_b.update(cx_b, |editor, cx| {
1799 editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx);
1800 assert_eq!(editor.text(cx), "fn main() { a.first_method() }");
1801 });
1802
1803 // Return a resolved completion from the host's language server.
1804 // The resolved completion has an additional text edit.
1805 fake_language_server.handle_request::<lsp::request::ResolveCompletionItem, _, _>(
1806 |params, _| async move {
1807 assert_eq!(params.label, "first_method(…)");
1808 Ok(lsp::CompletionItem {
1809 label: "first_method(…)".into(),
1810 detail: Some("fn(&mut self, B) -> C".into()),
1811 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
1812 new_text: "first_method($1)".to_string(),
1813 range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
1814 })),
1815 additional_text_edits: Some(vec![lsp::TextEdit {
1816 new_text: "use d::SomeTrait;\n".to_string(),
1817 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
1818 }]),
1819 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
1820 ..Default::default()
1821 })
1822 },
1823 );
1824
1825 // The additional edit is applied.
1826 buffer_a
1827 .condition(cx_a, |buffer, _| {
1828 buffer.text() == "use d::SomeTrait;\nfn main() { a.first_method() }"
1829 })
1830 .await;
1831 buffer_b
1832 .condition(cx_b, |buffer, _| {
1833 buffer.text() == "use d::SomeTrait;\nfn main() { a.first_method() }"
1834 })
1835 .await;
1836}
1837
1838#[gpui::test(iterations = 10)]
1839async fn test_reloading_buffer_manually(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
1840 cx_a.foreground().forbid_parking();
1841 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
1842 let client_a = server.create_client(cx_a, "user_a").await;
1843 let client_b = server.create_client(cx_b, "user_b").await;
1844 server
1845 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
1846 .await;
1847
1848 client_a
1849 .fs
1850 .insert_tree("/a", json!({ "a.rs": "let one = 1;" }))
1851 .await;
1852 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1853 let buffer_a = project_a
1854 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))
1855 .await
1856 .unwrap();
1857
1858 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
1859
1860 let buffer_b = cx_b
1861 .background()
1862 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
1863 .await
1864 .unwrap();
1865 buffer_b.update(cx_b, |buffer, cx| {
1866 buffer.edit([(4..7, "six")], None, cx);
1867 buffer.edit([(10..11, "6")], None, cx);
1868 assert_eq!(buffer.text(), "let six = 6;");
1869 assert!(buffer.is_dirty());
1870 assert!(!buffer.has_conflict());
1871 });
1872 buffer_a
1873 .condition(cx_a, |buffer, _| buffer.text() == "let six = 6;")
1874 .await;
1875
1876 client_a
1877 .fs
1878 .save(
1879 "/a/a.rs".as_ref(),
1880 &Rope::from("let seven = 7;"),
1881 LineEnding::Unix,
1882 )
1883 .await
1884 .unwrap();
1885 buffer_a
1886 .condition(cx_a, |buffer, _| buffer.has_conflict())
1887 .await;
1888 buffer_b
1889 .condition(cx_b, |buffer, _| buffer.has_conflict())
1890 .await;
1891
1892 project_b
1893 .update(cx_b, |project, cx| {
1894 project.reload_buffers(HashSet::from_iter([buffer_b.clone()]), true, cx)
1895 })
1896 .await
1897 .unwrap();
1898 buffer_a.read_with(cx_a, |buffer, _| {
1899 assert_eq!(buffer.text(), "let seven = 7;");
1900 assert!(!buffer.is_dirty());
1901 assert!(!buffer.has_conflict());
1902 });
1903 buffer_b.read_with(cx_b, |buffer, _| {
1904 assert_eq!(buffer.text(), "let seven = 7;");
1905 assert!(!buffer.is_dirty());
1906 assert!(!buffer.has_conflict());
1907 });
1908
1909 buffer_a.update(cx_a, |buffer, cx| {
1910 // Undoing on the host is a no-op when the reload was initiated by the guest.
1911 buffer.undo(cx);
1912 assert_eq!(buffer.text(), "let seven = 7;");
1913 assert!(!buffer.is_dirty());
1914 assert!(!buffer.has_conflict());
1915 });
1916 buffer_b.update(cx_b, |buffer, cx| {
1917 // Undoing on the guest rolls back the buffer to before it was reloaded but the conflict gets cleared.
1918 buffer.undo(cx);
1919 assert_eq!(buffer.text(), "let six = 6;");
1920 assert!(buffer.is_dirty());
1921 assert!(!buffer.has_conflict());
1922 });
1923}
1924
1925#[gpui::test(iterations = 10)]
1926async fn test_formatting_buffer(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
1927 use project::FormatTrigger;
1928
1929 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
1930 let client_a = server.create_client(cx_a, "user_a").await;
1931 let client_b = server.create_client(cx_b, "user_b").await;
1932 server
1933 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
1934 .await;
1935
1936 // Set up a fake language server.
1937 let mut language = Language::new(
1938 LanguageConfig {
1939 name: "Rust".into(),
1940 path_suffixes: vec!["rs".to_string()],
1941 ..Default::default()
1942 },
1943 Some(tree_sitter_rust::language()),
1944 );
1945 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
1946 client_a.language_registry.add(Arc::new(language));
1947
1948 // Here we insert a fake tree with a directory that exists on disk. This is needed
1949 // because later we'll invoke a command, which requires passing a working directory
1950 // that points to a valid location on disk.
1951 let directory = env::current_dir().unwrap();
1952 client_a
1953 .fs
1954 .insert_tree(&directory, json!({ "a.rs": "let one = \"two\"" }))
1955 .await;
1956 let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await;
1957 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
1958
1959 let buffer_b = cx_b
1960 .background()
1961 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
1962 .await
1963 .unwrap();
1964
1965 let fake_language_server = fake_language_servers.next().await.unwrap();
1966 fake_language_server.handle_request::<lsp::request::Formatting, _, _>(|_, _| async move {
1967 Ok(Some(vec![
1968 lsp::TextEdit {
1969 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 4)),
1970 new_text: "h".to_string(),
1971 },
1972 lsp::TextEdit {
1973 range: lsp::Range::new(lsp::Position::new(0, 7), lsp::Position::new(0, 7)),
1974 new_text: "y".to_string(),
1975 },
1976 ]))
1977 });
1978
1979 project_b
1980 .update(cx_b, |project, cx| {
1981 project.format(
1982 HashSet::from_iter([buffer_b.clone()]),
1983 true,
1984 FormatTrigger::Save,
1985 cx,
1986 )
1987 })
1988 .await
1989 .unwrap();
1990 assert_eq!(
1991 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
1992 "let honey = \"two\""
1993 );
1994
1995 // Ensure buffer can be formatted using an external command. Notice how the
1996 // host's configuration is honored as opposed to using the guest's settings.
1997 cx_a.update(|cx| {
1998 cx.update_global(|settings: &mut Settings, _| {
1999 settings.editor_defaults.formatter = Some(Formatter::External {
2000 command: "awk".to_string(),
2001 arguments: vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()],
2002 });
2003 });
2004 });
2005 project_b
2006 .update(cx_b, |project, cx| {
2007 project.format(
2008 HashSet::from_iter([buffer_b.clone()]),
2009 true,
2010 FormatTrigger::Save,
2011 cx,
2012 )
2013 })
2014 .await
2015 .unwrap();
2016 assert_eq!(
2017 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
2018 format!("let honey = \"{}/a.rs\"\n", directory.to_str().unwrap())
2019 );
2020}
2021
2022#[gpui::test(iterations = 10)]
2023async fn test_definition(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2024 cx_a.foreground().forbid_parking();
2025 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
2026 let client_a = server.create_client(cx_a, "user_a").await;
2027 let client_b = server.create_client(cx_b, "user_b").await;
2028 server
2029 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
2030 .await;
2031
2032 // Set up a fake language server.
2033 let mut language = Language::new(
2034 LanguageConfig {
2035 name: "Rust".into(),
2036 path_suffixes: vec!["rs".to_string()],
2037 ..Default::default()
2038 },
2039 Some(tree_sitter_rust::language()),
2040 );
2041 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
2042 client_a.language_registry.add(Arc::new(language));
2043
2044 client_a
2045 .fs
2046 .insert_tree(
2047 "/root",
2048 json!({
2049 "dir-1": {
2050 "a.rs": "const ONE: usize = b::TWO + b::THREE;",
2051 },
2052 "dir-2": {
2053 "b.rs": "const TWO: c::T2 = 2;\nconst THREE: usize = 3;",
2054 "c.rs": "type T2 = usize;",
2055 }
2056 }),
2057 )
2058 .await;
2059 let (project_a, worktree_id) = client_a.build_local_project("/root/dir-1", cx_a).await;
2060 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
2061
2062 // Open the file on client B.
2063 let buffer_b = cx_b
2064 .background()
2065 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
2066 .await
2067 .unwrap();
2068
2069 // Request the definition of a symbol as the guest.
2070 let fake_language_server = fake_language_servers.next().await.unwrap();
2071 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
2072 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
2073 lsp::Location::new(
2074 lsp::Url::from_file_path("/root/dir-2/b.rs").unwrap(),
2075 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
2076 ),
2077 )))
2078 });
2079
2080 let definitions_1 = project_b
2081 .update(cx_b, |p, cx| p.definition(&buffer_b, 23, cx))
2082 .await
2083 .unwrap();
2084 cx_b.read(|cx| {
2085 assert_eq!(definitions_1.len(), 1);
2086 assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
2087 let target_buffer = definitions_1[0].target.buffer.read(cx);
2088 assert_eq!(
2089 target_buffer.text(),
2090 "const TWO: c::T2 = 2;\nconst THREE: usize = 3;"
2091 );
2092 assert_eq!(
2093 definitions_1[0].target.range.to_point(target_buffer),
2094 Point::new(0, 6)..Point::new(0, 9)
2095 );
2096 });
2097
2098 // Try getting more definitions for the same buffer, ensuring the buffer gets reused from
2099 // the previous call to `definition`.
2100 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
2101 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
2102 lsp::Location::new(
2103 lsp::Url::from_file_path("/root/dir-2/b.rs").unwrap(),
2104 lsp::Range::new(lsp::Position::new(1, 6), lsp::Position::new(1, 11)),
2105 ),
2106 )))
2107 });
2108
2109 let definitions_2 = project_b
2110 .update(cx_b, |p, cx| p.definition(&buffer_b, 33, cx))
2111 .await
2112 .unwrap();
2113 cx_b.read(|cx| {
2114 assert_eq!(definitions_2.len(), 1);
2115 assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
2116 let target_buffer = definitions_2[0].target.buffer.read(cx);
2117 assert_eq!(
2118 target_buffer.text(),
2119 "const TWO: c::T2 = 2;\nconst THREE: usize = 3;"
2120 );
2121 assert_eq!(
2122 definitions_2[0].target.range.to_point(target_buffer),
2123 Point::new(1, 6)..Point::new(1, 11)
2124 );
2125 });
2126 assert_eq!(
2127 definitions_1[0].target.buffer,
2128 definitions_2[0].target.buffer
2129 );
2130
2131 fake_language_server.handle_request::<lsp::request::GotoTypeDefinition, _, _>(
2132 |req, _| async move {
2133 assert_eq!(
2134 req.text_document_position_params.position,
2135 lsp::Position::new(0, 7)
2136 );
2137 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
2138 lsp::Location::new(
2139 lsp::Url::from_file_path("/root/dir-2/c.rs").unwrap(),
2140 lsp::Range::new(lsp::Position::new(0, 5), lsp::Position::new(0, 7)),
2141 ),
2142 )))
2143 },
2144 );
2145
2146 let type_definitions = project_b
2147 .update(cx_b, |p, cx| p.type_definition(&buffer_b, 7, cx))
2148 .await
2149 .unwrap();
2150 cx_b.read(|cx| {
2151 assert_eq!(type_definitions.len(), 1);
2152 let target_buffer = type_definitions[0].target.buffer.read(cx);
2153 assert_eq!(target_buffer.text(), "type T2 = usize;");
2154 assert_eq!(
2155 type_definitions[0].target.range.to_point(target_buffer),
2156 Point::new(0, 5)..Point::new(0, 7)
2157 );
2158 });
2159}
2160
2161#[gpui::test(iterations = 10)]
2162async fn test_references(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2163 cx_a.foreground().forbid_parking();
2164 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
2165 let client_a = server.create_client(cx_a, "user_a").await;
2166 let client_b = server.create_client(cx_b, "user_b").await;
2167 server
2168 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
2169 .await;
2170
2171 // Set up a fake language server.
2172 let mut language = Language::new(
2173 LanguageConfig {
2174 name: "Rust".into(),
2175 path_suffixes: vec!["rs".to_string()],
2176 ..Default::default()
2177 },
2178 Some(tree_sitter_rust::language()),
2179 );
2180 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
2181 client_a.language_registry.add(Arc::new(language));
2182
2183 client_a
2184 .fs
2185 .insert_tree(
2186 "/root",
2187 json!({
2188 "dir-1": {
2189 "one.rs": "const ONE: usize = 1;",
2190 "two.rs": "const TWO: usize = one::ONE + one::ONE;",
2191 },
2192 "dir-2": {
2193 "three.rs": "const THREE: usize = two::TWO + one::ONE;",
2194 }
2195 }),
2196 )
2197 .await;
2198 let (project_a, worktree_id) = client_a.build_local_project("/root/dir-1", cx_a).await;
2199 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
2200
2201 // Open the file on client B.
2202 let buffer_b = cx_b
2203 .background()
2204 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx)))
2205 .await
2206 .unwrap();
2207
2208 // Request references to a symbol as the guest.
2209 let fake_language_server = fake_language_servers.next().await.unwrap();
2210 fake_language_server.handle_request::<lsp::request::References, _, _>(|params, _| async move {
2211 assert_eq!(
2212 params.text_document_position.text_document.uri.as_str(),
2213 "file:///root/dir-1/one.rs"
2214 );
2215 Ok(Some(vec![
2216 lsp::Location {
2217 uri: lsp::Url::from_file_path("/root/dir-1/two.rs").unwrap(),
2218 range: lsp::Range::new(lsp::Position::new(0, 24), lsp::Position::new(0, 27)),
2219 },
2220 lsp::Location {
2221 uri: lsp::Url::from_file_path("/root/dir-1/two.rs").unwrap(),
2222 range: lsp::Range::new(lsp::Position::new(0, 35), lsp::Position::new(0, 38)),
2223 },
2224 lsp::Location {
2225 uri: lsp::Url::from_file_path("/root/dir-2/three.rs").unwrap(),
2226 range: lsp::Range::new(lsp::Position::new(0, 37), lsp::Position::new(0, 40)),
2227 },
2228 ]))
2229 });
2230
2231 let references = project_b
2232 .update(cx_b, |p, cx| p.references(&buffer_b, 7, cx))
2233 .await
2234 .unwrap();
2235 cx_b.read(|cx| {
2236 assert_eq!(references.len(), 3);
2237 assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
2238
2239 let two_buffer = references[0].buffer.read(cx);
2240 let three_buffer = references[2].buffer.read(cx);
2241 assert_eq!(
2242 two_buffer.file().unwrap().path().as_ref(),
2243 Path::new("two.rs")
2244 );
2245 assert_eq!(references[1].buffer, references[0].buffer);
2246 assert_eq!(
2247 three_buffer.file().unwrap().full_path(cx),
2248 Path::new("three.rs")
2249 );
2250
2251 assert_eq!(references[0].range.to_offset(two_buffer), 24..27);
2252 assert_eq!(references[1].range.to_offset(two_buffer), 35..38);
2253 assert_eq!(references[2].range.to_offset(three_buffer), 37..40);
2254 });
2255}
2256
2257#[gpui::test(iterations = 10)]
2258async fn test_project_search(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2259 cx_a.foreground().forbid_parking();
2260 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
2261 let client_a = server.create_client(cx_a, "user_a").await;
2262 let client_b = server.create_client(cx_b, "user_b").await;
2263 server
2264 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
2265 .await;
2266
2267 client_a
2268 .fs
2269 .insert_tree(
2270 "/root",
2271 json!({
2272 "dir-1": {
2273 "a": "hello world",
2274 "b": "goodnight moon",
2275 "c": "a world of goo",
2276 "d": "world champion of clown world",
2277 },
2278 "dir-2": {
2279 "e": "disney world is fun",
2280 }
2281 }),
2282 )
2283 .await;
2284 let (project_a, _) = client_a.build_local_project("/root/dir-1", cx_a).await;
2285 let (worktree_2, _) = project_a
2286 .update(cx_a, |p, cx| {
2287 p.find_or_create_local_worktree("/root/dir-2", true, cx)
2288 })
2289 .await
2290 .unwrap();
2291 worktree_2
2292 .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
2293 .await;
2294
2295 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
2296
2297 // Perform a search as the guest.
2298 let results = project_b
2299 .update(cx_b, |project, cx| {
2300 project.search(SearchQuery::text("world", false, false), cx)
2301 })
2302 .await
2303 .unwrap();
2304
2305 let mut ranges_by_path = results
2306 .into_iter()
2307 .map(|(buffer, ranges)| {
2308 buffer.read_with(cx_b, |buffer, cx| {
2309 let path = buffer.file().unwrap().full_path(cx);
2310 let offset_ranges = ranges
2311 .into_iter()
2312 .map(|range| range.to_offset(buffer))
2313 .collect::<Vec<_>>();
2314 (path, offset_ranges)
2315 })
2316 })
2317 .collect::<Vec<_>>();
2318 ranges_by_path.sort_by_key(|(path, _)| path.clone());
2319
2320 assert_eq!(
2321 ranges_by_path,
2322 &[
2323 (PathBuf::from("dir-1/a"), vec![6..11]),
2324 (PathBuf::from("dir-1/c"), vec![2..7]),
2325 (PathBuf::from("dir-1/d"), vec![0..5, 24..29]),
2326 (PathBuf::from("dir-2/e"), vec![7..12]),
2327 ]
2328 );
2329}
2330
2331#[gpui::test(iterations = 10)]
2332async fn test_document_highlights(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2333 cx_a.foreground().forbid_parking();
2334 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
2335 let client_a = server.create_client(cx_a, "user_a").await;
2336 let client_b = server.create_client(cx_b, "user_b").await;
2337 server
2338 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
2339 .await;
2340
2341 client_a
2342 .fs
2343 .insert_tree(
2344 "/root-1",
2345 json!({
2346 "main.rs": "fn double(number: i32) -> i32 { number + number }",
2347 }),
2348 )
2349 .await;
2350
2351 // Set up a fake language server.
2352 let mut language = Language::new(
2353 LanguageConfig {
2354 name: "Rust".into(),
2355 path_suffixes: vec!["rs".to_string()],
2356 ..Default::default()
2357 },
2358 Some(tree_sitter_rust::language()),
2359 );
2360 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
2361 client_a.language_registry.add(Arc::new(language));
2362
2363 let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
2364 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
2365
2366 // Open the file on client B.
2367 let buffer_b = cx_b
2368 .background()
2369 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)))
2370 .await
2371 .unwrap();
2372
2373 // Request document highlights as the guest.
2374 let fake_language_server = fake_language_servers.next().await.unwrap();
2375 fake_language_server.handle_request::<lsp::request::DocumentHighlightRequest, _, _>(
2376 |params, _| async move {
2377 assert_eq!(
2378 params
2379 .text_document_position_params
2380 .text_document
2381 .uri
2382 .as_str(),
2383 "file:///root-1/main.rs"
2384 );
2385 assert_eq!(
2386 params.text_document_position_params.position,
2387 lsp::Position::new(0, 34)
2388 );
2389 Ok(Some(vec![
2390 lsp::DocumentHighlight {
2391 kind: Some(lsp::DocumentHighlightKind::WRITE),
2392 range: lsp::Range::new(lsp::Position::new(0, 10), lsp::Position::new(0, 16)),
2393 },
2394 lsp::DocumentHighlight {
2395 kind: Some(lsp::DocumentHighlightKind::READ),
2396 range: lsp::Range::new(lsp::Position::new(0, 32), lsp::Position::new(0, 38)),
2397 },
2398 lsp::DocumentHighlight {
2399 kind: Some(lsp::DocumentHighlightKind::READ),
2400 range: lsp::Range::new(lsp::Position::new(0, 41), lsp::Position::new(0, 47)),
2401 },
2402 ]))
2403 },
2404 );
2405
2406 let highlights = project_b
2407 .update(cx_b, |p, cx| p.document_highlights(&buffer_b, 34, cx))
2408 .await
2409 .unwrap();
2410 buffer_b.read_with(cx_b, |buffer, _| {
2411 let snapshot = buffer.snapshot();
2412
2413 let highlights = highlights
2414 .into_iter()
2415 .map(|highlight| (highlight.kind, highlight.range.to_offset(&snapshot)))
2416 .collect::<Vec<_>>();
2417 assert_eq!(
2418 highlights,
2419 &[
2420 (lsp::DocumentHighlightKind::WRITE, 10..16),
2421 (lsp::DocumentHighlightKind::READ, 32..38),
2422 (lsp::DocumentHighlightKind::READ, 41..47)
2423 ]
2424 )
2425 });
2426}
2427
2428#[gpui::test(iterations = 10)]
2429async fn test_lsp_hover(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2430 cx_a.foreground().forbid_parking();
2431 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
2432 let client_a = server.create_client(cx_a, "user_a").await;
2433 let client_b = server.create_client(cx_b, "user_b").await;
2434 server
2435 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
2436 .await;
2437
2438 client_a
2439 .fs
2440 .insert_tree(
2441 "/root-1",
2442 json!({
2443 "main.rs": "use std::collections::HashMap;",
2444 }),
2445 )
2446 .await;
2447
2448 // Set up a fake language server.
2449 let mut language = Language::new(
2450 LanguageConfig {
2451 name: "Rust".into(),
2452 path_suffixes: vec!["rs".to_string()],
2453 ..Default::default()
2454 },
2455 Some(tree_sitter_rust::language()),
2456 );
2457 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
2458 client_a.language_registry.add(Arc::new(language));
2459
2460 let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
2461 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
2462
2463 // Open the file as the guest
2464 let buffer_b = cx_b
2465 .background()
2466 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)))
2467 .await
2468 .unwrap();
2469
2470 // Request hover information as the guest.
2471 let fake_language_server = fake_language_servers.next().await.unwrap();
2472 fake_language_server.handle_request::<lsp::request::HoverRequest, _, _>(
2473 |params, _| async move {
2474 assert_eq!(
2475 params
2476 .text_document_position_params
2477 .text_document
2478 .uri
2479 .as_str(),
2480 "file:///root-1/main.rs"
2481 );
2482 assert_eq!(
2483 params.text_document_position_params.position,
2484 lsp::Position::new(0, 22)
2485 );
2486 Ok(Some(lsp::Hover {
2487 contents: lsp::HoverContents::Array(vec![
2488 lsp::MarkedString::String("Test hover content.".to_string()),
2489 lsp::MarkedString::LanguageString(lsp::LanguageString {
2490 language: "Rust".to_string(),
2491 value: "let foo = 42;".to_string(),
2492 }),
2493 ]),
2494 range: Some(lsp::Range::new(
2495 lsp::Position::new(0, 22),
2496 lsp::Position::new(0, 29),
2497 )),
2498 }))
2499 },
2500 );
2501
2502 let hover_info = project_b
2503 .update(cx_b, |p, cx| p.hover(&buffer_b, 22, cx))
2504 .await
2505 .unwrap()
2506 .unwrap();
2507 buffer_b.read_with(cx_b, |buffer, _| {
2508 let snapshot = buffer.snapshot();
2509 assert_eq!(hover_info.range.unwrap().to_offset(&snapshot), 22..29);
2510 assert_eq!(
2511 hover_info.contents,
2512 vec![
2513 project::HoverBlock {
2514 text: "Test hover content.".to_string(),
2515 language: None,
2516 },
2517 project::HoverBlock {
2518 text: "let foo = 42;".to_string(),
2519 language: Some("Rust".to_string()),
2520 }
2521 ]
2522 );
2523 });
2524}
2525
2526#[gpui::test(iterations = 10)]
2527async fn test_project_symbols(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2528 cx_a.foreground().forbid_parking();
2529 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
2530 let client_a = server.create_client(cx_a, "user_a").await;
2531 let client_b = server.create_client(cx_b, "user_b").await;
2532 server
2533 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
2534 .await;
2535
2536 // Set up a fake language server.
2537 let mut language = Language::new(
2538 LanguageConfig {
2539 name: "Rust".into(),
2540 path_suffixes: vec!["rs".to_string()],
2541 ..Default::default()
2542 },
2543 Some(tree_sitter_rust::language()),
2544 );
2545 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
2546 client_a.language_registry.add(Arc::new(language));
2547
2548 client_a
2549 .fs
2550 .insert_tree(
2551 "/code",
2552 json!({
2553 "crate-1": {
2554 "one.rs": "const ONE: usize = 1;",
2555 },
2556 "crate-2": {
2557 "two.rs": "const TWO: usize = 2; const THREE: usize = 3;",
2558 },
2559 "private": {
2560 "passwords.txt": "the-password",
2561 }
2562 }),
2563 )
2564 .await;
2565 let (project_a, worktree_id) = client_a.build_local_project("/code/crate-1", cx_a).await;
2566 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
2567
2568 // Cause the language server to start.
2569 let _buffer = cx_b
2570 .background()
2571 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx)))
2572 .await
2573 .unwrap();
2574
2575 let fake_language_server = fake_language_servers.next().await.unwrap();
2576 fake_language_server.handle_request::<lsp::request::WorkspaceSymbol, _, _>(|_, _| async move {
2577 #[allow(deprecated)]
2578 Ok(Some(vec![lsp::SymbolInformation {
2579 name: "TWO".into(),
2580 location: lsp::Location {
2581 uri: lsp::Url::from_file_path("/code/crate-2/two.rs").unwrap(),
2582 range: lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
2583 },
2584 kind: lsp::SymbolKind::CONSTANT,
2585 tags: None,
2586 container_name: None,
2587 deprecated: None,
2588 }]))
2589 });
2590
2591 // Request the definition of a symbol as the guest.
2592 let symbols = project_b
2593 .update(cx_b, |p, cx| p.symbols("two", cx))
2594 .await
2595 .unwrap();
2596 assert_eq!(symbols.len(), 1);
2597 assert_eq!(symbols[0].name, "TWO");
2598
2599 // Open one of the returned symbols.
2600 let buffer_b_2 = project_b
2601 .update(cx_b, |project, cx| {
2602 project.open_buffer_for_symbol(&symbols[0], cx)
2603 })
2604 .await
2605 .unwrap();
2606 buffer_b_2.read_with(cx_b, |buffer, _| {
2607 assert_eq!(
2608 buffer.file().unwrap().path().as_ref(),
2609 Path::new("../crate-2/two.rs")
2610 );
2611 });
2612
2613 // Attempt to craft a symbol and violate host's privacy by opening an arbitrary file.
2614 let mut fake_symbol = symbols[0].clone();
2615 fake_symbol.path.path = Path::new("/code/secrets").into();
2616 let error = project_b
2617 .update(cx_b, |project, cx| {
2618 project.open_buffer_for_symbol(&fake_symbol, cx)
2619 })
2620 .await
2621 .unwrap_err();
2622 assert!(error.to_string().contains("invalid symbol signature"));
2623}
2624
2625#[gpui::test(iterations = 10)]
2626async fn test_open_buffer_while_getting_definition_pointing_to_it(
2627 cx_a: &mut TestAppContext,
2628 cx_b: &mut TestAppContext,
2629 mut rng: StdRng,
2630) {
2631 cx_a.foreground().forbid_parking();
2632 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
2633 let client_a = server.create_client(cx_a, "user_a").await;
2634 let client_b = server.create_client(cx_b, "user_b").await;
2635 server
2636 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
2637 .await;
2638
2639 // Set up a fake language server.
2640 let mut language = Language::new(
2641 LanguageConfig {
2642 name: "Rust".into(),
2643 path_suffixes: vec!["rs".to_string()],
2644 ..Default::default()
2645 },
2646 Some(tree_sitter_rust::language()),
2647 );
2648 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
2649 client_a.language_registry.add(Arc::new(language));
2650
2651 client_a
2652 .fs
2653 .insert_tree(
2654 "/root",
2655 json!({
2656 "a.rs": "const ONE: usize = b::TWO;",
2657 "b.rs": "const TWO: usize = 2",
2658 }),
2659 )
2660 .await;
2661 let (project_a, worktree_id) = client_a.build_local_project("/root", cx_a).await;
2662 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
2663
2664 let buffer_b1 = cx_b
2665 .background()
2666 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
2667 .await
2668 .unwrap();
2669
2670 let fake_language_server = fake_language_servers.next().await.unwrap();
2671 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
2672 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
2673 lsp::Location::new(
2674 lsp::Url::from_file_path("/root/b.rs").unwrap(),
2675 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
2676 ),
2677 )))
2678 });
2679
2680 let definitions;
2681 let buffer_b2;
2682 if rng.gen() {
2683 definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
2684 buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx));
2685 } else {
2686 buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx));
2687 definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
2688 }
2689
2690 let buffer_b2 = buffer_b2.await.unwrap();
2691 let definitions = definitions.await.unwrap();
2692 assert_eq!(definitions.len(), 1);
2693 assert_eq!(definitions[0].target.buffer, buffer_b2);
2694}
2695
2696#[gpui::test(iterations = 10)]
2697async fn test_collaborating_with_code_actions(
2698 cx_a: &mut TestAppContext,
2699 cx_b: &mut TestAppContext,
2700) {
2701 cx_a.foreground().forbid_parking();
2702 cx_b.update(editor::init);
2703 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
2704 let client_a = server.create_client(cx_a, "user_a").await;
2705 let client_b = server.create_client(cx_b, "user_b").await;
2706 server
2707 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
2708 .await;
2709
2710 // Set up a fake language server.
2711 let mut language = Language::new(
2712 LanguageConfig {
2713 name: "Rust".into(),
2714 path_suffixes: vec!["rs".to_string()],
2715 ..Default::default()
2716 },
2717 Some(tree_sitter_rust::language()),
2718 );
2719 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
2720 client_a.language_registry.add(Arc::new(language));
2721
2722 client_a
2723 .fs
2724 .insert_tree(
2725 "/a",
2726 json!({
2727 "main.rs": "mod other;\nfn main() { let foo = other::foo(); }",
2728 "other.rs": "pub fn foo() -> usize { 4 }",
2729 }),
2730 )
2731 .await;
2732 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
2733
2734 // Join the project as client B.
2735 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
2736 let (_window_b, workspace_b) =
2737 cx_b.add_window(|cx| Workspace::new(project_b.clone(), |_, _| unimplemented!(), cx));
2738 let editor_b = workspace_b
2739 .update(cx_b, |workspace, cx| {
2740 workspace.open_path((worktree_id, "main.rs"), true, cx)
2741 })
2742 .await
2743 .unwrap()
2744 .downcast::<Editor>()
2745 .unwrap();
2746
2747 let mut fake_language_server = fake_language_servers.next().await.unwrap();
2748 fake_language_server
2749 .handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
2750 assert_eq!(
2751 params.text_document.uri,
2752 lsp::Url::from_file_path("/a/main.rs").unwrap(),
2753 );
2754 assert_eq!(params.range.start, lsp::Position::new(0, 0));
2755 assert_eq!(params.range.end, lsp::Position::new(0, 0));
2756 Ok(None)
2757 })
2758 .next()
2759 .await;
2760
2761 // Move cursor to a location that contains code actions.
2762 editor_b.update(cx_b, |editor, cx| {
2763 editor.change_selections(None, cx, |s| {
2764 s.select_ranges([Point::new(1, 31)..Point::new(1, 31)])
2765 });
2766 cx.focus(&editor_b);
2767 });
2768
2769 fake_language_server
2770 .handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
2771 assert_eq!(
2772 params.text_document.uri,
2773 lsp::Url::from_file_path("/a/main.rs").unwrap(),
2774 );
2775 assert_eq!(params.range.start, lsp::Position::new(1, 31));
2776 assert_eq!(params.range.end, lsp::Position::new(1, 31));
2777
2778 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
2779 lsp::CodeAction {
2780 title: "Inline into all callers".to_string(),
2781 edit: Some(lsp::WorkspaceEdit {
2782 changes: Some(
2783 [
2784 (
2785 lsp::Url::from_file_path("/a/main.rs").unwrap(),
2786 vec![lsp::TextEdit::new(
2787 lsp::Range::new(
2788 lsp::Position::new(1, 22),
2789 lsp::Position::new(1, 34),
2790 ),
2791 "4".to_string(),
2792 )],
2793 ),
2794 (
2795 lsp::Url::from_file_path("/a/other.rs").unwrap(),
2796 vec![lsp::TextEdit::new(
2797 lsp::Range::new(
2798 lsp::Position::new(0, 0),
2799 lsp::Position::new(0, 27),
2800 ),
2801 "".to_string(),
2802 )],
2803 ),
2804 ]
2805 .into_iter()
2806 .collect(),
2807 ),
2808 ..Default::default()
2809 }),
2810 data: Some(json!({
2811 "codeActionParams": {
2812 "range": {
2813 "start": {"line": 1, "column": 31},
2814 "end": {"line": 1, "column": 31},
2815 }
2816 }
2817 })),
2818 ..Default::default()
2819 },
2820 )]))
2821 })
2822 .next()
2823 .await;
2824
2825 // Toggle code actions and wait for them to display.
2826 editor_b.update(cx_b, |editor, cx| {
2827 editor.toggle_code_actions(
2828 &ToggleCodeActions {
2829 deployed_from_indicator: false,
2830 },
2831 cx,
2832 );
2833 });
2834 editor_b
2835 .condition(cx_b, |editor, _| editor.context_menu_visible())
2836 .await;
2837
2838 fake_language_server.remove_request_handler::<lsp::request::CodeActionRequest>();
2839
2840 // Confirming the code action will trigger a resolve request.
2841 let confirm_action = workspace_b
2842 .update(cx_b, |workspace, cx| {
2843 Editor::confirm_code_action(workspace, &ConfirmCodeAction { item_ix: Some(0) }, cx)
2844 })
2845 .unwrap();
2846 fake_language_server.handle_request::<lsp::request::CodeActionResolveRequest, _, _>(
2847 |_, _| async move {
2848 Ok(lsp::CodeAction {
2849 title: "Inline into all callers".to_string(),
2850 edit: Some(lsp::WorkspaceEdit {
2851 changes: Some(
2852 [
2853 (
2854 lsp::Url::from_file_path("/a/main.rs").unwrap(),
2855 vec![lsp::TextEdit::new(
2856 lsp::Range::new(
2857 lsp::Position::new(1, 22),
2858 lsp::Position::new(1, 34),
2859 ),
2860 "4".to_string(),
2861 )],
2862 ),
2863 (
2864 lsp::Url::from_file_path("/a/other.rs").unwrap(),
2865 vec![lsp::TextEdit::new(
2866 lsp::Range::new(
2867 lsp::Position::new(0, 0),
2868 lsp::Position::new(0, 27),
2869 ),
2870 "".to_string(),
2871 )],
2872 ),
2873 ]
2874 .into_iter()
2875 .collect(),
2876 ),
2877 ..Default::default()
2878 }),
2879 ..Default::default()
2880 })
2881 },
2882 );
2883
2884 // After the action is confirmed, an editor containing both modified files is opened.
2885 confirm_action.await.unwrap();
2886 let code_action_editor = workspace_b.read_with(cx_b, |workspace, cx| {
2887 workspace
2888 .active_item(cx)
2889 .unwrap()
2890 .downcast::<Editor>()
2891 .unwrap()
2892 });
2893 code_action_editor.update(cx_b, |editor, cx| {
2894 assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
2895 editor.undo(&Undo, cx);
2896 assert_eq!(
2897 editor.text(cx),
2898 "mod other;\nfn main() { let foo = other::foo(); }\npub fn foo() -> usize { 4 }"
2899 );
2900 editor.redo(&Redo, cx);
2901 assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
2902 });
2903}
2904
2905#[gpui::test(iterations = 10)]
2906async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2907 cx_a.foreground().forbid_parking();
2908 cx_b.update(editor::init);
2909 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
2910 let client_a = server.create_client(cx_a, "user_a").await;
2911 let client_b = server.create_client(cx_b, "user_b").await;
2912 server
2913 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
2914 .await;
2915
2916 // Set up a fake language server.
2917 let mut language = Language::new(
2918 LanguageConfig {
2919 name: "Rust".into(),
2920 path_suffixes: vec!["rs".to_string()],
2921 ..Default::default()
2922 },
2923 Some(tree_sitter_rust::language()),
2924 );
2925 let mut fake_language_servers = language
2926 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
2927 capabilities: lsp::ServerCapabilities {
2928 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
2929 prepare_provider: Some(true),
2930 work_done_progress_options: Default::default(),
2931 })),
2932 ..Default::default()
2933 },
2934 ..Default::default()
2935 }))
2936 .await;
2937 client_a.language_registry.add(Arc::new(language));
2938
2939 client_a
2940 .fs
2941 .insert_tree(
2942 "/dir",
2943 json!({
2944 "one.rs": "const ONE: usize = 1;",
2945 "two.rs": "const TWO: usize = one::ONE + one::ONE;"
2946 }),
2947 )
2948 .await;
2949 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
2950 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
2951
2952 let (_window_b, workspace_b) =
2953 cx_b.add_window(|cx| Workspace::new(project_b.clone(), |_, _| unimplemented!(), cx));
2954 let editor_b = workspace_b
2955 .update(cx_b, |workspace, cx| {
2956 workspace.open_path((worktree_id, "one.rs"), true, cx)
2957 })
2958 .await
2959 .unwrap()
2960 .downcast::<Editor>()
2961 .unwrap();
2962 let fake_language_server = fake_language_servers.next().await.unwrap();
2963
2964 // Move cursor to a location that can be renamed.
2965 let prepare_rename = editor_b.update(cx_b, |editor, cx| {
2966 editor.change_selections(None, cx, |s| s.select_ranges([7..7]));
2967 editor.rename(&Rename, cx).unwrap()
2968 });
2969
2970 fake_language_server
2971 .handle_request::<lsp::request::PrepareRenameRequest, _, _>(|params, _| async move {
2972 assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs");
2973 assert_eq!(params.position, lsp::Position::new(0, 7));
2974 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
2975 lsp::Position::new(0, 6),
2976 lsp::Position::new(0, 9),
2977 ))))
2978 })
2979 .next()
2980 .await
2981 .unwrap();
2982 prepare_rename.await.unwrap();
2983 editor_b.update(cx_b, |editor, cx| {
2984 let rename = editor.pending_rename().unwrap();
2985 let buffer = editor.buffer().read(cx).snapshot(cx);
2986 assert_eq!(
2987 rename.range.start.to_offset(&buffer)..rename.range.end.to_offset(&buffer),
2988 6..9
2989 );
2990 rename.editor.update(cx, |rename_editor, cx| {
2991 rename_editor.buffer().update(cx, |rename_buffer, cx| {
2992 rename_buffer.edit([(0..3, "THREE")], None, cx);
2993 });
2994 });
2995 });
2996
2997 let confirm_rename = workspace_b.update(cx_b, |workspace, cx| {
2998 Editor::confirm_rename(workspace, &ConfirmRename, cx).unwrap()
2999 });
3000 fake_language_server
3001 .handle_request::<lsp::request::Rename, _, _>(|params, _| async move {
3002 assert_eq!(
3003 params.text_document_position.text_document.uri.as_str(),
3004 "file:///dir/one.rs"
3005 );
3006 assert_eq!(
3007 params.text_document_position.position,
3008 lsp::Position::new(0, 6)
3009 );
3010 assert_eq!(params.new_name, "THREE");
3011 Ok(Some(lsp::WorkspaceEdit {
3012 changes: Some(
3013 [
3014 (
3015 lsp::Url::from_file_path("/dir/one.rs").unwrap(),
3016 vec![lsp::TextEdit::new(
3017 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
3018 "THREE".to_string(),
3019 )],
3020 ),
3021 (
3022 lsp::Url::from_file_path("/dir/two.rs").unwrap(),
3023 vec![
3024 lsp::TextEdit::new(
3025 lsp::Range::new(
3026 lsp::Position::new(0, 24),
3027 lsp::Position::new(0, 27),
3028 ),
3029 "THREE".to_string(),
3030 ),
3031 lsp::TextEdit::new(
3032 lsp::Range::new(
3033 lsp::Position::new(0, 35),
3034 lsp::Position::new(0, 38),
3035 ),
3036 "THREE".to_string(),
3037 ),
3038 ],
3039 ),
3040 ]
3041 .into_iter()
3042 .collect(),
3043 ),
3044 ..Default::default()
3045 }))
3046 })
3047 .next()
3048 .await
3049 .unwrap();
3050 confirm_rename.await.unwrap();
3051
3052 let rename_editor = workspace_b.read_with(cx_b, |workspace, cx| {
3053 workspace
3054 .active_item(cx)
3055 .unwrap()
3056 .downcast::<Editor>()
3057 .unwrap()
3058 });
3059 rename_editor.update(cx_b, |editor, cx| {
3060 assert_eq!(
3061 editor.text(cx),
3062 "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
3063 );
3064 editor.undo(&Undo, cx);
3065 assert_eq!(
3066 editor.text(cx),
3067 "const ONE: usize = 1;\nconst TWO: usize = one::ONE + one::ONE;"
3068 );
3069 editor.redo(&Redo, cx);
3070 assert_eq!(
3071 editor.text(cx),
3072 "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
3073 );
3074 });
3075
3076 // Ensure temporary rename edits cannot be undone/redone.
3077 editor_b.update(cx_b, |editor, cx| {
3078 editor.undo(&Undo, cx);
3079 assert_eq!(editor.text(cx), "const ONE: usize = 1;");
3080 editor.undo(&Undo, cx);
3081 assert_eq!(editor.text(cx), "const ONE: usize = 1;");
3082 editor.redo(&Redo, cx);
3083 assert_eq!(editor.text(cx), "const THREE: usize = 1;");
3084 })
3085}
3086
3087#[gpui::test(iterations = 10)]
3088async fn test_language_server_statuses(
3089 deterministic: Arc<Deterministic>,
3090 cx_a: &mut TestAppContext,
3091 cx_b: &mut TestAppContext,
3092) {
3093 deterministic.forbid_parking();
3094
3095 cx_b.update(editor::init);
3096 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
3097 let client_a = server.create_client(cx_a, "user_a").await;
3098 let client_b = server.create_client(cx_b, "user_b").await;
3099 server
3100 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
3101 .await;
3102
3103 // Set up a fake language server.
3104 let mut language = Language::new(
3105 LanguageConfig {
3106 name: "Rust".into(),
3107 path_suffixes: vec!["rs".to_string()],
3108 ..Default::default()
3109 },
3110 Some(tree_sitter_rust::language()),
3111 );
3112 let mut fake_language_servers = language
3113 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
3114 name: "the-language-server",
3115 ..Default::default()
3116 }))
3117 .await;
3118 client_a.language_registry.add(Arc::new(language));
3119
3120 client_a
3121 .fs
3122 .insert_tree(
3123 "/dir",
3124 json!({
3125 "main.rs": "const ONE: usize = 1;",
3126 }),
3127 )
3128 .await;
3129 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3130
3131 let _buffer_a = project_a
3132 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
3133 .await
3134 .unwrap();
3135
3136 let fake_language_server = fake_language_servers.next().await.unwrap();
3137 fake_language_server.start_progress("the-token").await;
3138 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
3139 token: lsp::NumberOrString::String("the-token".to_string()),
3140 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
3141 lsp::WorkDoneProgressReport {
3142 message: Some("the-message".to_string()),
3143 ..Default::default()
3144 },
3145 )),
3146 });
3147 deterministic.run_until_parked();
3148 project_a.read_with(cx_a, |project, _| {
3149 let status = project.language_server_statuses().next().unwrap();
3150 assert_eq!(status.name, "the-language-server");
3151 assert_eq!(status.pending_work.len(), 1);
3152 assert_eq!(
3153 status.pending_work["the-token"].message.as_ref().unwrap(),
3154 "the-message"
3155 );
3156 });
3157
3158 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
3159 project_b.read_with(cx_b, |project, _| {
3160 let status = project.language_server_statuses().next().unwrap();
3161 assert_eq!(status.name, "the-language-server");
3162 });
3163
3164 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
3165 token: lsp::NumberOrString::String("the-token".to_string()),
3166 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
3167 lsp::WorkDoneProgressReport {
3168 message: Some("the-message-2".to_string()),
3169 ..Default::default()
3170 },
3171 )),
3172 });
3173 deterministic.run_until_parked();
3174 project_a.read_with(cx_a, |project, _| {
3175 let status = project.language_server_statuses().next().unwrap();
3176 assert_eq!(status.name, "the-language-server");
3177 assert_eq!(status.pending_work.len(), 1);
3178 assert_eq!(
3179 status.pending_work["the-token"].message.as_ref().unwrap(),
3180 "the-message-2"
3181 );
3182 });
3183 project_b.read_with(cx_b, |project, _| {
3184 let status = project.language_server_statuses().next().unwrap();
3185 assert_eq!(status.name, "the-language-server");
3186 assert_eq!(status.pending_work.len(), 1);
3187 assert_eq!(
3188 status.pending_work["the-token"].message.as_ref().unwrap(),
3189 "the-message-2"
3190 );
3191 });
3192}
3193
3194#[gpui::test(iterations = 10)]
3195async fn test_basic_chat(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
3196 cx_a.foreground().forbid_parking();
3197 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
3198 let client_a = server.create_client(cx_a, "user_a").await;
3199 let client_b = server.create_client(cx_b, "user_b").await;
3200
3201 // Create an org that includes these 2 users.
3202 let db = &server.app_state.db;
3203 let org_id = db.create_org("Test Org", "test-org").await.unwrap();
3204 db.add_org_member(org_id, client_a.current_user_id(cx_a), false)
3205 .await
3206 .unwrap();
3207 db.add_org_member(org_id, client_b.current_user_id(cx_b), false)
3208 .await
3209 .unwrap();
3210
3211 // Create a channel that includes all the users.
3212 let channel_id = db.create_org_channel(org_id, "test-channel").await.unwrap();
3213 db.add_channel_member(channel_id, client_a.current_user_id(cx_a), false)
3214 .await
3215 .unwrap();
3216 db.add_channel_member(channel_id, client_b.current_user_id(cx_b), false)
3217 .await
3218 .unwrap();
3219 db.create_channel_message(
3220 channel_id,
3221 client_b.current_user_id(cx_b),
3222 "hello A, it's B.",
3223 OffsetDateTime::now_utc(),
3224 1,
3225 )
3226 .await
3227 .unwrap();
3228
3229 let channels_a =
3230 cx_a.add_model(|cx| ChannelList::new(client_a.user_store.clone(), client_a.clone(), cx));
3231 channels_a
3232 .condition(cx_a, |list, _| list.available_channels().is_some())
3233 .await;
3234 channels_a.read_with(cx_a, |list, _| {
3235 assert_eq!(
3236 list.available_channels().unwrap(),
3237 &[ChannelDetails {
3238 id: channel_id.to_proto(),
3239 name: "test-channel".to_string()
3240 }]
3241 )
3242 });
3243 let channel_a = channels_a.update(cx_a, |this, cx| {
3244 this.get_channel(channel_id.to_proto(), cx).unwrap()
3245 });
3246 channel_a.read_with(cx_a, |channel, _| assert!(channel.messages().is_empty()));
3247 channel_a
3248 .condition(cx_a, |channel, _| {
3249 channel_messages(channel)
3250 == [("user_b".to_string(), "hello A, it's B.".to_string(), false)]
3251 })
3252 .await;
3253
3254 let channels_b =
3255 cx_b.add_model(|cx| ChannelList::new(client_b.user_store.clone(), client_b.clone(), cx));
3256 channels_b
3257 .condition(cx_b, |list, _| list.available_channels().is_some())
3258 .await;
3259 channels_b.read_with(cx_b, |list, _| {
3260 assert_eq!(
3261 list.available_channels().unwrap(),
3262 &[ChannelDetails {
3263 id: channel_id.to_proto(),
3264 name: "test-channel".to_string()
3265 }]
3266 )
3267 });
3268
3269 let channel_b = channels_b.update(cx_b, |this, cx| {
3270 this.get_channel(channel_id.to_proto(), cx).unwrap()
3271 });
3272 channel_b.read_with(cx_b, |channel, _| assert!(channel.messages().is_empty()));
3273 channel_b
3274 .condition(cx_b, |channel, _| {
3275 channel_messages(channel)
3276 == [("user_b".to_string(), "hello A, it's B.".to_string(), false)]
3277 })
3278 .await;
3279
3280 channel_a
3281 .update(cx_a, |channel, cx| {
3282 channel
3283 .send_message("oh, hi B.".to_string(), cx)
3284 .unwrap()
3285 .detach();
3286 let task = channel.send_message("sup".to_string(), cx).unwrap();
3287 assert_eq!(
3288 channel_messages(channel),
3289 &[
3290 ("user_b".to_string(), "hello A, it's B.".to_string(), false),
3291 ("user_a".to_string(), "oh, hi B.".to_string(), true),
3292 ("user_a".to_string(), "sup".to_string(), true)
3293 ]
3294 );
3295 task
3296 })
3297 .await
3298 .unwrap();
3299
3300 channel_b
3301 .condition(cx_b, |channel, _| {
3302 channel_messages(channel)
3303 == [
3304 ("user_b".to_string(), "hello A, it's B.".to_string(), false),
3305 ("user_a".to_string(), "oh, hi B.".to_string(), false),
3306 ("user_a".to_string(), "sup".to_string(), false),
3307 ]
3308 })
3309 .await;
3310
3311 assert_eq!(
3312 server
3313 .store()
3314 .await
3315 .channel(channel_id)
3316 .unwrap()
3317 .connection_ids
3318 .len(),
3319 2
3320 );
3321 cx_b.update(|_| drop(channel_b));
3322 server
3323 .condition(|state| state.channel(channel_id).unwrap().connection_ids.len() == 1)
3324 .await;
3325
3326 cx_a.update(|_| drop(channel_a));
3327 server
3328 .condition(|state| state.channel(channel_id).is_none())
3329 .await;
3330}
3331
3332#[gpui::test(iterations = 10)]
3333async fn test_chat_message_validation(cx_a: &mut TestAppContext) {
3334 cx_a.foreground().forbid_parking();
3335 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
3336 let client_a = server.create_client(cx_a, "user_a").await;
3337
3338 let db = &server.app_state.db;
3339 let org_id = db.create_org("Test Org", "test-org").await.unwrap();
3340 let channel_id = db.create_org_channel(org_id, "test-channel").await.unwrap();
3341 db.add_org_member(org_id, client_a.current_user_id(cx_a), false)
3342 .await
3343 .unwrap();
3344 db.add_channel_member(channel_id, client_a.current_user_id(cx_a), false)
3345 .await
3346 .unwrap();
3347
3348 let channels_a =
3349 cx_a.add_model(|cx| ChannelList::new(client_a.user_store.clone(), client_a.clone(), cx));
3350 channels_a
3351 .condition(cx_a, |list, _| list.available_channels().is_some())
3352 .await;
3353 let channel_a = channels_a.update(cx_a, |this, cx| {
3354 this.get_channel(channel_id.to_proto(), cx).unwrap()
3355 });
3356
3357 // Messages aren't allowed to be too long.
3358 channel_a
3359 .update(cx_a, |channel, cx| {
3360 let long_body = "this is long.\n".repeat(1024);
3361 channel.send_message(long_body, cx).unwrap()
3362 })
3363 .await
3364 .unwrap_err();
3365
3366 // Messages aren't allowed to be blank.
3367 channel_a.update(cx_a, |channel, cx| {
3368 channel.send_message(String::new(), cx).unwrap_err()
3369 });
3370
3371 // Leading and trailing whitespace are trimmed.
3372 channel_a
3373 .update(cx_a, |channel, cx| {
3374 channel
3375 .send_message("\n surrounded by whitespace \n".to_string(), cx)
3376 .unwrap()
3377 })
3378 .await
3379 .unwrap();
3380 assert_eq!(
3381 db.get_channel_messages(channel_id, 10, None)
3382 .await
3383 .unwrap()
3384 .iter()
3385 .map(|m| &m.body)
3386 .collect::<Vec<_>>(),
3387 &["surrounded by whitespace"]
3388 );
3389}
3390
3391#[gpui::test(iterations = 10)]
3392async fn test_chat_reconnection(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
3393 cx_a.foreground().forbid_parking();
3394 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
3395 let client_a = server.create_client(cx_a, "user_a").await;
3396 let client_b = server.create_client(cx_b, "user_b").await;
3397
3398 let mut status_b = client_b.status();
3399
3400 // Create an org that includes these 2 users.
3401 let db = &server.app_state.db;
3402 let org_id = db.create_org("Test Org", "test-org").await.unwrap();
3403 db.add_org_member(org_id, client_a.current_user_id(cx_a), false)
3404 .await
3405 .unwrap();
3406 db.add_org_member(org_id, client_b.current_user_id(cx_b), false)
3407 .await
3408 .unwrap();
3409
3410 // Create a channel that includes all the users.
3411 let channel_id = db.create_org_channel(org_id, "test-channel").await.unwrap();
3412 db.add_channel_member(channel_id, client_a.current_user_id(cx_a), false)
3413 .await
3414 .unwrap();
3415 db.add_channel_member(channel_id, client_b.current_user_id(cx_b), false)
3416 .await
3417 .unwrap();
3418 db.create_channel_message(
3419 channel_id,
3420 client_b.current_user_id(cx_b),
3421 "hello A, it's B.",
3422 OffsetDateTime::now_utc(),
3423 2,
3424 )
3425 .await
3426 .unwrap();
3427
3428 let channels_a =
3429 cx_a.add_model(|cx| ChannelList::new(client_a.user_store.clone(), client_a.clone(), cx));
3430 channels_a
3431 .condition(cx_a, |list, _| list.available_channels().is_some())
3432 .await;
3433
3434 channels_a.read_with(cx_a, |list, _| {
3435 assert_eq!(
3436 list.available_channels().unwrap(),
3437 &[ChannelDetails {
3438 id: channel_id.to_proto(),
3439 name: "test-channel".to_string()
3440 }]
3441 )
3442 });
3443 let channel_a = channels_a.update(cx_a, |this, cx| {
3444 this.get_channel(channel_id.to_proto(), cx).unwrap()
3445 });
3446 channel_a.read_with(cx_a, |channel, _| assert!(channel.messages().is_empty()));
3447 channel_a
3448 .condition(cx_a, |channel, _| {
3449 channel_messages(channel)
3450 == [("user_b".to_string(), "hello A, it's B.".to_string(), false)]
3451 })
3452 .await;
3453
3454 let channels_b =
3455 cx_b.add_model(|cx| ChannelList::new(client_b.user_store.clone(), client_b.clone(), cx));
3456 channels_b
3457 .condition(cx_b, |list, _| list.available_channels().is_some())
3458 .await;
3459 channels_b.read_with(cx_b, |list, _| {
3460 assert_eq!(
3461 list.available_channels().unwrap(),
3462 &[ChannelDetails {
3463 id: channel_id.to_proto(),
3464 name: "test-channel".to_string()
3465 }]
3466 )
3467 });
3468
3469 let channel_b = channels_b.update(cx_b, |this, cx| {
3470 this.get_channel(channel_id.to_proto(), cx).unwrap()
3471 });
3472 channel_b.read_with(cx_b, |channel, _| assert!(channel.messages().is_empty()));
3473 channel_b
3474 .condition(cx_b, |channel, _| {
3475 channel_messages(channel)
3476 == [("user_b".to_string(), "hello A, it's B.".to_string(), false)]
3477 })
3478 .await;
3479
3480 // Disconnect client B, ensuring we can still access its cached channel data.
3481 server.forbid_connections();
3482 server.disconnect_client(client_b.current_user_id(cx_b));
3483 cx_b.foreground().advance_clock(rpc::RECEIVE_TIMEOUT);
3484 while !matches!(
3485 status_b.next().await,
3486 Some(client::Status::ReconnectionError { .. })
3487 ) {}
3488
3489 channels_b.read_with(cx_b, |channels, _| {
3490 assert_eq!(
3491 channels.available_channels().unwrap(),
3492 [ChannelDetails {
3493 id: channel_id.to_proto(),
3494 name: "test-channel".to_string()
3495 }]
3496 )
3497 });
3498 channel_b.read_with(cx_b, |channel, _| {
3499 assert_eq!(
3500 channel_messages(channel),
3501 [("user_b".to_string(), "hello A, it's B.".to_string(), false)]
3502 )
3503 });
3504
3505 // Send a message from client B while it is disconnected.
3506 channel_b
3507 .update(cx_b, |channel, cx| {
3508 let task = channel
3509 .send_message("can you see this?".to_string(), cx)
3510 .unwrap();
3511 assert_eq!(
3512 channel_messages(channel),
3513 &[
3514 ("user_b".to_string(), "hello A, it's B.".to_string(), false),
3515 ("user_b".to_string(), "can you see this?".to_string(), true)
3516 ]
3517 );
3518 task
3519 })
3520 .await
3521 .unwrap_err();
3522
3523 // Send a message from client A while B is disconnected.
3524 channel_a
3525 .update(cx_a, |channel, cx| {
3526 channel
3527 .send_message("oh, hi B.".to_string(), cx)
3528 .unwrap()
3529 .detach();
3530 let task = channel.send_message("sup".to_string(), cx).unwrap();
3531 assert_eq!(
3532 channel_messages(channel),
3533 &[
3534 ("user_b".to_string(), "hello A, it's B.".to_string(), false),
3535 ("user_a".to_string(), "oh, hi B.".to_string(), true),
3536 ("user_a".to_string(), "sup".to_string(), true)
3537 ]
3538 );
3539 task
3540 })
3541 .await
3542 .unwrap();
3543
3544 // Give client B a chance to reconnect.
3545 server.allow_connections();
3546 cx_b.foreground().advance_clock(Duration::from_secs(10));
3547
3548 // Verify that B sees the new messages upon reconnection, as well as the message client B
3549 // sent while offline.
3550 channel_b
3551 .condition(cx_b, |channel, _| {
3552 channel_messages(channel)
3553 == [
3554 ("user_b".to_string(), "hello A, it's B.".to_string(), false),
3555 ("user_a".to_string(), "oh, hi B.".to_string(), false),
3556 ("user_a".to_string(), "sup".to_string(), false),
3557 ("user_b".to_string(), "can you see this?".to_string(), false),
3558 ]
3559 })
3560 .await;
3561
3562 // Ensure client A and B can communicate normally after reconnection.
3563 channel_a
3564 .update(cx_a, |channel, cx| {
3565 channel.send_message("you online?".to_string(), cx).unwrap()
3566 })
3567 .await
3568 .unwrap();
3569 channel_b
3570 .condition(cx_b, |channel, _| {
3571 channel_messages(channel)
3572 == [
3573 ("user_b".to_string(), "hello A, it's B.".to_string(), false),
3574 ("user_a".to_string(), "oh, hi B.".to_string(), false),
3575 ("user_a".to_string(), "sup".to_string(), false),
3576 ("user_b".to_string(), "can you see this?".to_string(), false),
3577 ("user_a".to_string(), "you online?".to_string(), false),
3578 ]
3579 })
3580 .await;
3581
3582 channel_b
3583 .update(cx_b, |channel, cx| {
3584 channel.send_message("yep".to_string(), cx).unwrap()
3585 })
3586 .await
3587 .unwrap();
3588 channel_a
3589 .condition(cx_a, |channel, _| {
3590 channel_messages(channel)
3591 == [
3592 ("user_b".to_string(), "hello A, it's B.".to_string(), false),
3593 ("user_a".to_string(), "oh, hi B.".to_string(), false),
3594 ("user_a".to_string(), "sup".to_string(), false),
3595 ("user_b".to_string(), "can you see this?".to_string(), false),
3596 ("user_a".to_string(), "you online?".to_string(), false),
3597 ("user_b".to_string(), "yep".to_string(), false),
3598 ]
3599 })
3600 .await;
3601}
3602
3603#[gpui::test(iterations = 10)]
3604async fn test_contacts(
3605 deterministic: Arc<Deterministic>,
3606 cx_a: &mut TestAppContext,
3607 cx_b: &mut TestAppContext,
3608 cx_c: &mut TestAppContext,
3609) {
3610 cx_a.foreground().forbid_parking();
3611 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
3612 let client_a = server.create_client(cx_a, "user_a").await;
3613 let client_b = server.create_client(cx_b, "user_b").await;
3614 let client_c = server.create_client(cx_c, "user_c").await;
3615 server
3616 .make_contacts(vec![
3617 (&client_a, cx_a),
3618 (&client_b, cx_b),
3619 (&client_c, cx_c),
3620 ])
3621 .await;
3622
3623 deterministic.run_until_parked();
3624 assert_eq!(
3625 contacts(&client_a, cx_a),
3626 [("user_b".to_string(), true), ("user_c".to_string(), true)]
3627 );
3628 assert_eq!(
3629 contacts(&client_b, cx_b),
3630 [("user_a".to_string(), true), ("user_c".to_string(), true)]
3631 );
3632 assert_eq!(
3633 contacts(&client_c, cx_c),
3634 [("user_a".to_string(), true), ("user_b".to_string(), true)]
3635 );
3636
3637 // Share a project as client A.
3638 client_a.fs.create_dir(Path::new("/a")).await.unwrap();
3639 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
3640
3641 deterministic.run_until_parked();
3642 assert_eq!(
3643 contacts(&client_a, cx_a),
3644 [("user_b".to_string(), true), ("user_c".to_string(), true)]
3645 );
3646 assert_eq!(
3647 contacts(&client_b, cx_b),
3648 [("user_a".to_string(), true), ("user_c".to_string(), true)]
3649 );
3650 assert_eq!(
3651 contacts(&client_c, cx_c),
3652 [("user_a".to_string(), true), ("user_b".to_string(), true)]
3653 );
3654
3655 let _project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
3656
3657 deterministic.run_until_parked();
3658 assert_eq!(
3659 contacts(&client_a, cx_a),
3660 [("user_b".to_string(), true), ("user_c".to_string(), true)]
3661 );
3662 assert_eq!(
3663 contacts(&client_b, cx_b),
3664 [("user_a".to_string(), true,), ("user_c".to_string(), true)]
3665 );
3666 assert_eq!(
3667 contacts(&client_c, cx_c),
3668 [("user_a".to_string(), true,), ("user_b".to_string(), true)]
3669 );
3670
3671 // Add a local project as client B
3672 client_a.fs.create_dir("/b".as_ref()).await.unwrap();
3673 let (_project_b, _) = client_b.build_local_project("/b", cx_b).await;
3674
3675 deterministic.run_until_parked();
3676 assert_eq!(
3677 contacts(&client_a, cx_a),
3678 [("user_b".to_string(), true), ("user_c".to_string(), true)]
3679 );
3680 assert_eq!(
3681 contacts(&client_b, cx_b),
3682 [("user_a".to_string(), true), ("user_c".to_string(), true)]
3683 );
3684 assert_eq!(
3685 contacts(&client_c, cx_c),
3686 [("user_a".to_string(), true,), ("user_b".to_string(), true)]
3687 );
3688
3689 project_a
3690 .condition(cx_a, |project, _| {
3691 project.collaborators().contains_key(&client_b.peer_id)
3692 })
3693 .await;
3694
3695 cx_a.update(move |_| drop(project_a));
3696 deterministic.run_until_parked();
3697 assert_eq!(
3698 contacts(&client_a, cx_a),
3699 [("user_b".to_string(), true), ("user_c".to_string(), true)]
3700 );
3701 assert_eq!(
3702 contacts(&client_b, cx_b),
3703 [("user_a".to_string(), true), ("user_c".to_string(), true)]
3704 );
3705 assert_eq!(
3706 contacts(&client_c, cx_c),
3707 [("user_a".to_string(), true), ("user_b".to_string(), true)]
3708 );
3709
3710 server.disconnect_client(client_c.current_user_id(cx_c));
3711 server.forbid_connections();
3712 deterministic.advance_clock(rpc::RECEIVE_TIMEOUT);
3713 assert_eq!(
3714 contacts(&client_a, cx_a),
3715 [("user_b".to_string(), true), ("user_c".to_string(), false)]
3716 );
3717 assert_eq!(
3718 contacts(&client_b, cx_b),
3719 [("user_a".to_string(), true), ("user_c".to_string(), false)]
3720 );
3721 assert_eq!(contacts(&client_c, cx_c), []);
3722
3723 server.allow_connections();
3724 client_c
3725 .authenticate_and_connect(false, &cx_c.to_async())
3726 .await
3727 .unwrap();
3728
3729 deterministic.run_until_parked();
3730 assert_eq!(
3731 contacts(&client_a, cx_a),
3732 [("user_b".to_string(), true), ("user_c".to_string(), true)]
3733 );
3734 assert_eq!(
3735 contacts(&client_b, cx_b),
3736 [("user_a".to_string(), true), ("user_c".to_string(), true)]
3737 );
3738 assert_eq!(
3739 contacts(&client_c, cx_c),
3740 [("user_a".to_string(), true), ("user_b".to_string(), true)]
3741 );
3742
3743 #[allow(clippy::type_complexity)]
3744 fn contacts(client: &TestClient, cx: &TestAppContext) -> Vec<(String, bool)> {
3745 client.user_store.read_with(cx, |store, _| {
3746 store
3747 .contacts()
3748 .iter()
3749 .map(|contact| (contact.user.github_login.clone(), contact.online))
3750 .collect()
3751 })
3752 }
3753}
3754
3755#[gpui::test(iterations = 10)]
3756async fn test_contact_requests(
3757 executor: Arc<Deterministic>,
3758 cx_a: &mut TestAppContext,
3759 cx_a2: &mut TestAppContext,
3760 cx_b: &mut TestAppContext,
3761 cx_b2: &mut TestAppContext,
3762 cx_c: &mut TestAppContext,
3763 cx_c2: &mut TestAppContext,
3764) {
3765 cx_a.foreground().forbid_parking();
3766
3767 // Connect to a server as 3 clients.
3768 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
3769 let client_a = server.create_client(cx_a, "user_a").await;
3770 let client_a2 = server.create_client(cx_a2, "user_a").await;
3771 let client_b = server.create_client(cx_b, "user_b").await;
3772 let client_b2 = server.create_client(cx_b2, "user_b").await;
3773 let client_c = server.create_client(cx_c, "user_c").await;
3774 let client_c2 = server.create_client(cx_c2, "user_c").await;
3775
3776 assert_eq!(client_a.user_id().unwrap(), client_a2.user_id().unwrap());
3777 assert_eq!(client_b.user_id().unwrap(), client_b2.user_id().unwrap());
3778 assert_eq!(client_c.user_id().unwrap(), client_c2.user_id().unwrap());
3779
3780 // User A and User C request that user B become their contact.
3781 client_a
3782 .user_store
3783 .update(cx_a, |store, cx| {
3784 store.request_contact(client_b.user_id().unwrap(), cx)
3785 })
3786 .await
3787 .unwrap();
3788 client_c
3789 .user_store
3790 .update(cx_c, |store, cx| {
3791 store.request_contact(client_b.user_id().unwrap(), cx)
3792 })
3793 .await
3794 .unwrap();
3795 executor.run_until_parked();
3796
3797 // All users see the pending request appear in all their clients.
3798 assert_eq!(
3799 client_a.summarize_contacts(cx_a).outgoing_requests,
3800 &["user_b"]
3801 );
3802 assert_eq!(
3803 client_a2.summarize_contacts(cx_a2).outgoing_requests,
3804 &["user_b"]
3805 );
3806 assert_eq!(
3807 client_b.summarize_contacts(cx_b).incoming_requests,
3808 &["user_a", "user_c"]
3809 );
3810 assert_eq!(
3811 client_b2.summarize_contacts(cx_b2).incoming_requests,
3812 &["user_a", "user_c"]
3813 );
3814 assert_eq!(
3815 client_c.summarize_contacts(cx_c).outgoing_requests,
3816 &["user_b"]
3817 );
3818 assert_eq!(
3819 client_c2.summarize_contacts(cx_c2).outgoing_requests,
3820 &["user_b"]
3821 );
3822
3823 // Contact requests are present upon connecting (tested here via disconnect/reconnect)
3824 disconnect_and_reconnect(&client_a, cx_a).await;
3825 disconnect_and_reconnect(&client_b, cx_b).await;
3826 disconnect_and_reconnect(&client_c, cx_c).await;
3827 executor.run_until_parked();
3828 assert_eq!(
3829 client_a.summarize_contacts(cx_a).outgoing_requests,
3830 &["user_b"]
3831 );
3832 assert_eq!(
3833 client_b.summarize_contacts(cx_b).incoming_requests,
3834 &["user_a", "user_c"]
3835 );
3836 assert_eq!(
3837 client_c.summarize_contacts(cx_c).outgoing_requests,
3838 &["user_b"]
3839 );
3840
3841 // User B accepts the request from user A.
3842 client_b
3843 .user_store
3844 .update(cx_b, |store, cx| {
3845 store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx)
3846 })
3847 .await
3848 .unwrap();
3849
3850 executor.run_until_parked();
3851
3852 // User B sees user A as their contact now in all client, and the incoming request from them is removed.
3853 let contacts_b = client_b.summarize_contacts(cx_b);
3854 assert_eq!(contacts_b.current, &["user_a"]);
3855 assert_eq!(contacts_b.incoming_requests, &["user_c"]);
3856 let contacts_b2 = client_b2.summarize_contacts(cx_b2);
3857 assert_eq!(contacts_b2.current, &["user_a"]);
3858 assert_eq!(contacts_b2.incoming_requests, &["user_c"]);
3859
3860 // User A sees user B as their contact now in all clients, and the outgoing request to them is removed.
3861 let contacts_a = client_a.summarize_contacts(cx_a);
3862 assert_eq!(contacts_a.current, &["user_b"]);
3863 assert!(contacts_a.outgoing_requests.is_empty());
3864 let contacts_a2 = client_a2.summarize_contacts(cx_a2);
3865 assert_eq!(contacts_a2.current, &["user_b"]);
3866 assert!(contacts_a2.outgoing_requests.is_empty());
3867
3868 // Contacts are present upon connecting (tested here via disconnect/reconnect)
3869 disconnect_and_reconnect(&client_a, cx_a).await;
3870 disconnect_and_reconnect(&client_b, cx_b).await;
3871 disconnect_and_reconnect(&client_c, cx_c).await;
3872 executor.run_until_parked();
3873 assert_eq!(client_a.summarize_contacts(cx_a).current, &["user_b"]);
3874 assert_eq!(client_b.summarize_contacts(cx_b).current, &["user_a"]);
3875 assert_eq!(
3876 client_b.summarize_contacts(cx_b).incoming_requests,
3877 &["user_c"]
3878 );
3879 assert!(client_c.summarize_contacts(cx_c).current.is_empty());
3880 assert_eq!(
3881 client_c.summarize_contacts(cx_c).outgoing_requests,
3882 &["user_b"]
3883 );
3884
3885 // User B rejects the request from user C.
3886 client_b
3887 .user_store
3888 .update(cx_b, |store, cx| {
3889 store.respond_to_contact_request(client_c.user_id().unwrap(), false, cx)
3890 })
3891 .await
3892 .unwrap();
3893
3894 executor.run_until_parked();
3895
3896 // User B doesn't see user C as their contact, and the incoming request from them is removed.
3897 let contacts_b = client_b.summarize_contacts(cx_b);
3898 assert_eq!(contacts_b.current, &["user_a"]);
3899 assert!(contacts_b.incoming_requests.is_empty());
3900 let contacts_b2 = client_b2.summarize_contacts(cx_b2);
3901 assert_eq!(contacts_b2.current, &["user_a"]);
3902 assert!(contacts_b2.incoming_requests.is_empty());
3903
3904 // User C doesn't see user B as their contact, and the outgoing request to them is removed.
3905 let contacts_c = client_c.summarize_contacts(cx_c);
3906 assert!(contacts_c.current.is_empty());
3907 assert!(contacts_c.outgoing_requests.is_empty());
3908 let contacts_c2 = client_c2.summarize_contacts(cx_c2);
3909 assert!(contacts_c2.current.is_empty());
3910 assert!(contacts_c2.outgoing_requests.is_empty());
3911
3912 // Incoming/outgoing requests are not present upon connecting (tested here via disconnect/reconnect)
3913 disconnect_and_reconnect(&client_a, cx_a).await;
3914 disconnect_and_reconnect(&client_b, cx_b).await;
3915 disconnect_and_reconnect(&client_c, cx_c).await;
3916 executor.run_until_parked();
3917 assert_eq!(client_a.summarize_contacts(cx_a).current, &["user_b"]);
3918 assert_eq!(client_b.summarize_contacts(cx_b).current, &["user_a"]);
3919 assert!(client_b
3920 .summarize_contacts(cx_b)
3921 .incoming_requests
3922 .is_empty());
3923 assert!(client_c.summarize_contacts(cx_c).current.is_empty());
3924 assert!(client_c
3925 .summarize_contacts(cx_c)
3926 .outgoing_requests
3927 .is_empty());
3928
3929 async fn disconnect_and_reconnect(client: &TestClient, cx: &mut TestAppContext) {
3930 client.disconnect(&cx.to_async()).unwrap();
3931 client.clear_contacts(cx).await;
3932 client
3933 .authenticate_and_connect(false, &cx.to_async())
3934 .await
3935 .unwrap();
3936 }
3937}
3938
3939#[gpui::test(iterations = 10)]
3940async fn test_following(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
3941 cx_a.foreground().forbid_parking();
3942 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
3943 let client_a = server.create_client(cx_a, "user_a").await;
3944 let client_b = server.create_client(cx_b, "user_b").await;
3945 server
3946 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
3947 .await;
3948 cx_a.update(editor::init);
3949 cx_b.update(editor::init);
3950
3951 client_a
3952 .fs
3953 .insert_tree(
3954 "/a",
3955 json!({
3956 "1.txt": "one",
3957 "2.txt": "two",
3958 "3.txt": "three",
3959 }),
3960 )
3961 .await;
3962 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
3963
3964 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
3965
3966 // Client A opens some editors.
3967 let workspace_a = client_a.build_workspace(&project_a, cx_a);
3968 let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
3969 let editor_a1 = workspace_a
3970 .update(cx_a, |workspace, cx| {
3971 workspace.open_path((worktree_id, "1.txt"), true, cx)
3972 })
3973 .await
3974 .unwrap()
3975 .downcast::<Editor>()
3976 .unwrap();
3977 let editor_a2 = workspace_a
3978 .update(cx_a, |workspace, cx| {
3979 workspace.open_path((worktree_id, "2.txt"), true, cx)
3980 })
3981 .await
3982 .unwrap()
3983 .downcast::<Editor>()
3984 .unwrap();
3985
3986 // Client B opens an editor.
3987 let workspace_b = client_b.build_workspace(&project_b, cx_b);
3988 let editor_b1 = workspace_b
3989 .update(cx_b, |workspace, cx| {
3990 workspace.open_path((worktree_id, "1.txt"), true, cx)
3991 })
3992 .await
3993 .unwrap()
3994 .downcast::<Editor>()
3995 .unwrap();
3996
3997 let client_a_id = project_b.read_with(cx_b, |project, _| {
3998 project.collaborators().values().next().unwrap().peer_id
3999 });
4000 let client_b_id = project_a.read_with(cx_a, |project, _| {
4001 project.collaborators().values().next().unwrap().peer_id
4002 });
4003
4004 // When client B starts following client A, all visible view states are replicated to client B.
4005 editor_a1.update(cx_a, |editor, cx| {
4006 editor.change_selections(None, cx, |s| s.select_ranges([0..1]))
4007 });
4008 editor_a2.update(cx_a, |editor, cx| {
4009 editor.change_selections(None, cx, |s| s.select_ranges([2..3]))
4010 });
4011 workspace_b
4012 .update(cx_b, |workspace, cx| {
4013 workspace
4014 .toggle_follow(&ToggleFollow(client_a_id), cx)
4015 .unwrap()
4016 })
4017 .await
4018 .unwrap();
4019
4020 let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
4021 workspace
4022 .active_item(cx)
4023 .unwrap()
4024 .downcast::<Editor>()
4025 .unwrap()
4026 });
4027 assert!(cx_b.read(|cx| editor_b2.is_focused(cx)));
4028 assert_eq!(
4029 editor_b2.read_with(cx_b, |editor, cx| editor.project_path(cx)),
4030 Some((worktree_id, "2.txt").into())
4031 );
4032 assert_eq!(
4033 editor_b2.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
4034 vec![2..3]
4035 );
4036 assert_eq!(
4037 editor_b1.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
4038 vec![0..1]
4039 );
4040
4041 // When client A activates a different editor, client B does so as well.
4042 workspace_a.update(cx_a, |workspace, cx| {
4043 workspace.activate_item(&editor_a1, cx)
4044 });
4045 workspace_b
4046 .condition(cx_b, |workspace, cx| {
4047 workspace.active_item(cx).unwrap().id() == editor_b1.id()
4048 })
4049 .await;
4050
4051 // When client A navigates back and forth, client B does so as well.
4052 workspace_a
4053 .update(cx_a, |workspace, cx| {
4054 workspace::Pane::go_back(workspace, None, cx)
4055 })
4056 .await;
4057 workspace_b
4058 .condition(cx_b, |workspace, cx| {
4059 workspace.active_item(cx).unwrap().id() == editor_b2.id()
4060 })
4061 .await;
4062
4063 workspace_a
4064 .update(cx_a, |workspace, cx| {
4065 workspace::Pane::go_forward(workspace, None, cx)
4066 })
4067 .await;
4068 workspace_b
4069 .condition(cx_b, |workspace, cx| {
4070 workspace.active_item(cx).unwrap().id() == editor_b1.id()
4071 })
4072 .await;
4073
4074 // Changes to client A's editor are reflected on client B.
4075 editor_a1.update(cx_a, |editor, cx| {
4076 editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2]));
4077 });
4078 editor_b1
4079 .condition(cx_b, |editor, cx| {
4080 editor.selections.ranges(cx) == vec![1..1, 2..2]
4081 })
4082 .await;
4083
4084 editor_a1.update(cx_a, |editor, cx| editor.set_text("TWO", cx));
4085 editor_b1
4086 .condition(cx_b, |editor, cx| editor.text(cx) == "TWO")
4087 .await;
4088
4089 editor_a1.update(cx_a, |editor, cx| {
4090 editor.change_selections(None, cx, |s| s.select_ranges([3..3]));
4091 editor.set_scroll_position(vec2f(0., 100.), cx);
4092 });
4093 editor_b1
4094 .condition(cx_b, |editor, cx| {
4095 editor.selections.ranges(cx) == vec![3..3]
4096 })
4097 .await;
4098
4099 // After unfollowing, client B stops receiving updates from client A.
4100 workspace_b.update(cx_b, |workspace, cx| {
4101 workspace.unfollow(&workspace.active_pane().clone(), cx)
4102 });
4103 workspace_a.update(cx_a, |workspace, cx| {
4104 workspace.activate_item(&editor_a2, cx)
4105 });
4106 cx_a.foreground().run_until_parked();
4107 assert_eq!(
4108 workspace_b.read_with(cx_b, |workspace, cx| workspace
4109 .active_item(cx)
4110 .unwrap()
4111 .id()),
4112 editor_b1.id()
4113 );
4114
4115 // Client A starts following client B.
4116 workspace_a
4117 .update(cx_a, |workspace, cx| {
4118 workspace
4119 .toggle_follow(&ToggleFollow(client_b_id), cx)
4120 .unwrap()
4121 })
4122 .await
4123 .unwrap();
4124 assert_eq!(
4125 workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
4126 Some(client_b_id)
4127 );
4128 assert_eq!(
4129 workspace_a.read_with(cx_a, |workspace, cx| workspace
4130 .active_item(cx)
4131 .unwrap()
4132 .id()),
4133 editor_a1.id()
4134 );
4135
4136 // Following interrupts when client B disconnects.
4137 client_b.disconnect(&cx_b.to_async()).unwrap();
4138 cx_a.foreground().run_until_parked();
4139 assert_eq!(
4140 workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
4141 None
4142 );
4143}
4144
4145#[gpui::test(iterations = 10)]
4146async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
4147 cx_a.foreground().forbid_parking();
4148 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
4149 let client_a = server.create_client(cx_a, "user_a").await;
4150 let client_b = server.create_client(cx_b, "user_b").await;
4151 server
4152 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
4153 .await;
4154 cx_a.update(editor::init);
4155 cx_b.update(editor::init);
4156
4157 // Client A shares a project.
4158 client_a
4159 .fs
4160 .insert_tree(
4161 "/a",
4162 json!({
4163 "1.txt": "one",
4164 "2.txt": "two",
4165 "3.txt": "three",
4166 "4.txt": "four",
4167 }),
4168 )
4169 .await;
4170 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
4171
4172 // Client B joins the project.
4173 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
4174
4175 // Client A opens some editors.
4176 let workspace_a = client_a.build_workspace(&project_a, cx_a);
4177 let pane_a1 = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
4178 let _editor_a1 = workspace_a
4179 .update(cx_a, |workspace, cx| {
4180 workspace.open_path((worktree_id, "1.txt"), true, cx)
4181 })
4182 .await
4183 .unwrap()
4184 .downcast::<Editor>()
4185 .unwrap();
4186
4187 // Client B opens an editor.
4188 let workspace_b = client_b.build_workspace(&project_b, cx_b);
4189 let pane_b1 = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
4190 let _editor_b1 = workspace_b
4191 .update(cx_b, |workspace, cx| {
4192 workspace.open_path((worktree_id, "2.txt"), true, cx)
4193 })
4194 .await
4195 .unwrap()
4196 .downcast::<Editor>()
4197 .unwrap();
4198
4199 // Clients A and B follow each other in split panes
4200 workspace_a.update(cx_a, |workspace, cx| {
4201 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
4202 let pane_a1 = pane_a1.clone();
4203 cx.defer(move |workspace, _| {
4204 assert_ne!(*workspace.active_pane(), pane_a1);
4205 });
4206 });
4207 workspace_a
4208 .update(cx_a, |workspace, cx| {
4209 let leader_id = *project_a.read(cx).collaborators().keys().next().unwrap();
4210 workspace
4211 .toggle_follow(&workspace::ToggleFollow(leader_id), cx)
4212 .unwrap()
4213 })
4214 .await
4215 .unwrap();
4216 workspace_b.update(cx_b, |workspace, cx| {
4217 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
4218 let pane_b1 = pane_b1.clone();
4219 cx.defer(move |workspace, _| {
4220 assert_ne!(*workspace.active_pane(), pane_b1);
4221 });
4222 });
4223 workspace_b
4224 .update(cx_b, |workspace, cx| {
4225 let leader_id = *project_b.read(cx).collaborators().keys().next().unwrap();
4226 workspace
4227 .toggle_follow(&workspace::ToggleFollow(leader_id), cx)
4228 .unwrap()
4229 })
4230 .await
4231 .unwrap();
4232
4233 workspace_a.update(cx_a, |workspace, cx| {
4234 workspace.activate_next_pane(cx);
4235 });
4236 // Wait for focus effects to be fully flushed
4237 workspace_a.update(cx_a, |workspace, _| {
4238 assert_eq!(*workspace.active_pane(), pane_a1);
4239 });
4240
4241 workspace_a
4242 .update(cx_a, |workspace, cx| {
4243 workspace.open_path((worktree_id, "3.txt"), true, cx)
4244 })
4245 .await
4246 .unwrap();
4247 workspace_b.update(cx_b, |workspace, cx| {
4248 workspace.activate_next_pane(cx);
4249 });
4250
4251 workspace_b
4252 .update(cx_b, |workspace, cx| {
4253 assert_eq!(*workspace.active_pane(), pane_b1);
4254 workspace.open_path((worktree_id, "4.txt"), true, cx)
4255 })
4256 .await
4257 .unwrap();
4258 cx_a.foreground().run_until_parked();
4259
4260 // Ensure leader updates don't change the active pane of followers
4261 workspace_a.read_with(cx_a, |workspace, _| {
4262 assert_eq!(*workspace.active_pane(), pane_a1);
4263 });
4264 workspace_b.read_with(cx_b, |workspace, _| {
4265 assert_eq!(*workspace.active_pane(), pane_b1);
4266 });
4267
4268 // Ensure peers following each other doesn't cause an infinite loop.
4269 assert_eq!(
4270 workspace_a.read_with(cx_a, |workspace, cx| workspace
4271 .active_item(cx)
4272 .unwrap()
4273 .project_path(cx)),
4274 Some((worktree_id, "3.txt").into())
4275 );
4276 workspace_a.update(cx_a, |workspace, cx| {
4277 assert_eq!(
4278 workspace.active_item(cx).unwrap().project_path(cx),
4279 Some((worktree_id, "3.txt").into())
4280 );
4281 workspace.activate_next_pane(cx);
4282 });
4283
4284 workspace_a.update(cx_a, |workspace, cx| {
4285 assert_eq!(
4286 workspace.active_item(cx).unwrap().project_path(cx),
4287 Some((worktree_id, "4.txt").into())
4288 );
4289 });
4290
4291 workspace_b.update(cx_b, |workspace, cx| {
4292 assert_eq!(
4293 workspace.active_item(cx).unwrap().project_path(cx),
4294 Some((worktree_id, "4.txt").into())
4295 );
4296 workspace.activate_next_pane(cx);
4297 });
4298
4299 workspace_b.update(cx_b, |workspace, cx| {
4300 assert_eq!(
4301 workspace.active_item(cx).unwrap().project_path(cx),
4302 Some((worktree_id, "3.txt").into())
4303 );
4304 });
4305}
4306
4307#[gpui::test(iterations = 10)]
4308async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
4309 cx_a.foreground().forbid_parking();
4310
4311 // 2 clients connect to a server.
4312 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
4313 let client_a = server.create_client(cx_a, "user_a").await;
4314 let client_b = server.create_client(cx_b, "user_b").await;
4315 server
4316 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
4317 .await;
4318 cx_a.update(editor::init);
4319 cx_b.update(editor::init);
4320
4321 // Client A shares a project.
4322 client_a
4323 .fs
4324 .insert_tree(
4325 "/a",
4326 json!({
4327 "1.txt": "one",
4328 "2.txt": "two",
4329 "3.txt": "three",
4330 }),
4331 )
4332 .await;
4333 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
4334 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
4335
4336 // Client A opens some editors.
4337 let workspace_a = client_a.build_workspace(&project_a, cx_a);
4338 let _editor_a1 = workspace_a
4339 .update(cx_a, |workspace, cx| {
4340 workspace.open_path((worktree_id, "1.txt"), true, cx)
4341 })
4342 .await
4343 .unwrap()
4344 .downcast::<Editor>()
4345 .unwrap();
4346
4347 // Client B starts following client A.
4348 let workspace_b = client_b.build_workspace(&project_b, cx_b);
4349 let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
4350 let leader_id = project_b.read_with(cx_b, |project, _| {
4351 project.collaborators().values().next().unwrap().peer_id
4352 });
4353 workspace_b
4354 .update(cx_b, |workspace, cx| {
4355 workspace
4356 .toggle_follow(&ToggleFollow(leader_id), cx)
4357 .unwrap()
4358 })
4359 .await
4360 .unwrap();
4361 assert_eq!(
4362 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
4363 Some(leader_id)
4364 );
4365 let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
4366 workspace
4367 .active_item(cx)
4368 .unwrap()
4369 .downcast::<Editor>()
4370 .unwrap()
4371 });
4372
4373 // When client B moves, it automatically stops following client A.
4374 editor_b2.update(cx_b, |editor, cx| editor.move_right(&editor::MoveRight, cx));
4375 assert_eq!(
4376 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
4377 None
4378 );
4379
4380 workspace_b
4381 .update(cx_b, |workspace, cx| {
4382 workspace
4383 .toggle_follow(&ToggleFollow(leader_id), cx)
4384 .unwrap()
4385 })
4386 .await
4387 .unwrap();
4388 assert_eq!(
4389 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
4390 Some(leader_id)
4391 );
4392
4393 // When client B edits, it automatically stops following client A.
4394 editor_b2.update(cx_b, |editor, cx| editor.insert("X", cx));
4395 assert_eq!(
4396 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
4397 None
4398 );
4399
4400 workspace_b
4401 .update(cx_b, |workspace, cx| {
4402 workspace
4403 .toggle_follow(&ToggleFollow(leader_id), cx)
4404 .unwrap()
4405 })
4406 .await
4407 .unwrap();
4408 assert_eq!(
4409 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
4410 Some(leader_id)
4411 );
4412
4413 // When client B scrolls, it automatically stops following client A.
4414 editor_b2.update(cx_b, |editor, cx| {
4415 editor.set_scroll_position(vec2f(0., 3.), cx)
4416 });
4417 assert_eq!(
4418 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
4419 None
4420 );
4421
4422 workspace_b
4423 .update(cx_b, |workspace, cx| {
4424 workspace
4425 .toggle_follow(&ToggleFollow(leader_id), cx)
4426 .unwrap()
4427 })
4428 .await
4429 .unwrap();
4430 assert_eq!(
4431 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
4432 Some(leader_id)
4433 );
4434
4435 // When client B activates a different pane, it continues following client A in the original pane.
4436 workspace_b.update(cx_b, |workspace, cx| {
4437 workspace.split_pane(pane_b.clone(), SplitDirection::Right, cx)
4438 });
4439 assert_eq!(
4440 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
4441 Some(leader_id)
4442 );
4443
4444 workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
4445 assert_eq!(
4446 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
4447 Some(leader_id)
4448 );
4449
4450 // When client B activates a different item in the original pane, it automatically stops following client A.
4451 workspace_b
4452 .update(cx_b, |workspace, cx| {
4453 workspace.open_path((worktree_id, "2.txt"), true, cx)
4454 })
4455 .await
4456 .unwrap();
4457 assert_eq!(
4458 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
4459 None
4460 );
4461}
4462
4463#[gpui::test(iterations = 10)]
4464async fn test_peers_simultaneously_following_each_other(
4465 deterministic: Arc<Deterministic>,
4466 cx_a: &mut TestAppContext,
4467 cx_b: &mut TestAppContext,
4468) {
4469 deterministic.forbid_parking();
4470
4471 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
4472 let client_a = server.create_client(cx_a, "user_a").await;
4473 let client_b = server.create_client(cx_b, "user_b").await;
4474 server
4475 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
4476 .await;
4477 cx_a.update(editor::init);
4478 cx_b.update(editor::init);
4479
4480 client_a.fs.insert_tree("/a", json!({})).await;
4481 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
4482 let workspace_a = client_a.build_workspace(&project_a, cx_a);
4483
4484 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
4485 let workspace_b = client_b.build_workspace(&project_b, cx_b);
4486
4487 deterministic.run_until_parked();
4488 let client_a_id = project_b.read_with(cx_b, |project, _| {
4489 project.collaborators().values().next().unwrap().peer_id
4490 });
4491 let client_b_id = project_a.read_with(cx_a, |project, _| {
4492 project.collaborators().values().next().unwrap().peer_id
4493 });
4494
4495 let a_follow_b = workspace_a.update(cx_a, |workspace, cx| {
4496 workspace
4497 .toggle_follow(&ToggleFollow(client_b_id), cx)
4498 .unwrap()
4499 });
4500 let b_follow_a = workspace_b.update(cx_b, |workspace, cx| {
4501 workspace
4502 .toggle_follow(&ToggleFollow(client_a_id), cx)
4503 .unwrap()
4504 });
4505
4506 futures::try_join!(a_follow_b, b_follow_a).unwrap();
4507 workspace_a.read_with(cx_a, |workspace, _| {
4508 assert_eq!(
4509 workspace.leader_for_pane(workspace.active_pane()),
4510 Some(client_b_id)
4511 );
4512 });
4513 workspace_b.read_with(cx_b, |workspace, _| {
4514 assert_eq!(
4515 workspace.leader_for_pane(workspace.active_pane()),
4516 Some(client_a_id)
4517 );
4518 });
4519}
4520
4521#[gpui::test(iterations = 100)]
4522async fn test_random_collaboration(
4523 cx: &mut TestAppContext,
4524 deterministic: Arc<Deterministic>,
4525 rng: StdRng,
4526) {
4527 deterministic.forbid_parking();
4528 let max_peers = env::var("MAX_PEERS")
4529 .map(|i| i.parse().expect("invalid `MAX_PEERS` variable"))
4530 .unwrap_or(5);
4531 assert!(max_peers <= 5);
4532
4533 let max_operations = env::var("OPERATIONS")
4534 .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
4535 .unwrap_or(10);
4536
4537 let rng = Arc::new(Mutex::new(rng));
4538
4539 let guest_lang_registry = Arc::new(LanguageRegistry::test());
4540 let host_language_registry = Arc::new(LanguageRegistry::test());
4541
4542 let fs = FakeFs::new(cx.background());
4543 fs.insert_tree("/_collab", json!({"init": ""})).await;
4544
4545 let mut server = TestServer::start(cx.foreground(), cx.background()).await;
4546 let db = server.app_state.db.clone();
4547 let host_user_id = db.create_user("host", None, false).await.unwrap();
4548 let mut available_guests = vec![
4549 "guest-1".to_string(),
4550 "guest-2".to_string(),
4551 "guest-3".to_string(),
4552 "guest-4".to_string(),
4553 ];
4554
4555 for username in &available_guests {
4556 let guest_user_id = db.create_user(username, None, false).await.unwrap();
4557 assert_eq!(*username, format!("guest-{}", guest_user_id));
4558 server
4559 .app_state
4560 .db
4561 .send_contact_request(guest_user_id, host_user_id)
4562 .await
4563 .unwrap();
4564 server
4565 .app_state
4566 .db
4567 .respond_to_contact_request(host_user_id, guest_user_id, true)
4568 .await
4569 .unwrap();
4570 }
4571
4572 let mut clients = Vec::new();
4573 let mut user_ids = Vec::new();
4574 let mut op_start_signals = Vec::new();
4575
4576 let mut next_entity_id = 100000;
4577 let mut host_cx = TestAppContext::new(
4578 cx.foreground_platform(),
4579 cx.platform(),
4580 deterministic.build_foreground(next_entity_id),
4581 deterministic.build_background(),
4582 cx.font_cache(),
4583 cx.leak_detector(),
4584 next_entity_id,
4585 );
4586 let host = server.create_client(&mut host_cx, "host").await;
4587 let host_project = host_cx.update(|cx| {
4588 Project::local(
4589 true,
4590 host.client.clone(),
4591 host.user_store.clone(),
4592 host.project_store.clone(),
4593 host_language_registry.clone(),
4594 fs.clone(),
4595 cx,
4596 )
4597 });
4598 let host_project_id = host_project
4599 .update(&mut host_cx, |p, _| p.next_remote_id())
4600 .await;
4601
4602 let (collab_worktree, _) = host_project
4603 .update(&mut host_cx, |project, cx| {
4604 project.find_or_create_local_worktree("/_collab", true, cx)
4605 })
4606 .await
4607 .unwrap();
4608 collab_worktree
4609 .read_with(&host_cx, |tree, _| tree.as_local().unwrap().scan_complete())
4610 .await;
4611
4612 // Set up fake language servers.
4613 let mut language = Language::new(
4614 LanguageConfig {
4615 name: "Rust".into(),
4616 path_suffixes: vec!["rs".to_string()],
4617 ..Default::default()
4618 },
4619 None,
4620 );
4621 let _fake_servers = language
4622 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
4623 name: "the-fake-language-server",
4624 capabilities: lsp::LanguageServer::full_capabilities(),
4625 initializer: Some(Box::new({
4626 let rng = rng.clone();
4627 let fs = fs.clone();
4628 let project = host_project.downgrade();
4629 move |fake_server: &mut FakeLanguageServer| {
4630 fake_server.handle_request::<lsp::request::Completion, _, _>(
4631 |_, _| async move {
4632 Ok(Some(lsp::CompletionResponse::Array(vec![
4633 lsp::CompletionItem {
4634 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
4635 range: lsp::Range::new(
4636 lsp::Position::new(0, 0),
4637 lsp::Position::new(0, 0),
4638 ),
4639 new_text: "the-new-text".to_string(),
4640 })),
4641 ..Default::default()
4642 },
4643 ])))
4644 },
4645 );
4646
4647 fake_server.handle_request::<lsp::request::CodeActionRequest, _, _>(
4648 |_, _| async move {
4649 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
4650 lsp::CodeAction {
4651 title: "the-code-action".to_string(),
4652 ..Default::default()
4653 },
4654 )]))
4655 },
4656 );
4657
4658 fake_server.handle_request::<lsp::request::PrepareRenameRequest, _, _>(
4659 |params, _| async move {
4660 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
4661 params.position,
4662 params.position,
4663 ))))
4664 },
4665 );
4666
4667 fake_server.handle_request::<lsp::request::GotoDefinition, _, _>({
4668 let fs = fs.clone();
4669 let rng = rng.clone();
4670 move |_, _| {
4671 let fs = fs.clone();
4672 let rng = rng.clone();
4673 async move {
4674 let files = fs.files().await;
4675 let mut rng = rng.lock();
4676 let count = rng.gen_range::<usize, _>(1..3);
4677 let files = (0..count)
4678 .map(|_| files.choose(&mut *rng).unwrap())
4679 .collect::<Vec<_>>();
4680 log::info!("LSP: Returning definitions in files {:?}", &files);
4681 Ok(Some(lsp::GotoDefinitionResponse::Array(
4682 files
4683 .into_iter()
4684 .map(|file| lsp::Location {
4685 uri: lsp::Url::from_file_path(file).unwrap(),
4686 range: Default::default(),
4687 })
4688 .collect(),
4689 )))
4690 }
4691 }
4692 });
4693
4694 fake_server.handle_request::<lsp::request::DocumentHighlightRequest, _, _>({
4695 let rng = rng.clone();
4696 let project = project;
4697 move |params, mut cx| {
4698 let highlights = if let Some(project) = project.upgrade(&cx) {
4699 project.update(&mut cx, |project, cx| {
4700 let path = params
4701 .text_document_position_params
4702 .text_document
4703 .uri
4704 .to_file_path()
4705 .unwrap();
4706 let (worktree, relative_path) =
4707 project.find_local_worktree(&path, cx)?;
4708 let project_path =
4709 ProjectPath::from((worktree.read(cx).id(), relative_path));
4710 let buffer =
4711 project.get_open_buffer(&project_path, cx)?.read(cx);
4712
4713 let mut highlights = Vec::new();
4714 let highlight_count = rng.lock().gen_range(1..=5);
4715 let mut prev_end = 0;
4716 for _ in 0..highlight_count {
4717 let range =
4718 buffer.random_byte_range(prev_end, &mut *rng.lock());
4719
4720 highlights.push(lsp::DocumentHighlight {
4721 range: range_to_lsp(range.to_point_utf16(buffer)),
4722 kind: Some(lsp::DocumentHighlightKind::READ),
4723 });
4724 prev_end = range.end;
4725 }
4726 Some(highlights)
4727 })
4728 } else {
4729 None
4730 };
4731 async move { Ok(highlights) }
4732 }
4733 });
4734 }
4735 })),
4736 ..Default::default()
4737 }))
4738 .await;
4739 host_language_registry.add(Arc::new(language));
4740
4741 let op_start_signal = futures::channel::mpsc::unbounded();
4742 user_ids.push(host.current_user_id(&host_cx));
4743 op_start_signals.push(op_start_signal.0);
4744 clients.push(host_cx.foreground().spawn(host.simulate_host(
4745 host_project,
4746 op_start_signal.1,
4747 rng.clone(),
4748 host_cx,
4749 )));
4750
4751 let disconnect_host_at = if rng.lock().gen_bool(0.2) {
4752 rng.lock().gen_range(0..max_operations)
4753 } else {
4754 max_operations
4755 };
4756
4757 let mut operations = 0;
4758 while operations < max_operations {
4759 if operations == disconnect_host_at {
4760 server.disconnect_client(user_ids[0]);
4761 deterministic.advance_clock(RECEIVE_TIMEOUT);
4762 drop(op_start_signals);
4763
4764 deterministic.start_waiting();
4765 let mut clients = futures::future::join_all(clients).await;
4766 deterministic.finish_waiting();
4767 deterministic.run_until_parked();
4768
4769 let (host, host_project, mut host_cx, host_err) = clients.remove(0);
4770 if let Some(host_err) = host_err {
4771 log::error!("host error - {:?}", host_err);
4772 }
4773 host_project.read_with(&host_cx, |project, _| assert!(!project.is_shared()));
4774 for (guest, guest_project, mut guest_cx, guest_err) in clients {
4775 if let Some(guest_err) = guest_err {
4776 log::error!("{} error - {:?}", guest.username, guest_err);
4777 }
4778
4779 guest_project.read_with(&guest_cx, |project, _| assert!(project.is_read_only()));
4780 guest_cx.update(|_| drop((guest, guest_project)));
4781 }
4782 host_cx.update(|_| drop((host, host_project)));
4783
4784 return;
4785 }
4786
4787 let distribution = rng.lock().gen_range(0..100);
4788 match distribution {
4789 0..=19 if !available_guests.is_empty() => {
4790 let guest_ix = rng.lock().gen_range(0..available_guests.len());
4791 let guest_username = available_guests.remove(guest_ix);
4792 log::info!("Adding new connection for {}", guest_username);
4793 next_entity_id += 100000;
4794 let mut guest_cx = TestAppContext::new(
4795 cx.foreground_platform(),
4796 cx.platform(),
4797 deterministic.build_foreground(next_entity_id),
4798 deterministic.build_background(),
4799 cx.font_cache(),
4800 cx.leak_detector(),
4801 next_entity_id,
4802 );
4803
4804 deterministic.start_waiting();
4805 let guest = server.create_client(&mut guest_cx, &guest_username).await;
4806 let guest_project = Project::remote(
4807 host_project_id,
4808 guest.client.clone(),
4809 guest.user_store.clone(),
4810 guest.project_store.clone(),
4811 guest_lang_registry.clone(),
4812 FakeFs::new(cx.background()),
4813 guest_cx.to_async(),
4814 )
4815 .await
4816 .unwrap();
4817 deterministic.finish_waiting();
4818
4819 let op_start_signal = futures::channel::mpsc::unbounded();
4820 user_ids.push(guest.current_user_id(&guest_cx));
4821 op_start_signals.push(op_start_signal.0);
4822 clients.push(guest_cx.foreground().spawn(guest.simulate_guest(
4823 guest_username.clone(),
4824 guest_project,
4825 op_start_signal.1,
4826 rng.clone(),
4827 guest_cx,
4828 )));
4829
4830 log::info!("Added connection for {}", guest_username);
4831 operations += 1;
4832 }
4833 20..=29 if clients.len() > 1 => {
4834 let guest_ix = rng.lock().gen_range(1..clients.len());
4835 log::info!("Removing guest {}", user_ids[guest_ix]);
4836 let removed_guest_id = user_ids.remove(guest_ix);
4837 let guest = clients.remove(guest_ix);
4838 op_start_signals.remove(guest_ix);
4839 server.forbid_connections();
4840 server.disconnect_client(removed_guest_id);
4841 deterministic.advance_clock(RECEIVE_TIMEOUT);
4842 deterministic.start_waiting();
4843 log::info!("Waiting for guest {} to exit...", removed_guest_id);
4844 let (guest, guest_project, mut guest_cx, guest_err) = guest.await;
4845 deterministic.finish_waiting();
4846 server.allow_connections();
4847
4848 if let Some(guest_err) = guest_err {
4849 log::error!("{} error - {:?}", guest.username, guest_err);
4850 }
4851 guest_project.read_with(&guest_cx, |project, _| assert!(project.is_read_only()));
4852 for user_id in &user_ids {
4853 let contacts = server.app_state.db.get_contacts(*user_id).await.unwrap();
4854 let contacts = server
4855 .store
4856 .lock()
4857 .await
4858 .build_initial_contacts_update(contacts)
4859 .contacts;
4860 for contact in contacts {
4861 if contact.online {
4862 assert_ne!(
4863 contact.user_id, removed_guest_id.0 as u64,
4864 "removed guest is still a contact of another peer"
4865 );
4866 }
4867 }
4868 }
4869
4870 log::info!("{} removed", guest.username);
4871 available_guests.push(guest.username.clone());
4872 guest_cx.update(|_| drop((guest, guest_project)));
4873
4874 operations += 1;
4875 }
4876 _ => {
4877 while operations < max_operations && rng.lock().gen_bool(0.7) {
4878 op_start_signals
4879 .choose(&mut *rng.lock())
4880 .unwrap()
4881 .unbounded_send(())
4882 .unwrap();
4883 operations += 1;
4884 }
4885
4886 if rng.lock().gen_bool(0.8) {
4887 deterministic.run_until_parked();
4888 }
4889 }
4890 }
4891 }
4892
4893 drop(op_start_signals);
4894 deterministic.start_waiting();
4895 let mut clients = futures::future::join_all(clients).await;
4896 deterministic.finish_waiting();
4897 deterministic.run_until_parked();
4898
4899 let (host_client, host_project, mut host_cx, host_err) = clients.remove(0);
4900 if let Some(host_err) = host_err {
4901 panic!("host error - {:?}", host_err);
4902 }
4903 let host_worktree_snapshots = host_project.read_with(&host_cx, |project, cx| {
4904 project
4905 .worktrees(cx)
4906 .map(|worktree| {
4907 let snapshot = worktree.read(cx).snapshot();
4908 (snapshot.id(), snapshot)
4909 })
4910 .collect::<BTreeMap<_, _>>()
4911 });
4912
4913 host_project.read_with(&host_cx, |project, cx| project.check_invariants(cx));
4914
4915 for (guest_client, guest_project, mut guest_cx, guest_err) in clients.into_iter() {
4916 if let Some(guest_err) = guest_err {
4917 panic!("{} error - {:?}", guest_client.username, guest_err);
4918 }
4919 let worktree_snapshots = guest_project.read_with(&guest_cx, |project, cx| {
4920 project
4921 .worktrees(cx)
4922 .map(|worktree| {
4923 let worktree = worktree.read(cx);
4924 (worktree.id(), worktree.snapshot())
4925 })
4926 .collect::<BTreeMap<_, _>>()
4927 });
4928
4929 assert_eq!(
4930 worktree_snapshots.keys().collect::<Vec<_>>(),
4931 host_worktree_snapshots.keys().collect::<Vec<_>>(),
4932 "{} has different worktrees than the host",
4933 guest_client.username
4934 );
4935 for (id, host_snapshot) in &host_worktree_snapshots {
4936 let guest_snapshot = &worktree_snapshots[id];
4937 assert_eq!(
4938 guest_snapshot.root_name(),
4939 host_snapshot.root_name(),
4940 "{} has different root name than the host for worktree {}",
4941 guest_client.username,
4942 id
4943 );
4944 assert_eq!(
4945 guest_snapshot.entries(false).collect::<Vec<_>>(),
4946 host_snapshot.entries(false).collect::<Vec<_>>(),
4947 "{} has different snapshot than the host for worktree {}",
4948 guest_client.username,
4949 id
4950 );
4951 assert_eq!(guest_snapshot.scan_id(), host_snapshot.scan_id());
4952 }
4953
4954 guest_project.read_with(&guest_cx, |project, cx| project.check_invariants(cx));
4955
4956 for guest_buffer in &guest_client.buffers {
4957 let buffer_id = guest_buffer.read_with(&guest_cx, |buffer, _| buffer.remote_id());
4958 let host_buffer = host_project.read_with(&host_cx, |project, cx| {
4959 project.buffer_for_id(buffer_id, cx).unwrap_or_else(|| {
4960 panic!(
4961 "host does not have buffer for guest:{}, peer:{}, id:{}",
4962 guest_client.username, guest_client.peer_id, buffer_id
4963 )
4964 })
4965 });
4966 let path =
4967 host_buffer.read_with(&host_cx, |buffer, cx| buffer.file().unwrap().full_path(cx));
4968
4969 assert_eq!(
4970 guest_buffer.read_with(&guest_cx, |buffer, _| buffer.deferred_ops_len()),
4971 0,
4972 "{}, buffer {}, path {:?} has deferred operations",
4973 guest_client.username,
4974 buffer_id,
4975 path,
4976 );
4977 assert_eq!(
4978 guest_buffer.read_with(&guest_cx, |buffer, _| buffer.text()),
4979 host_buffer.read_with(&host_cx, |buffer, _| buffer.text()),
4980 "{}, buffer {}, path {:?}, differs from the host's buffer",
4981 guest_client.username,
4982 buffer_id,
4983 path
4984 );
4985 }
4986
4987 guest_cx.update(|_| drop((guest_project, guest_client)));
4988 }
4989
4990 host_cx.update(|_| drop((host_client, host_project)));
4991}
4992
4993struct TestServer {
4994 peer: Arc<Peer>,
4995 app_state: Arc<AppState>,
4996 server: Arc<Server>,
4997 foreground: Rc<executor::Foreground>,
4998 notifications: mpsc::UnboundedReceiver<()>,
4999 connection_killers: Arc<Mutex<HashMap<UserId, Arc<AtomicBool>>>>,
5000 forbid_connections: Arc<AtomicBool>,
5001 _test_db: TestDb,
5002}
5003
5004impl TestServer {
5005 async fn start(
5006 foreground: Rc<executor::Foreground>,
5007 background: Arc<executor::Background>,
5008 ) -> Self {
5009 let test_db = TestDb::fake(background.clone());
5010 let app_state = Self::build_app_state(&test_db).await;
5011 let peer = Peer::new();
5012 let notifications = mpsc::unbounded();
5013 let server = Server::new(app_state.clone(), Some(notifications.0));
5014 Self {
5015 peer,
5016 app_state,
5017 server,
5018 foreground,
5019 notifications: notifications.1,
5020 connection_killers: Default::default(),
5021 forbid_connections: Default::default(),
5022 _test_db: test_db,
5023 }
5024 }
5025
5026 async fn create_client(&mut self, cx: &mut TestAppContext, name: &str) -> TestClient {
5027 cx.update(|cx| {
5028 let mut settings = Settings::test(cx);
5029 settings.projects_online_by_default = false;
5030 cx.set_global(settings);
5031 });
5032
5033 let http = FakeHttpClient::with_404_response();
5034 let user_id = if let Ok(Some(user)) = self.app_state.db.get_user_by_github_login(name).await
5035 {
5036 user.id
5037 } else {
5038 self.app_state
5039 .db
5040 .create_user(name, None, false)
5041 .await
5042 .unwrap()
5043 };
5044 let client_name = name.to_string();
5045 let mut client = Client::new(http.clone());
5046 let server = self.server.clone();
5047 let db = self.app_state.db.clone();
5048 let connection_killers = self.connection_killers.clone();
5049 let forbid_connections = self.forbid_connections.clone();
5050 let (connection_id_tx, mut connection_id_rx) = mpsc::channel(16);
5051
5052 Arc::get_mut(&mut client)
5053 .unwrap()
5054 .set_id(user_id.0 as usize)
5055 .override_authenticate(move |cx| {
5056 cx.spawn(|_| async move {
5057 let access_token = "the-token".to_string();
5058 Ok(Credentials {
5059 user_id: user_id.0 as u64,
5060 access_token,
5061 })
5062 })
5063 })
5064 .override_establish_connection(move |credentials, cx| {
5065 assert_eq!(credentials.user_id, user_id.0 as u64);
5066 assert_eq!(credentials.access_token, "the-token");
5067
5068 let server = server.clone();
5069 let db = db.clone();
5070 let connection_killers = connection_killers.clone();
5071 let forbid_connections = forbid_connections.clone();
5072 let client_name = client_name.clone();
5073 let connection_id_tx = connection_id_tx.clone();
5074 cx.spawn(move |cx| async move {
5075 if forbid_connections.load(SeqCst) {
5076 Err(EstablishConnectionError::other(anyhow!(
5077 "server is forbidding connections"
5078 )))
5079 } else {
5080 let (client_conn, server_conn, killed) =
5081 Connection::in_memory(cx.background());
5082 connection_killers.lock().insert(user_id, killed);
5083 let user = db.get_user_by_id(user_id).await.unwrap().unwrap();
5084 cx.background()
5085 .spawn(server.handle_connection(
5086 server_conn,
5087 client_name,
5088 user,
5089 Some(connection_id_tx),
5090 cx.background(),
5091 ))
5092 .detach();
5093 Ok(client_conn)
5094 }
5095 })
5096 });
5097
5098 let fs = FakeFs::new(cx.background());
5099 let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
5100 let project_store = cx.add_model(|_| ProjectStore::new(project::Db::open_fake()));
5101 let app_state = Arc::new(workspace::AppState {
5102 client: client.clone(),
5103 user_store: user_store.clone(),
5104 project_store: project_store.clone(),
5105 languages: Arc::new(LanguageRegistry::new(Task::ready(()))),
5106 themes: ThemeRegistry::new((), cx.font_cache()),
5107 fs: fs.clone(),
5108 build_window_options: Default::default,
5109 initialize_workspace: |_, _, _| unimplemented!(),
5110 default_item_factory: |_, _| unimplemented!(),
5111 });
5112
5113 Channel::init(&client);
5114 Project::init(&client);
5115 cx.update(|cx| workspace::init(app_state.clone(), cx));
5116
5117 client
5118 .authenticate_and_connect(false, &cx.to_async())
5119 .await
5120 .unwrap();
5121 let peer_id = PeerId(connection_id_rx.next().await.unwrap().0);
5122
5123 let client = TestClient {
5124 client,
5125 peer_id,
5126 username: name.to_string(),
5127 user_store,
5128 project_store,
5129 fs,
5130 language_registry: Arc::new(LanguageRegistry::test()),
5131 buffers: Default::default(),
5132 };
5133 client.wait_for_current_user(cx).await;
5134 client
5135 }
5136
5137 fn disconnect_client(&self, user_id: UserId) {
5138 self.connection_killers
5139 .lock()
5140 .remove(&user_id)
5141 .unwrap()
5142 .store(true, SeqCst);
5143 }
5144
5145 fn forbid_connections(&self) {
5146 self.forbid_connections.store(true, SeqCst);
5147 }
5148
5149 fn allow_connections(&self) {
5150 self.forbid_connections.store(false, SeqCst);
5151 }
5152
5153 async fn make_contacts(&self, mut clients: Vec<(&TestClient, &mut TestAppContext)>) {
5154 while let Some((client_a, cx_a)) = clients.pop() {
5155 for (client_b, cx_b) in &mut clients {
5156 client_a
5157 .user_store
5158 .update(cx_a, |store, cx| {
5159 store.request_contact(client_b.user_id().unwrap(), cx)
5160 })
5161 .await
5162 .unwrap();
5163 cx_a.foreground().run_until_parked();
5164 client_b
5165 .user_store
5166 .update(*cx_b, |store, cx| {
5167 store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx)
5168 })
5169 .await
5170 .unwrap();
5171 }
5172 }
5173 }
5174
5175 async fn build_app_state(test_db: &TestDb) -> Arc<AppState> {
5176 Arc::new(AppState {
5177 db: test_db.db().clone(),
5178 api_token: Default::default(),
5179 invite_link_prefix: Default::default(),
5180 })
5181 }
5182
5183 async fn condition<F>(&mut self, mut predicate: F)
5184 where
5185 F: FnMut(&Store) -> bool,
5186 {
5187 assert!(
5188 self.foreground.parking_forbidden(),
5189 "you must call forbid_parking to use server conditions so we don't block indefinitely"
5190 );
5191 while !(predicate)(&*self.server.store.lock().await) {
5192 self.foreground.start_waiting();
5193 self.notifications.next().await;
5194 self.foreground.finish_waiting();
5195 }
5196 }
5197}
5198
5199impl Deref for TestServer {
5200 type Target = Server;
5201
5202 fn deref(&self) -> &Self::Target {
5203 &self.server
5204 }
5205}
5206
5207impl Drop for TestServer {
5208 fn drop(&mut self) {
5209 self.peer.reset();
5210 }
5211}
5212
5213struct TestClient {
5214 client: Arc<Client>,
5215 username: String,
5216 pub peer_id: PeerId,
5217 pub user_store: ModelHandle<UserStore>,
5218 pub project_store: ModelHandle<ProjectStore>,
5219 language_registry: Arc<LanguageRegistry>,
5220 fs: Arc<FakeFs>,
5221 buffers: HashSet<ModelHandle<language::Buffer>>,
5222}
5223
5224impl Deref for TestClient {
5225 type Target = Arc<Client>;
5226
5227 fn deref(&self) -> &Self::Target {
5228 &self.client
5229 }
5230}
5231
5232struct ContactsSummary {
5233 pub current: Vec<String>,
5234 pub outgoing_requests: Vec<String>,
5235 pub incoming_requests: Vec<String>,
5236}
5237
5238impl TestClient {
5239 pub fn current_user_id(&self, cx: &TestAppContext) -> UserId {
5240 UserId::from_proto(
5241 self.user_store
5242 .read_with(cx, |user_store, _| user_store.current_user().unwrap().id),
5243 )
5244 }
5245
5246 async fn wait_for_current_user(&self, cx: &TestAppContext) {
5247 let mut authed_user = self
5248 .user_store
5249 .read_with(cx, |user_store, _| user_store.watch_current_user());
5250 while authed_user.next().await.unwrap().is_none() {}
5251 }
5252
5253 async fn clear_contacts(&self, cx: &mut TestAppContext) {
5254 self.user_store
5255 .update(cx, |store, _| store.clear_contacts())
5256 .await;
5257 }
5258
5259 fn summarize_contacts(&self, cx: &TestAppContext) -> ContactsSummary {
5260 self.user_store.read_with(cx, |store, _| ContactsSummary {
5261 current: store
5262 .contacts()
5263 .iter()
5264 .map(|contact| contact.user.github_login.clone())
5265 .collect(),
5266 outgoing_requests: store
5267 .outgoing_contact_requests()
5268 .iter()
5269 .map(|user| user.github_login.clone())
5270 .collect(),
5271 incoming_requests: store
5272 .incoming_contact_requests()
5273 .iter()
5274 .map(|user| user.github_login.clone())
5275 .collect(),
5276 })
5277 }
5278
5279 async fn build_local_project(
5280 &self,
5281 root_path: impl AsRef<Path>,
5282 cx: &mut TestAppContext,
5283 ) -> (ModelHandle<Project>, WorktreeId) {
5284 let project = cx.update(|cx| {
5285 Project::local(
5286 true,
5287 self.client.clone(),
5288 self.user_store.clone(),
5289 self.project_store.clone(),
5290 self.language_registry.clone(),
5291 self.fs.clone(),
5292 cx,
5293 )
5294 });
5295 let (worktree, _) = project
5296 .update(cx, |p, cx| {
5297 p.find_or_create_local_worktree(root_path, true, cx)
5298 })
5299 .await
5300 .unwrap();
5301 worktree
5302 .read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete())
5303 .await;
5304 project
5305 .update(cx, |project, _| project.next_remote_id())
5306 .await;
5307 (project, worktree.read_with(cx, |tree, _| tree.id()))
5308 }
5309
5310 async fn build_remote_project(
5311 &self,
5312 host_project: &ModelHandle<Project>,
5313 host_cx: &mut TestAppContext,
5314 guest_cx: &mut TestAppContext,
5315 ) -> ModelHandle<Project> {
5316 let host_project_id = host_project
5317 .read_with(host_cx, |project, _| project.next_remote_id())
5318 .await;
5319 let guest_user_id = self.user_id().unwrap();
5320 let languages = host_project.read_with(host_cx, |project, _| project.languages().clone());
5321 let project_b = guest_cx.spawn(|cx| {
5322 Project::remote(
5323 host_project_id,
5324 self.client.clone(),
5325 self.user_store.clone(),
5326 self.project_store.clone(),
5327 languages,
5328 FakeFs::new(cx.background()),
5329 cx,
5330 )
5331 });
5332 host_cx.foreground().run_until_parked();
5333 host_project.update(host_cx, |project, cx| {
5334 project.respond_to_join_request(guest_user_id, true, cx)
5335 });
5336 let project = project_b.await.unwrap();
5337 project
5338 }
5339
5340 fn build_workspace(
5341 &self,
5342 project: &ModelHandle<Project>,
5343 cx: &mut TestAppContext,
5344 ) -> ViewHandle<Workspace> {
5345 let (_, root_view) = cx.add_window(|_| EmptyView);
5346 cx.add_view(&root_view, |cx| {
5347 Workspace::new(project.clone(), |_, _| unimplemented!(), cx)
5348 })
5349 }
5350
5351 async fn simulate_host(
5352 mut self,
5353 project: ModelHandle<Project>,
5354 op_start_signal: futures::channel::mpsc::UnboundedReceiver<()>,
5355 rng: Arc<Mutex<StdRng>>,
5356 mut cx: TestAppContext,
5357 ) -> (
5358 Self,
5359 ModelHandle<Project>,
5360 TestAppContext,
5361 Option<anyhow::Error>,
5362 ) {
5363 async fn simulate_host_internal(
5364 client: &mut TestClient,
5365 project: ModelHandle<Project>,
5366 mut op_start_signal: futures::channel::mpsc::UnboundedReceiver<()>,
5367 rng: Arc<Mutex<StdRng>>,
5368 cx: &mut TestAppContext,
5369 ) -> anyhow::Result<()> {
5370 let fs = project.read_with(cx, |project, _| project.fs().clone());
5371
5372 cx.update(|cx| {
5373 cx.subscribe(&project, move |project, event, cx| {
5374 if let project::Event::ContactRequestedJoin(user) = event {
5375 log::info!("Host: accepting join request from {}", user.github_login);
5376 project.update(cx, |project, cx| {
5377 project.respond_to_join_request(user.id, true, cx)
5378 });
5379 }
5380 })
5381 .detach();
5382 });
5383
5384 while op_start_signal.next().await.is_some() {
5385 let distribution = rng.lock().gen_range::<usize, _>(0..100);
5386 let files = fs.as_fake().files().await;
5387 match distribution {
5388 0..=19 if !files.is_empty() => {
5389 let path = files.choose(&mut *rng.lock()).unwrap();
5390 let mut path = path.as_path();
5391 while let Some(parent_path) = path.parent() {
5392 path = parent_path;
5393 if rng.lock().gen() {
5394 break;
5395 }
5396 }
5397
5398 log::info!("Host: find/create local worktree {:?}", path);
5399 let find_or_create_worktree = project.update(cx, |project, cx| {
5400 project.find_or_create_local_worktree(path, true, cx)
5401 });
5402 if rng.lock().gen() {
5403 cx.background().spawn(find_or_create_worktree).detach();
5404 } else {
5405 find_or_create_worktree.await?;
5406 }
5407 }
5408 20..=79 if !files.is_empty() => {
5409 let buffer = if client.buffers.is_empty() || rng.lock().gen() {
5410 let file = files.choose(&mut *rng.lock()).unwrap();
5411 let (worktree, path) = project
5412 .update(cx, |project, cx| {
5413 project.find_or_create_local_worktree(file.clone(), true, cx)
5414 })
5415 .await?;
5416 let project_path =
5417 worktree.read_with(cx, |worktree, _| (worktree.id(), path));
5418 log::info!(
5419 "Host: opening path {:?}, worktree {}, relative_path {:?}",
5420 file,
5421 project_path.0,
5422 project_path.1
5423 );
5424 let buffer = project
5425 .update(cx, |project, cx| project.open_buffer(project_path, cx))
5426 .await
5427 .unwrap();
5428 client.buffers.insert(buffer.clone());
5429 buffer
5430 } else {
5431 client
5432 .buffers
5433 .iter()
5434 .choose(&mut *rng.lock())
5435 .unwrap()
5436 .clone()
5437 };
5438
5439 if rng.lock().gen_bool(0.1) {
5440 cx.update(|cx| {
5441 log::info!(
5442 "Host: dropping buffer {:?}",
5443 buffer.read(cx).file().unwrap().full_path(cx)
5444 );
5445 client.buffers.remove(&buffer);
5446 drop(buffer);
5447 });
5448 } else {
5449 buffer.update(cx, |buffer, cx| {
5450 log::info!(
5451 "Host: updating buffer {:?} ({})",
5452 buffer.file().unwrap().full_path(cx),
5453 buffer.remote_id()
5454 );
5455
5456 if rng.lock().gen_bool(0.7) {
5457 buffer.randomly_edit(&mut *rng.lock(), 5, cx);
5458 } else {
5459 buffer.randomly_undo_redo(&mut *rng.lock(), cx);
5460 }
5461 });
5462 }
5463 }
5464 _ => loop {
5465 let path_component_count = rng.lock().gen_range::<usize, _>(1..=5);
5466 let mut path = PathBuf::new();
5467 path.push("/");
5468 for _ in 0..path_component_count {
5469 let letter = rng.lock().gen_range(b'a'..=b'z');
5470 path.push(std::str::from_utf8(&[letter]).unwrap());
5471 }
5472 path.set_extension("rs");
5473 let parent_path = path.parent().unwrap();
5474
5475 log::info!("Host: creating file {:?}", path,);
5476
5477 if fs.create_dir(parent_path).await.is_ok()
5478 && fs.create_file(&path, Default::default()).await.is_ok()
5479 {
5480 break;
5481 } else {
5482 log::info!("Host: cannot create file");
5483 }
5484 },
5485 }
5486
5487 cx.background().simulate_random_delay().await;
5488 }
5489
5490 Ok(())
5491 }
5492
5493 let result =
5494 simulate_host_internal(&mut self, project.clone(), op_start_signal, rng, &mut cx).await;
5495 log::info!("Host done");
5496 (self, project, cx, result.err())
5497 }
5498
5499 pub async fn simulate_guest(
5500 mut self,
5501 guest_username: String,
5502 project: ModelHandle<Project>,
5503 op_start_signal: futures::channel::mpsc::UnboundedReceiver<()>,
5504 rng: Arc<Mutex<StdRng>>,
5505 mut cx: TestAppContext,
5506 ) -> (
5507 Self,
5508 ModelHandle<Project>,
5509 TestAppContext,
5510 Option<anyhow::Error>,
5511 ) {
5512 async fn simulate_guest_internal(
5513 client: &mut TestClient,
5514 guest_username: &str,
5515 project: ModelHandle<Project>,
5516 mut op_start_signal: futures::channel::mpsc::UnboundedReceiver<()>,
5517 rng: Arc<Mutex<StdRng>>,
5518 cx: &mut TestAppContext,
5519 ) -> anyhow::Result<()> {
5520 while op_start_signal.next().await.is_some() {
5521 let buffer = if client.buffers.is_empty() || rng.lock().gen() {
5522 let worktree = if let Some(worktree) = project.read_with(cx, |project, cx| {
5523 project
5524 .worktrees(cx)
5525 .filter(|worktree| {
5526 let worktree = worktree.read(cx);
5527 worktree.is_visible()
5528 && worktree.entries(false).any(|e| e.is_file())
5529 })
5530 .choose(&mut *rng.lock())
5531 }) {
5532 worktree
5533 } else {
5534 cx.background().simulate_random_delay().await;
5535 continue;
5536 };
5537
5538 let (worktree_root_name, project_path) =
5539 worktree.read_with(cx, |worktree, _| {
5540 let entry = worktree
5541 .entries(false)
5542 .filter(|e| e.is_file())
5543 .choose(&mut *rng.lock())
5544 .unwrap();
5545 (
5546 worktree.root_name().to_string(),
5547 (worktree.id(), entry.path.clone()),
5548 )
5549 });
5550 log::info!(
5551 "{}: opening path {:?} in worktree {} ({})",
5552 guest_username,
5553 project_path.1,
5554 project_path.0,
5555 worktree_root_name,
5556 );
5557 let buffer = project
5558 .update(cx, |project, cx| {
5559 project.open_buffer(project_path.clone(), cx)
5560 })
5561 .await?;
5562 log::info!(
5563 "{}: opened path {:?} in worktree {} ({}) with buffer id {}",
5564 guest_username,
5565 project_path.1,
5566 project_path.0,
5567 worktree_root_name,
5568 buffer.read_with(cx, |buffer, _| buffer.remote_id())
5569 );
5570 client.buffers.insert(buffer.clone());
5571 buffer
5572 } else {
5573 client
5574 .buffers
5575 .iter()
5576 .choose(&mut *rng.lock())
5577 .unwrap()
5578 .clone()
5579 };
5580
5581 let choice = rng.lock().gen_range(0..100);
5582 match choice {
5583 0..=9 => {
5584 cx.update(|cx| {
5585 log::info!(
5586 "{}: dropping buffer {:?}",
5587 guest_username,
5588 buffer.read(cx).file().unwrap().full_path(cx)
5589 );
5590 client.buffers.remove(&buffer);
5591 drop(buffer);
5592 });
5593 }
5594 10..=19 => {
5595 let completions = project.update(cx, |project, cx| {
5596 log::info!(
5597 "{}: requesting completions for buffer {} ({:?})",
5598 guest_username,
5599 buffer.read(cx).remote_id(),
5600 buffer.read(cx).file().unwrap().full_path(cx)
5601 );
5602 let offset = rng.lock().gen_range(0..=buffer.read(cx).len());
5603 project.completions(&buffer, offset, cx)
5604 });
5605 let completions = cx.background().spawn(async move {
5606 completions
5607 .await
5608 .map_err(|err| anyhow!("completions request failed: {:?}", err))
5609 });
5610 if rng.lock().gen_bool(0.3) {
5611 log::info!("{}: detaching completions request", guest_username);
5612 cx.update(|cx| completions.detach_and_log_err(cx));
5613 } else {
5614 completions.await?;
5615 }
5616 }
5617 20..=29 => {
5618 let code_actions = project.update(cx, |project, cx| {
5619 log::info!(
5620 "{}: requesting code actions for buffer {} ({:?})",
5621 guest_username,
5622 buffer.read(cx).remote_id(),
5623 buffer.read(cx).file().unwrap().full_path(cx)
5624 );
5625 let range = buffer.read(cx).random_byte_range(0, &mut *rng.lock());
5626 project.code_actions(&buffer, range, cx)
5627 });
5628 let code_actions = cx.background().spawn(async move {
5629 code_actions
5630 .await
5631 .map_err(|err| anyhow!("code actions request failed: {:?}", err))
5632 });
5633 if rng.lock().gen_bool(0.3) {
5634 log::info!("{}: detaching code actions request", guest_username);
5635 cx.update(|cx| code_actions.detach_and_log_err(cx));
5636 } else {
5637 code_actions.await?;
5638 }
5639 }
5640 30..=39 if buffer.read_with(cx, |buffer, _| buffer.is_dirty()) => {
5641 let (requested_version, save) = buffer.update(cx, |buffer, cx| {
5642 log::info!(
5643 "{}: saving buffer {} ({:?})",
5644 guest_username,
5645 buffer.remote_id(),
5646 buffer.file().unwrap().full_path(cx)
5647 );
5648 (buffer.version(), buffer.save(cx))
5649 });
5650 let save = cx.background().spawn(async move {
5651 let (saved_version, _, _) = save
5652 .await
5653 .map_err(|err| anyhow!("save request failed: {:?}", err))?;
5654 assert!(saved_version.observed_all(&requested_version));
5655 Ok::<_, anyhow::Error>(())
5656 });
5657 if rng.lock().gen_bool(0.3) {
5658 log::info!("{}: detaching save request", guest_username);
5659 cx.update(|cx| save.detach_and_log_err(cx));
5660 } else {
5661 save.await?;
5662 }
5663 }
5664 40..=44 => {
5665 let prepare_rename = project.update(cx, |project, cx| {
5666 log::info!(
5667 "{}: preparing rename for buffer {} ({:?})",
5668 guest_username,
5669 buffer.read(cx).remote_id(),
5670 buffer.read(cx).file().unwrap().full_path(cx)
5671 );
5672 let offset = rng.lock().gen_range(0..=buffer.read(cx).len());
5673 project.prepare_rename(buffer, offset, cx)
5674 });
5675 let prepare_rename = cx.background().spawn(async move {
5676 prepare_rename
5677 .await
5678 .map_err(|err| anyhow!("prepare rename request failed: {:?}", err))
5679 });
5680 if rng.lock().gen_bool(0.3) {
5681 log::info!("{}: detaching prepare rename request", guest_username);
5682 cx.update(|cx| prepare_rename.detach_and_log_err(cx));
5683 } else {
5684 prepare_rename.await?;
5685 }
5686 }
5687 45..=49 => {
5688 let definitions = project.update(cx, |project, cx| {
5689 log::info!(
5690 "{}: requesting definitions for buffer {} ({:?})",
5691 guest_username,
5692 buffer.read(cx).remote_id(),
5693 buffer.read(cx).file().unwrap().full_path(cx)
5694 );
5695 let offset = rng.lock().gen_range(0..=buffer.read(cx).len());
5696 project.definition(&buffer, offset, cx)
5697 });
5698 let definitions = cx.background().spawn(async move {
5699 definitions
5700 .await
5701 .map_err(|err| anyhow!("definitions request failed: {:?}", err))
5702 });
5703 if rng.lock().gen_bool(0.3) {
5704 log::info!("{}: detaching definitions request", guest_username);
5705 cx.update(|cx| definitions.detach_and_log_err(cx));
5706 } else {
5707 client.buffers.extend(
5708 definitions.await?.into_iter().map(|loc| loc.target.buffer),
5709 );
5710 }
5711 }
5712 50..=54 => {
5713 let highlights = project.update(cx, |project, cx| {
5714 log::info!(
5715 "{}: requesting highlights for buffer {} ({:?})",
5716 guest_username,
5717 buffer.read(cx).remote_id(),
5718 buffer.read(cx).file().unwrap().full_path(cx)
5719 );
5720 let offset = rng.lock().gen_range(0..=buffer.read(cx).len());
5721 project.document_highlights(&buffer, offset, cx)
5722 });
5723 let highlights = cx.background().spawn(async move {
5724 highlights
5725 .await
5726 .map_err(|err| anyhow!("highlights request failed: {:?}", err))
5727 });
5728 if rng.lock().gen_bool(0.3) {
5729 log::info!("{}: detaching highlights request", guest_username);
5730 cx.update(|cx| highlights.detach_and_log_err(cx));
5731 } else {
5732 highlights.await?;
5733 }
5734 }
5735 55..=59 => {
5736 let search = project.update(cx, |project, cx| {
5737 let query = rng.lock().gen_range('a'..='z');
5738 log::info!("{}: project-wide search {:?}", guest_username, query);
5739 project.search(SearchQuery::text(query, false, false), cx)
5740 });
5741 let search = cx.background().spawn(async move {
5742 search
5743 .await
5744 .map_err(|err| anyhow!("search request failed: {:?}", err))
5745 });
5746 if rng.lock().gen_bool(0.3) {
5747 log::info!("{}: detaching search request", guest_username);
5748 cx.update(|cx| search.detach_and_log_err(cx));
5749 } else {
5750 client.buffers.extend(search.await?.into_keys());
5751 }
5752 }
5753 60..=69 => {
5754 let worktree = project
5755 .read_with(cx, |project, cx| {
5756 project
5757 .worktrees(cx)
5758 .filter(|worktree| {
5759 let worktree = worktree.read(cx);
5760 worktree.is_visible()
5761 && worktree.entries(false).any(|e| e.is_file())
5762 && worktree.root_entry().map_or(false, |e| e.is_dir())
5763 })
5764 .choose(&mut *rng.lock())
5765 })
5766 .unwrap();
5767 let (worktree_id, worktree_root_name) = worktree
5768 .read_with(cx, |worktree, _| {
5769 (worktree.id(), worktree.root_name().to_string())
5770 });
5771
5772 let mut new_name = String::new();
5773 for _ in 0..10 {
5774 let letter = rng.lock().gen_range('a'..='z');
5775 new_name.push(letter);
5776 }
5777 let mut new_path = PathBuf::new();
5778 new_path.push(new_name);
5779 new_path.set_extension("rs");
5780 log::info!(
5781 "{}: creating {:?} in worktree {} ({})",
5782 guest_username,
5783 new_path,
5784 worktree_id,
5785 worktree_root_name,
5786 );
5787 project
5788 .update(cx, |project, cx| {
5789 project.create_entry((worktree_id, new_path), false, cx)
5790 })
5791 .unwrap()
5792 .await?;
5793 }
5794 _ => {
5795 buffer.update(cx, |buffer, cx| {
5796 log::info!(
5797 "{}: updating buffer {} ({:?})",
5798 guest_username,
5799 buffer.remote_id(),
5800 buffer.file().unwrap().full_path(cx)
5801 );
5802 if rng.lock().gen_bool(0.7) {
5803 buffer.randomly_edit(&mut *rng.lock(), 5, cx);
5804 } else {
5805 buffer.randomly_undo_redo(&mut *rng.lock(), cx);
5806 }
5807 });
5808 }
5809 }
5810 cx.background().simulate_random_delay().await;
5811 }
5812 Ok(())
5813 }
5814
5815 let result = simulate_guest_internal(
5816 &mut self,
5817 &guest_username,
5818 project.clone(),
5819 op_start_signal,
5820 rng,
5821 &mut cx,
5822 )
5823 .await;
5824 log::info!("{}: done", guest_username);
5825
5826 (self, project, cx, result.err())
5827 }
5828}
5829
5830impl Drop for TestClient {
5831 fn drop(&mut self) {
5832 self.client.tear_down();
5833 }
5834}
5835
5836impl Executor for Arc<gpui::executor::Background> {
5837 type Sleep = gpui::executor::Timer;
5838
5839 fn spawn_detached<F: 'static + Send + Future<Output = ()>>(&self, future: F) {
5840 self.spawn(future).detach();
5841 }
5842
5843 fn sleep(&self, duration: Duration) -> Self::Sleep {
5844 self.as_ref().timer(duration)
5845 }
5846}
5847
5848fn channel_messages(channel: &Channel) -> Vec<(String, String, bool)> {
5849 channel
5850 .messages()
5851 .cursor::<()>()
5852 .map(|m| {
5853 (
5854 m.sender.github_login.clone(),
5855 m.body.clone(),
5856 m.is_pending(),
5857 )
5858 })
5859 .collect()
5860}
5861
5862#[derive(Debug, Eq, PartialEq)]
5863struct RoomParticipants {
5864 remote: Vec<String>,
5865 pending: Vec<String>,
5866}
5867
5868async fn room_participants(
5869 room: &ModelHandle<Room>,
5870 client: &TestClient,
5871 cx: &mut TestAppContext,
5872) -> RoomParticipants {
5873 let remote_users = room.update(cx, |room, cx| {
5874 room.remote_participants()
5875 .values()
5876 .map(|participant| {
5877 client
5878 .user_store
5879 .update(cx, |users, cx| users.get_user(participant.user_id, cx))
5880 })
5881 .collect::<Vec<_>>()
5882 });
5883 let remote_users = futures::future::try_join_all(remote_users).await.unwrap();
5884 let pending_users = room.read_with(cx, |room, _| {
5885 room.pending_users().iter().cloned().collect::<Vec<_>>()
5886 });
5887
5888 RoomParticipants {
5889 remote: remote_users
5890 .into_iter()
5891 .map(|user| user.github_login.clone())
5892 .collect(),
5893 pending: pending_users
5894 .into_iter()
5895 .map(|user| user.github_login.clone())
5896 .collect(),
5897 }
5898}