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