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