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