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