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