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 worktree_a.read_with(cx_a, |worktree, _| {
2575 assert_eq!(
2576 worktree
2577 .paths()
2578 .map(|p| p.to_string_lossy())
2579 .collect::<Vec<_>>(),
2580 ["a.txt", "b.txt", "d.txt", "f.txt"]
2581 );
2582 });
2583 worktree_b.read_with(cx_b, |worktree, _| {
2584 assert_eq!(
2585 worktree
2586 .paths()
2587 .map(|p| p.to_string_lossy())
2588 .collect::<Vec<_>>(),
2589 ["a.txt", "b.txt", "d.txt", "f.txt"]
2590 );
2591 });
2592
2593 project_b
2594 .update(cx_b, |project, cx| {
2595 project.delete_entry(entry.id, cx).unwrap()
2596 })
2597 .await
2598 .unwrap();
2599 worktree_a.read_with(cx_a, |worktree, _| {
2600 assert_eq!(
2601 worktree
2602 .paths()
2603 .map(|p| p.to_string_lossy())
2604 .collect::<Vec<_>>(),
2605 ["a.txt", "b.txt", "f.txt"]
2606 );
2607 });
2608 worktree_b.read_with(cx_b, |worktree, _| {
2609 assert_eq!(
2610 worktree
2611 .paths()
2612 .map(|p| p.to_string_lossy())
2613 .collect::<Vec<_>>(),
2614 ["a.txt", "b.txt", "f.txt"]
2615 );
2616 });
2617}
2618
2619#[gpui::test(iterations = 10)]
2620async fn test_buffer_conflict_after_save(
2621 deterministic: Arc<Deterministic>,
2622 cx_a: &mut TestAppContext,
2623 cx_b: &mut TestAppContext,
2624) {
2625 deterministic.forbid_parking();
2626 let mut server = TestServer::start(&deterministic).await;
2627 let client_a = server.create_client(cx_a, "user_a").await;
2628 let client_b = server.create_client(cx_b, "user_b").await;
2629 server
2630 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2631 .await;
2632 let active_call_a = cx_a.read(ActiveCall::global);
2633
2634 client_a
2635 .fs
2636 .insert_tree(
2637 "/dir",
2638 json!({
2639 "a.txt": "a-contents",
2640 }),
2641 )
2642 .await;
2643 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
2644 let project_id = active_call_a
2645 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2646 .await
2647 .unwrap();
2648 let project_b = client_b.build_remote_project(project_id, cx_b).await;
2649
2650 // Open a buffer as client B
2651 let buffer_b = project_b
2652 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
2653 .await
2654 .unwrap();
2655
2656 buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "world ")], None, cx));
2657 buffer_b.read_with(cx_b, |buf, _| {
2658 assert!(buf.is_dirty());
2659 assert!(!buf.has_conflict());
2660 });
2661
2662 buffer_b.update(cx_b, |buf, cx| buf.save(cx)).await.unwrap();
2663 cx_a.foreground().forbid_parking();
2664 buffer_b.read_with(cx_b, |buffer_b, _| assert!(!buffer_b.is_dirty()));
2665 buffer_b.read_with(cx_b, |buf, _| {
2666 assert!(!buf.has_conflict());
2667 });
2668
2669 buffer_b.update(cx_b, |buf, cx| buf.edit([(0..0, "hello ")], None, cx));
2670 buffer_b.read_with(cx_b, |buf, _| {
2671 assert!(buf.is_dirty());
2672 assert!(!buf.has_conflict());
2673 });
2674}
2675
2676#[gpui::test(iterations = 10)]
2677async fn test_buffer_reloading(
2678 deterministic: Arc<Deterministic>,
2679 cx_a: &mut TestAppContext,
2680 cx_b: &mut TestAppContext,
2681) {
2682 deterministic.forbid_parking();
2683 let mut server = TestServer::start(&deterministic).await;
2684 let client_a = server.create_client(cx_a, "user_a").await;
2685 let client_b = server.create_client(cx_b, "user_b").await;
2686 server
2687 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2688 .await;
2689 let active_call_a = cx_a.read(ActiveCall::global);
2690
2691 client_a
2692 .fs
2693 .insert_tree(
2694 "/dir",
2695 json!({
2696 "a.txt": "a\nb\nc",
2697 }),
2698 )
2699 .await;
2700 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
2701 let project_id = active_call_a
2702 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2703 .await
2704 .unwrap();
2705 let project_b = client_b.build_remote_project(project_id, cx_b).await;
2706
2707 // Open a buffer as client B
2708 let buffer_b = project_b
2709 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
2710 .await
2711 .unwrap();
2712 buffer_b.read_with(cx_b, |buf, _| {
2713 assert!(!buf.is_dirty());
2714 assert!(!buf.has_conflict());
2715 assert_eq!(buf.line_ending(), LineEnding::Unix);
2716 });
2717
2718 let new_contents = Rope::from("d\ne\nf");
2719 client_a
2720 .fs
2721 .save("/dir/a.txt".as_ref(), &new_contents, LineEnding::Windows)
2722 .await
2723 .unwrap();
2724 cx_a.foreground().run_until_parked();
2725 buffer_b.read_with(cx_b, |buf, _| {
2726 assert_eq!(buf.text(), new_contents.to_string());
2727 assert!(!buf.is_dirty());
2728 assert!(!buf.has_conflict());
2729 assert_eq!(buf.line_ending(), LineEnding::Windows);
2730 });
2731}
2732
2733#[gpui::test(iterations = 10)]
2734async fn test_editing_while_guest_opens_buffer(
2735 deterministic: Arc<Deterministic>,
2736 cx_a: &mut TestAppContext,
2737 cx_b: &mut TestAppContext,
2738) {
2739 deterministic.forbid_parking();
2740 let mut server = TestServer::start(&deterministic).await;
2741 let client_a = server.create_client(cx_a, "user_a").await;
2742 let client_b = server.create_client(cx_b, "user_b").await;
2743 server
2744 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2745 .await;
2746 let active_call_a = cx_a.read(ActiveCall::global);
2747
2748 client_a
2749 .fs
2750 .insert_tree("/dir", json!({ "a.txt": "a-contents" }))
2751 .await;
2752 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
2753 let project_id = active_call_a
2754 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2755 .await
2756 .unwrap();
2757 let project_b = client_b.build_remote_project(project_id, cx_b).await;
2758
2759 // Open a buffer as client A
2760 let buffer_a = project_a
2761 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
2762 .await
2763 .unwrap();
2764
2765 // Start opening the same buffer as client B
2766 let buffer_b = cx_b
2767 .background()
2768 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)));
2769
2770 // Edit the buffer as client A while client B is still opening it.
2771 cx_b.background().simulate_random_delay().await;
2772 buffer_a.update(cx_a, |buf, cx| buf.edit([(0..0, "X")], None, cx));
2773 cx_b.background().simulate_random_delay().await;
2774 buffer_a.update(cx_a, |buf, cx| buf.edit([(1..1, "Y")], None, cx));
2775
2776 let text = buffer_a.read_with(cx_a, |buf, _| buf.text());
2777 let buffer_b = buffer_b.await.unwrap();
2778 cx_a.foreground().run_until_parked();
2779 buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), text));
2780}
2781
2782#[gpui::test(iterations = 10)]
2783async fn test_leaving_worktree_while_opening_buffer(
2784 deterministic: Arc<Deterministic>,
2785 cx_a: &mut TestAppContext,
2786 cx_b: &mut TestAppContext,
2787) {
2788 deterministic.forbid_parking();
2789 let mut server = TestServer::start(&deterministic).await;
2790 let client_a = server.create_client(cx_a, "user_a").await;
2791 let client_b = server.create_client(cx_b, "user_b").await;
2792 server
2793 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2794 .await;
2795 let active_call_a = cx_a.read(ActiveCall::global);
2796
2797 client_a
2798 .fs
2799 .insert_tree("/dir", json!({ "a.txt": "a-contents" }))
2800 .await;
2801 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
2802 let project_id = active_call_a
2803 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2804 .await
2805 .unwrap();
2806 let project_b = client_b.build_remote_project(project_id, cx_b).await;
2807
2808 // See that a guest has joined as client A.
2809 cx_a.foreground().run_until_parked();
2810 project_a.read_with(cx_a, |p, _| assert_eq!(p.collaborators().len(), 1));
2811
2812 // Begin opening a buffer as client B, but leave the project before the open completes.
2813 let buffer_b = cx_b
2814 .background()
2815 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)));
2816 cx_b.update(|_| drop(project_b));
2817 drop(buffer_b);
2818
2819 // See that the guest has left.
2820 cx_a.foreground().run_until_parked();
2821 project_a.read_with(cx_a, |p, _| assert!(p.collaborators().is_empty()));
2822}
2823
2824#[gpui::test(iterations = 10)]
2825async fn test_canceling_buffer_opening(
2826 deterministic: Arc<Deterministic>,
2827 cx_a: &mut TestAppContext,
2828 cx_b: &mut TestAppContext,
2829) {
2830 deterministic.forbid_parking();
2831
2832 let mut server = TestServer::start(&deterministic).await;
2833 let client_a = server.create_client(cx_a, "user_a").await;
2834 let client_b = server.create_client(cx_b, "user_b").await;
2835 server
2836 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2837 .await;
2838 let active_call_a = cx_a.read(ActiveCall::global);
2839
2840 client_a
2841 .fs
2842 .insert_tree(
2843 "/dir",
2844 json!({
2845 "a.txt": "abc",
2846 }),
2847 )
2848 .await;
2849 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
2850 let project_id = active_call_a
2851 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2852 .await
2853 .unwrap();
2854 let project_b = client_b.build_remote_project(project_id, cx_b).await;
2855
2856 let buffer_a = project_a
2857 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
2858 .await
2859 .unwrap();
2860
2861 // Open a buffer as client B but cancel after a random amount of time.
2862 let buffer_b = project_b.update(cx_b, |p, cx| p.open_buffer_by_id(buffer_a.id() as u64, cx));
2863 deterministic.simulate_random_delay().await;
2864 drop(buffer_b);
2865
2866 // Try opening the same buffer again as client B, and ensure we can
2867 // still do it despite the cancellation above.
2868 let buffer_b = project_b
2869 .update(cx_b, |p, cx| p.open_buffer_by_id(buffer_a.id() as u64, cx))
2870 .await
2871 .unwrap();
2872 buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "abc"));
2873}
2874
2875#[gpui::test(iterations = 10)]
2876async fn test_leaving_project(
2877 deterministic: Arc<Deterministic>,
2878 cx_a: &mut TestAppContext,
2879 cx_b: &mut TestAppContext,
2880 cx_c: &mut TestAppContext,
2881) {
2882 deterministic.forbid_parking();
2883 let mut server = TestServer::start(&deterministic).await;
2884 let client_a = server.create_client(cx_a, "user_a").await;
2885 let client_b = server.create_client(cx_b, "user_b").await;
2886 let client_c = server.create_client(cx_c, "user_c").await;
2887 server
2888 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
2889 .await;
2890 let active_call_a = cx_a.read(ActiveCall::global);
2891
2892 client_a
2893 .fs
2894 .insert_tree(
2895 "/a",
2896 json!({
2897 "a.txt": "a-contents",
2898 "b.txt": "b-contents",
2899 }),
2900 )
2901 .await;
2902 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
2903 let project_id = active_call_a
2904 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2905 .await
2906 .unwrap();
2907 let project_b1 = client_b.build_remote_project(project_id, cx_b).await;
2908 let project_c = client_c.build_remote_project(project_id, cx_c).await;
2909
2910 // Client A sees that a guest has joined.
2911 deterministic.run_until_parked();
2912 project_a.read_with(cx_a, |project, _| {
2913 assert_eq!(project.collaborators().len(), 2);
2914 });
2915 project_b1.read_with(cx_b, |project, _| {
2916 assert_eq!(project.collaborators().len(), 2);
2917 });
2918 project_c.read_with(cx_c, |project, _| {
2919 assert_eq!(project.collaborators().len(), 2);
2920 });
2921
2922 // Client B opens a buffer.
2923 let buffer_b1 = project_b1
2924 .update(cx_b, |project, cx| {
2925 let worktree_id = project.worktrees(cx).next().unwrap().read(cx).id();
2926 project.open_buffer((worktree_id, "a.txt"), cx)
2927 })
2928 .await
2929 .unwrap();
2930 buffer_b1.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "a-contents"));
2931
2932 // Drop client B's project and ensure client A and client C observe client B leaving.
2933 cx_b.update(|_| drop(project_b1));
2934 deterministic.run_until_parked();
2935 project_a.read_with(cx_a, |project, _| {
2936 assert_eq!(project.collaborators().len(), 1);
2937 });
2938 project_c.read_with(cx_c, |project, _| {
2939 assert_eq!(project.collaborators().len(), 1);
2940 });
2941
2942 // Client B re-joins the project and can open buffers as before.
2943 let project_b2 = client_b.build_remote_project(project_id, cx_b).await;
2944 deterministic.run_until_parked();
2945 project_a.read_with(cx_a, |project, _| {
2946 assert_eq!(project.collaborators().len(), 2);
2947 });
2948 project_b2.read_with(cx_b, |project, _| {
2949 assert_eq!(project.collaborators().len(), 2);
2950 });
2951 project_c.read_with(cx_c, |project, _| {
2952 assert_eq!(project.collaborators().len(), 2);
2953 });
2954
2955 let buffer_b2 = project_b2
2956 .update(cx_b, |project, cx| {
2957 let worktree_id = project.worktrees(cx).next().unwrap().read(cx).id();
2958 project.open_buffer((worktree_id, "a.txt"), cx)
2959 })
2960 .await
2961 .unwrap();
2962 buffer_b2.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "a-contents"));
2963
2964 // Drop client B's connection and ensure client A and client C observe client B leaving.
2965 client_b.disconnect(&cx_b.to_async()).unwrap();
2966 deterministic.advance_clock(RECONNECT_TIMEOUT);
2967 project_a.read_with(cx_a, |project, _| {
2968 assert_eq!(project.collaborators().len(), 1);
2969 });
2970 project_b2.read_with(cx_b, |project, _| {
2971 assert!(project.is_read_only());
2972 });
2973 project_c.read_with(cx_c, |project, _| {
2974 assert_eq!(project.collaborators().len(), 1);
2975 });
2976
2977 // Client B can't join the project, unless they re-join the room.
2978 cx_b.spawn(|cx| {
2979 Project::remote(
2980 project_id,
2981 client_b.client.clone(),
2982 client_b.user_store.clone(),
2983 client_b.language_registry.clone(),
2984 FakeFs::new(cx.background()),
2985 cx,
2986 )
2987 })
2988 .await
2989 .unwrap_err();
2990
2991 // Simulate connection loss for client C and ensure client A observes client C leaving the project.
2992 client_c.wait_for_current_user(cx_c).await;
2993 server.forbid_connections();
2994 server.disconnect_client(client_c.peer_id().unwrap());
2995 deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
2996 deterministic.run_until_parked();
2997 project_a.read_with(cx_a, |project, _| {
2998 assert_eq!(project.collaborators().len(), 0);
2999 });
3000 project_b2.read_with(cx_b, |project, _| {
3001 assert!(project.is_read_only());
3002 });
3003 project_c.read_with(cx_c, |project, _| {
3004 assert!(project.is_read_only());
3005 });
3006}
3007
3008#[gpui::test(iterations = 10)]
3009async fn test_collaborating_with_diagnostics(
3010 deterministic: Arc<Deterministic>,
3011 cx_a: &mut TestAppContext,
3012 cx_b: &mut TestAppContext,
3013 cx_c: &mut TestAppContext,
3014) {
3015 deterministic.forbid_parking();
3016 let mut server = TestServer::start(&deterministic).await;
3017 let client_a = server.create_client(cx_a, "user_a").await;
3018 let client_b = server.create_client(cx_b, "user_b").await;
3019 let client_c = server.create_client(cx_c, "user_c").await;
3020 server
3021 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
3022 .await;
3023 let active_call_a = cx_a.read(ActiveCall::global);
3024
3025 // Set up a fake language server.
3026 let mut language = Language::new(
3027 LanguageConfig {
3028 name: "Rust".into(),
3029 path_suffixes: vec!["rs".to_string()],
3030 ..Default::default()
3031 },
3032 Some(tree_sitter_rust::language()),
3033 );
3034 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
3035 client_a.language_registry.add(Arc::new(language));
3036
3037 // Share a project as client A
3038 client_a
3039 .fs
3040 .insert_tree(
3041 "/a",
3042 json!({
3043 "a.rs": "let one = two",
3044 "other.rs": "",
3045 }),
3046 )
3047 .await;
3048 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
3049
3050 // Cause the language server to start.
3051 let _buffer = project_a
3052 .update(cx_a, |project, cx| {
3053 project.open_buffer(
3054 ProjectPath {
3055 worktree_id,
3056 path: Path::new("other.rs").into(),
3057 },
3058 cx,
3059 )
3060 })
3061 .await
3062 .unwrap();
3063
3064 // Simulate a language server reporting errors for a file.
3065 let mut fake_language_server = fake_language_servers.next().await.unwrap();
3066 fake_language_server
3067 .receive_notification::<lsp::notification::DidOpenTextDocument>()
3068 .await;
3069 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3070 lsp::PublishDiagnosticsParams {
3071 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
3072 version: None,
3073 diagnostics: vec![lsp::Diagnostic {
3074 severity: Some(lsp::DiagnosticSeverity::WARNING),
3075 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
3076 message: "message 0".to_string(),
3077 ..Default::default()
3078 }],
3079 },
3080 );
3081
3082 // Client A shares the project and, simultaneously, the language server
3083 // publishes a diagnostic. This is done to ensure that the server always
3084 // observes the latest diagnostics for a worktree.
3085 let project_id = active_call_a
3086 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3087 .await
3088 .unwrap();
3089 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3090 lsp::PublishDiagnosticsParams {
3091 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
3092 version: None,
3093 diagnostics: vec![lsp::Diagnostic {
3094 severity: Some(lsp::DiagnosticSeverity::ERROR),
3095 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
3096 message: "message 1".to_string(),
3097 ..Default::default()
3098 }],
3099 },
3100 );
3101
3102 // Join the worktree as client B.
3103 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3104
3105 // Wait for server to see the diagnostics update.
3106 deterministic.run_until_parked();
3107
3108 // Ensure client B observes the new diagnostics.
3109 project_b.read_with(cx_b, |project, cx| {
3110 assert_eq!(
3111 project.diagnostic_summaries(cx).collect::<Vec<_>>(),
3112 &[(
3113 ProjectPath {
3114 worktree_id,
3115 path: Arc::from(Path::new("a.rs")),
3116 },
3117 DiagnosticSummary {
3118 error_count: 1,
3119 warning_count: 0,
3120 ..Default::default()
3121 },
3122 )]
3123 )
3124 });
3125
3126 // Join project as client C and observe the diagnostics.
3127 let project_c = client_c.build_remote_project(project_id, cx_c).await;
3128 let project_c_diagnostic_summaries =
3129 Rc::new(RefCell::new(project_c.read_with(cx_c, |project, cx| {
3130 project.diagnostic_summaries(cx).collect::<Vec<_>>()
3131 })));
3132 project_c.update(cx_c, |_, cx| {
3133 let summaries = project_c_diagnostic_summaries.clone();
3134 cx.subscribe(&project_c, {
3135 move |p, _, event, cx| {
3136 if let project::Event::DiskBasedDiagnosticsFinished { .. } = event {
3137 *summaries.borrow_mut() = p.diagnostic_summaries(cx).collect();
3138 }
3139 }
3140 })
3141 .detach();
3142 });
3143
3144 deterministic.run_until_parked();
3145 assert_eq!(
3146 project_c_diagnostic_summaries.borrow().as_slice(),
3147 &[(
3148 ProjectPath {
3149 worktree_id,
3150 path: Arc::from(Path::new("a.rs")),
3151 },
3152 DiagnosticSummary {
3153 error_count: 1,
3154 warning_count: 0,
3155 ..Default::default()
3156 },
3157 )]
3158 );
3159
3160 // Simulate a language server reporting more errors for a file.
3161 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3162 lsp::PublishDiagnosticsParams {
3163 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
3164 version: None,
3165 diagnostics: vec![
3166 lsp::Diagnostic {
3167 severity: Some(lsp::DiagnosticSeverity::ERROR),
3168 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
3169 message: "message 1".to_string(),
3170 ..Default::default()
3171 },
3172 lsp::Diagnostic {
3173 severity: Some(lsp::DiagnosticSeverity::WARNING),
3174 range: lsp::Range::new(lsp::Position::new(0, 10), lsp::Position::new(0, 13)),
3175 message: "message 2".to_string(),
3176 ..Default::default()
3177 },
3178 ],
3179 },
3180 );
3181
3182 // Clients B and C get the updated summaries
3183 deterministic.run_until_parked();
3184 project_b.read_with(cx_b, |project, cx| {
3185 assert_eq!(
3186 project.diagnostic_summaries(cx).collect::<Vec<_>>(),
3187 [(
3188 ProjectPath {
3189 worktree_id,
3190 path: Arc::from(Path::new("a.rs")),
3191 },
3192 DiagnosticSummary {
3193 error_count: 1,
3194 warning_count: 1,
3195 ..Default::default()
3196 },
3197 )]
3198 );
3199 });
3200 project_c.read_with(cx_c, |project, cx| {
3201 assert_eq!(
3202 project.diagnostic_summaries(cx).collect::<Vec<_>>(),
3203 [(
3204 ProjectPath {
3205 worktree_id,
3206 path: Arc::from(Path::new("a.rs")),
3207 },
3208 DiagnosticSummary {
3209 error_count: 1,
3210 warning_count: 1,
3211 ..Default::default()
3212 },
3213 )]
3214 );
3215 });
3216
3217 // Open the file with the errors on client B. They should be present.
3218 let buffer_b = cx_b
3219 .background()
3220 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
3221 .await
3222 .unwrap();
3223
3224 buffer_b.read_with(cx_b, |buffer, _| {
3225 assert_eq!(
3226 buffer
3227 .snapshot()
3228 .diagnostics_in_range::<_, Point>(0..buffer.len(), false)
3229 .collect::<Vec<_>>(),
3230 &[
3231 DiagnosticEntry {
3232 range: Point::new(0, 4)..Point::new(0, 7),
3233 diagnostic: Diagnostic {
3234 group_id: 2,
3235 message: "message 1".to_string(),
3236 severity: lsp::DiagnosticSeverity::ERROR,
3237 is_primary: true,
3238 ..Default::default()
3239 }
3240 },
3241 DiagnosticEntry {
3242 range: Point::new(0, 10)..Point::new(0, 13),
3243 diagnostic: Diagnostic {
3244 group_id: 3,
3245 severity: lsp::DiagnosticSeverity::WARNING,
3246 message: "message 2".to_string(),
3247 is_primary: true,
3248 ..Default::default()
3249 }
3250 }
3251 ]
3252 );
3253 });
3254
3255 // Simulate a language server reporting no errors for a file.
3256 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3257 lsp::PublishDiagnosticsParams {
3258 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
3259 version: None,
3260 diagnostics: vec![],
3261 },
3262 );
3263 deterministic.run_until_parked();
3264 project_a.read_with(cx_a, |project, cx| {
3265 assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
3266 });
3267 project_b.read_with(cx_b, |project, cx| {
3268 assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
3269 });
3270 project_c.read_with(cx_c, |project, cx| {
3271 assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
3272 });
3273}
3274
3275#[gpui::test(iterations = 10)]
3276async fn test_collaborating_with_completion(
3277 deterministic: Arc<Deterministic>,
3278 cx_a: &mut TestAppContext,
3279 cx_b: &mut TestAppContext,
3280) {
3281 deterministic.forbid_parking();
3282 let mut server = TestServer::start(&deterministic).await;
3283 let client_a = server.create_client(cx_a, "user_a").await;
3284 let client_b = server.create_client(cx_b, "user_b").await;
3285 server
3286 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3287 .await;
3288 let active_call_a = cx_a.read(ActiveCall::global);
3289
3290 // Set up a fake language server.
3291 let mut language = Language::new(
3292 LanguageConfig {
3293 name: "Rust".into(),
3294 path_suffixes: vec!["rs".to_string()],
3295 ..Default::default()
3296 },
3297 Some(tree_sitter_rust::language()),
3298 );
3299 let mut fake_language_servers = language
3300 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
3301 capabilities: lsp::ServerCapabilities {
3302 completion_provider: Some(lsp::CompletionOptions {
3303 trigger_characters: Some(vec![".".to_string()]),
3304 ..Default::default()
3305 }),
3306 ..Default::default()
3307 },
3308 ..Default::default()
3309 }))
3310 .await;
3311 client_a.language_registry.add(Arc::new(language));
3312
3313 client_a
3314 .fs
3315 .insert_tree(
3316 "/a",
3317 json!({
3318 "main.rs": "fn main() { a }",
3319 "other.rs": "",
3320 }),
3321 )
3322 .await;
3323 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
3324 let project_id = active_call_a
3325 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3326 .await
3327 .unwrap();
3328 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3329
3330 // Open a file in an editor as the guest.
3331 let buffer_b = project_b
3332 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
3333 .await
3334 .unwrap();
3335 let (_, window_b) = cx_b.add_window(|_| EmptyView);
3336 let editor_b = cx_b.add_view(&window_b, |cx| {
3337 Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), cx)
3338 });
3339
3340 let fake_language_server = fake_language_servers.next().await.unwrap();
3341 cx_a.foreground().run_until_parked();
3342 buffer_b.read_with(cx_b, |buffer, _| {
3343 assert!(!buffer.completion_triggers().is_empty())
3344 });
3345
3346 // Type a completion trigger character as the guest.
3347 editor_b.update(cx_b, |editor, cx| {
3348 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
3349 editor.handle_input(".", cx);
3350 cx.focus(&editor_b);
3351 });
3352
3353 // Receive a completion request as the host's language server.
3354 // Return some completions from the host's language server.
3355 cx_a.foreground().start_waiting();
3356 fake_language_server
3357 .handle_request::<lsp::request::Completion, _, _>(|params, _| async move {
3358 assert_eq!(
3359 params.text_document_position.text_document.uri,
3360 lsp::Url::from_file_path("/a/main.rs").unwrap(),
3361 );
3362 assert_eq!(
3363 params.text_document_position.position,
3364 lsp::Position::new(0, 14),
3365 );
3366
3367 Ok(Some(lsp::CompletionResponse::Array(vec![
3368 lsp::CompletionItem {
3369 label: "first_method(…)".into(),
3370 detail: Some("fn(&mut self, B) -> C".into()),
3371 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
3372 new_text: "first_method($1)".to_string(),
3373 range: lsp::Range::new(
3374 lsp::Position::new(0, 14),
3375 lsp::Position::new(0, 14),
3376 ),
3377 })),
3378 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
3379 ..Default::default()
3380 },
3381 lsp::CompletionItem {
3382 label: "second_method(…)".into(),
3383 detail: Some("fn(&mut self, C) -> D<E>".into()),
3384 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
3385 new_text: "second_method()".to_string(),
3386 range: lsp::Range::new(
3387 lsp::Position::new(0, 14),
3388 lsp::Position::new(0, 14),
3389 ),
3390 })),
3391 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
3392 ..Default::default()
3393 },
3394 ])))
3395 })
3396 .next()
3397 .await
3398 .unwrap();
3399 cx_a.foreground().finish_waiting();
3400
3401 // Open the buffer on the host.
3402 let buffer_a = project_a
3403 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
3404 .await
3405 .unwrap();
3406 cx_a.foreground().run_until_parked();
3407 buffer_a.read_with(cx_a, |buffer, _| {
3408 assert_eq!(buffer.text(), "fn main() { a. }")
3409 });
3410
3411 // Confirm a completion on the guest.
3412 editor_b.read_with(cx_b, |editor, _| assert!(editor.context_menu_visible()));
3413 editor_b.update(cx_b, |editor, cx| {
3414 editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx);
3415 assert_eq!(editor.text(cx), "fn main() { a.first_method() }");
3416 });
3417
3418 // Return a resolved completion from the host's language server.
3419 // The resolved completion has an additional text edit.
3420 fake_language_server.handle_request::<lsp::request::ResolveCompletionItem, _, _>(
3421 |params, _| async move {
3422 assert_eq!(params.label, "first_method(…)");
3423 Ok(lsp::CompletionItem {
3424 label: "first_method(…)".into(),
3425 detail: Some("fn(&mut self, B) -> C".into()),
3426 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
3427 new_text: "first_method($1)".to_string(),
3428 range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
3429 })),
3430 additional_text_edits: Some(vec![lsp::TextEdit {
3431 new_text: "use d::SomeTrait;\n".to_string(),
3432 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
3433 }]),
3434 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
3435 ..Default::default()
3436 })
3437 },
3438 );
3439
3440 // The additional edit is applied.
3441 cx_a.foreground().run_until_parked();
3442 buffer_a.read_with(cx_a, |buffer, _| {
3443 assert_eq!(
3444 buffer.text(),
3445 "use d::SomeTrait;\nfn main() { a.first_method() }"
3446 );
3447 });
3448 buffer_b.read_with(cx_b, |buffer, _| {
3449 assert_eq!(
3450 buffer.text(),
3451 "use d::SomeTrait;\nfn main() { a.first_method() }"
3452 );
3453 });
3454}
3455
3456#[gpui::test(iterations = 10)]
3457async fn test_reloading_buffer_manually(
3458 deterministic: Arc<Deterministic>,
3459 cx_a: &mut TestAppContext,
3460 cx_b: &mut TestAppContext,
3461) {
3462 deterministic.forbid_parking();
3463 let mut server = TestServer::start(&deterministic).await;
3464 let client_a = server.create_client(cx_a, "user_a").await;
3465 let client_b = server.create_client(cx_b, "user_b").await;
3466 server
3467 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3468 .await;
3469 let active_call_a = cx_a.read(ActiveCall::global);
3470
3471 client_a
3472 .fs
3473 .insert_tree("/a", json!({ "a.rs": "let one = 1;" }))
3474 .await;
3475 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
3476 let buffer_a = project_a
3477 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))
3478 .await
3479 .unwrap();
3480 let project_id = active_call_a
3481 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3482 .await
3483 .unwrap();
3484
3485 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3486
3487 let buffer_b = cx_b
3488 .background()
3489 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
3490 .await
3491 .unwrap();
3492 buffer_b.update(cx_b, |buffer, cx| {
3493 buffer.edit([(4..7, "six")], None, cx);
3494 buffer.edit([(10..11, "6")], None, cx);
3495 assert_eq!(buffer.text(), "let six = 6;");
3496 assert!(buffer.is_dirty());
3497 assert!(!buffer.has_conflict());
3498 });
3499 cx_a.foreground().run_until_parked();
3500 buffer_a.read_with(cx_a, |buffer, _| assert_eq!(buffer.text(), "let six = 6;"));
3501
3502 client_a
3503 .fs
3504 .save(
3505 "/a/a.rs".as_ref(),
3506 &Rope::from("let seven = 7;"),
3507 LineEnding::Unix,
3508 )
3509 .await
3510 .unwrap();
3511 cx_a.foreground().run_until_parked();
3512 buffer_a.read_with(cx_a, |buffer, _| assert!(buffer.has_conflict()));
3513 buffer_b.read_with(cx_b, |buffer, _| assert!(buffer.has_conflict()));
3514
3515 project_b
3516 .update(cx_b, |project, cx| {
3517 project.reload_buffers(HashSet::from_iter([buffer_b.clone()]), true, cx)
3518 })
3519 .await
3520 .unwrap();
3521 buffer_a.read_with(cx_a, |buffer, _| {
3522 assert_eq!(buffer.text(), "let seven = 7;");
3523 assert!(!buffer.is_dirty());
3524 assert!(!buffer.has_conflict());
3525 });
3526 buffer_b.read_with(cx_b, |buffer, _| {
3527 assert_eq!(buffer.text(), "let seven = 7;");
3528 assert!(!buffer.is_dirty());
3529 assert!(!buffer.has_conflict());
3530 });
3531
3532 buffer_a.update(cx_a, |buffer, cx| {
3533 // Undoing on the host is a no-op when the reload was initiated by the guest.
3534 buffer.undo(cx);
3535 assert_eq!(buffer.text(), "let seven = 7;");
3536 assert!(!buffer.is_dirty());
3537 assert!(!buffer.has_conflict());
3538 });
3539 buffer_b.update(cx_b, |buffer, cx| {
3540 // Undoing on the guest rolls back the buffer to before it was reloaded but the conflict gets cleared.
3541 buffer.undo(cx);
3542 assert_eq!(buffer.text(), "let six = 6;");
3543 assert!(buffer.is_dirty());
3544 assert!(!buffer.has_conflict());
3545 });
3546}
3547
3548#[gpui::test(iterations = 10)]
3549async fn test_formatting_buffer(
3550 deterministic: Arc<Deterministic>,
3551 cx_a: &mut TestAppContext,
3552 cx_b: &mut TestAppContext,
3553) {
3554 use project::FormatTrigger;
3555
3556 let mut server = TestServer::start(&deterministic).await;
3557 let client_a = server.create_client(cx_a, "user_a").await;
3558 let client_b = server.create_client(cx_b, "user_b").await;
3559 server
3560 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3561 .await;
3562 let active_call_a = cx_a.read(ActiveCall::global);
3563
3564 // Set up a fake language server.
3565 let mut language = Language::new(
3566 LanguageConfig {
3567 name: "Rust".into(),
3568 path_suffixes: vec!["rs".to_string()],
3569 ..Default::default()
3570 },
3571 Some(tree_sitter_rust::language()),
3572 );
3573 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
3574 client_a.language_registry.add(Arc::new(language));
3575
3576 // Here we insert a fake tree with a directory that exists on disk. This is needed
3577 // because later we'll invoke a command, which requires passing a working directory
3578 // that points to a valid location on disk.
3579 let directory = env::current_dir().unwrap();
3580 client_a
3581 .fs
3582 .insert_tree(&directory, json!({ "a.rs": "let one = \"two\"" }))
3583 .await;
3584 let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await;
3585 let project_id = active_call_a
3586 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3587 .await
3588 .unwrap();
3589 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3590
3591 let buffer_b = cx_b
3592 .background()
3593 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
3594 .await
3595 .unwrap();
3596
3597 let fake_language_server = fake_language_servers.next().await.unwrap();
3598 fake_language_server.handle_request::<lsp::request::Formatting, _, _>(|_, _| async move {
3599 Ok(Some(vec![
3600 lsp::TextEdit {
3601 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 4)),
3602 new_text: "h".to_string(),
3603 },
3604 lsp::TextEdit {
3605 range: lsp::Range::new(lsp::Position::new(0, 7), lsp::Position::new(0, 7)),
3606 new_text: "y".to_string(),
3607 },
3608 ]))
3609 });
3610
3611 project_b
3612 .update(cx_b, |project, cx| {
3613 project.format(
3614 HashSet::from_iter([buffer_b.clone()]),
3615 true,
3616 FormatTrigger::Save,
3617 cx,
3618 )
3619 })
3620 .await
3621 .unwrap();
3622 assert_eq!(
3623 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
3624 "let honey = \"two\""
3625 );
3626
3627 // Ensure buffer can be formatted using an external command. Notice how the
3628 // host's configuration is honored as opposed to using the guest's settings.
3629 cx_a.update(|cx| {
3630 cx.update_global(|settings: &mut Settings, _| {
3631 settings.editor_defaults.formatter = Some(Formatter::External {
3632 command: "awk".to_string(),
3633 arguments: vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()],
3634 });
3635 });
3636 });
3637 project_b
3638 .update(cx_b, |project, cx| {
3639 project.format(
3640 HashSet::from_iter([buffer_b.clone()]),
3641 true,
3642 FormatTrigger::Save,
3643 cx,
3644 )
3645 })
3646 .await
3647 .unwrap();
3648 assert_eq!(
3649 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
3650 format!("let honey = \"{}/a.rs\"\n", directory.to_str().unwrap())
3651 );
3652}
3653
3654#[gpui::test(iterations = 10)]
3655async fn test_definition(
3656 deterministic: Arc<Deterministic>,
3657 cx_a: &mut TestAppContext,
3658 cx_b: &mut TestAppContext,
3659) {
3660 deterministic.forbid_parking();
3661 let mut server = TestServer::start(&deterministic).await;
3662 let client_a = server.create_client(cx_a, "user_a").await;
3663 let client_b = server.create_client(cx_b, "user_b").await;
3664 server
3665 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3666 .await;
3667 let active_call_a = cx_a.read(ActiveCall::global);
3668
3669 // Set up a fake language server.
3670 let mut language = Language::new(
3671 LanguageConfig {
3672 name: "Rust".into(),
3673 path_suffixes: vec!["rs".to_string()],
3674 ..Default::default()
3675 },
3676 Some(tree_sitter_rust::language()),
3677 );
3678 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
3679 client_a.language_registry.add(Arc::new(language));
3680
3681 client_a
3682 .fs
3683 .insert_tree(
3684 "/root",
3685 json!({
3686 "dir-1": {
3687 "a.rs": "const ONE: usize = b::TWO + b::THREE;",
3688 },
3689 "dir-2": {
3690 "b.rs": "const TWO: c::T2 = 2;\nconst THREE: usize = 3;",
3691 "c.rs": "type T2 = usize;",
3692 }
3693 }),
3694 )
3695 .await;
3696 let (project_a, worktree_id) = client_a.build_local_project("/root/dir-1", cx_a).await;
3697 let project_id = active_call_a
3698 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3699 .await
3700 .unwrap();
3701 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3702
3703 // Open the file on client B.
3704 let buffer_b = cx_b
3705 .background()
3706 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
3707 .await
3708 .unwrap();
3709
3710 // Request the definition of a symbol as the guest.
3711 let fake_language_server = fake_language_servers.next().await.unwrap();
3712 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
3713 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
3714 lsp::Location::new(
3715 lsp::Url::from_file_path("/root/dir-2/b.rs").unwrap(),
3716 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
3717 ),
3718 )))
3719 });
3720
3721 let definitions_1 = project_b
3722 .update(cx_b, |p, cx| p.definition(&buffer_b, 23, cx))
3723 .await
3724 .unwrap();
3725 cx_b.read(|cx| {
3726 assert_eq!(definitions_1.len(), 1);
3727 assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
3728 let target_buffer = definitions_1[0].target.buffer.read(cx);
3729 assert_eq!(
3730 target_buffer.text(),
3731 "const TWO: c::T2 = 2;\nconst THREE: usize = 3;"
3732 );
3733 assert_eq!(
3734 definitions_1[0].target.range.to_point(target_buffer),
3735 Point::new(0, 6)..Point::new(0, 9)
3736 );
3737 });
3738
3739 // Try getting more definitions for the same buffer, ensuring the buffer gets reused from
3740 // the previous call to `definition`.
3741 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
3742 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
3743 lsp::Location::new(
3744 lsp::Url::from_file_path("/root/dir-2/b.rs").unwrap(),
3745 lsp::Range::new(lsp::Position::new(1, 6), lsp::Position::new(1, 11)),
3746 ),
3747 )))
3748 });
3749
3750 let definitions_2 = project_b
3751 .update(cx_b, |p, cx| p.definition(&buffer_b, 33, cx))
3752 .await
3753 .unwrap();
3754 cx_b.read(|cx| {
3755 assert_eq!(definitions_2.len(), 1);
3756 assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
3757 let target_buffer = definitions_2[0].target.buffer.read(cx);
3758 assert_eq!(
3759 target_buffer.text(),
3760 "const TWO: c::T2 = 2;\nconst THREE: usize = 3;"
3761 );
3762 assert_eq!(
3763 definitions_2[0].target.range.to_point(target_buffer),
3764 Point::new(1, 6)..Point::new(1, 11)
3765 );
3766 });
3767 assert_eq!(
3768 definitions_1[0].target.buffer,
3769 definitions_2[0].target.buffer
3770 );
3771
3772 fake_language_server.handle_request::<lsp::request::GotoTypeDefinition, _, _>(
3773 |req, _| async move {
3774 assert_eq!(
3775 req.text_document_position_params.position,
3776 lsp::Position::new(0, 7)
3777 );
3778 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
3779 lsp::Location::new(
3780 lsp::Url::from_file_path("/root/dir-2/c.rs").unwrap(),
3781 lsp::Range::new(lsp::Position::new(0, 5), lsp::Position::new(0, 7)),
3782 ),
3783 )))
3784 },
3785 );
3786
3787 let type_definitions = project_b
3788 .update(cx_b, |p, cx| p.type_definition(&buffer_b, 7, cx))
3789 .await
3790 .unwrap();
3791 cx_b.read(|cx| {
3792 assert_eq!(type_definitions.len(), 1);
3793 let target_buffer = type_definitions[0].target.buffer.read(cx);
3794 assert_eq!(target_buffer.text(), "type T2 = usize;");
3795 assert_eq!(
3796 type_definitions[0].target.range.to_point(target_buffer),
3797 Point::new(0, 5)..Point::new(0, 7)
3798 );
3799 });
3800}
3801
3802#[gpui::test(iterations = 10)]
3803async fn test_references(
3804 deterministic: Arc<Deterministic>,
3805 cx_a: &mut TestAppContext,
3806 cx_b: &mut TestAppContext,
3807) {
3808 deterministic.forbid_parking();
3809 let mut server = TestServer::start(&deterministic).await;
3810 let client_a = server.create_client(cx_a, "user_a").await;
3811 let client_b = server.create_client(cx_b, "user_b").await;
3812 server
3813 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3814 .await;
3815 let active_call_a = cx_a.read(ActiveCall::global);
3816
3817 // Set up a fake language server.
3818 let mut language = Language::new(
3819 LanguageConfig {
3820 name: "Rust".into(),
3821 path_suffixes: vec!["rs".to_string()],
3822 ..Default::default()
3823 },
3824 Some(tree_sitter_rust::language()),
3825 );
3826 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
3827 client_a.language_registry.add(Arc::new(language));
3828
3829 client_a
3830 .fs
3831 .insert_tree(
3832 "/root",
3833 json!({
3834 "dir-1": {
3835 "one.rs": "const ONE: usize = 1;",
3836 "two.rs": "const TWO: usize = one::ONE + one::ONE;",
3837 },
3838 "dir-2": {
3839 "three.rs": "const THREE: usize = two::TWO + one::ONE;",
3840 }
3841 }),
3842 )
3843 .await;
3844 let (project_a, worktree_id) = client_a.build_local_project("/root/dir-1", cx_a).await;
3845 let project_id = active_call_a
3846 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3847 .await
3848 .unwrap();
3849 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3850
3851 // Open the file on client B.
3852 let buffer_b = cx_b
3853 .background()
3854 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx)))
3855 .await
3856 .unwrap();
3857
3858 // Request references to a symbol as the guest.
3859 let fake_language_server = fake_language_servers.next().await.unwrap();
3860 fake_language_server.handle_request::<lsp::request::References, _, _>(|params, _| async move {
3861 assert_eq!(
3862 params.text_document_position.text_document.uri.as_str(),
3863 "file:///root/dir-1/one.rs"
3864 );
3865 Ok(Some(vec![
3866 lsp::Location {
3867 uri: lsp::Url::from_file_path("/root/dir-1/two.rs").unwrap(),
3868 range: lsp::Range::new(lsp::Position::new(0, 24), lsp::Position::new(0, 27)),
3869 },
3870 lsp::Location {
3871 uri: lsp::Url::from_file_path("/root/dir-1/two.rs").unwrap(),
3872 range: lsp::Range::new(lsp::Position::new(0, 35), lsp::Position::new(0, 38)),
3873 },
3874 lsp::Location {
3875 uri: lsp::Url::from_file_path("/root/dir-2/three.rs").unwrap(),
3876 range: lsp::Range::new(lsp::Position::new(0, 37), lsp::Position::new(0, 40)),
3877 },
3878 ]))
3879 });
3880
3881 let references = project_b
3882 .update(cx_b, |p, cx| p.references(&buffer_b, 7, cx))
3883 .await
3884 .unwrap();
3885 cx_b.read(|cx| {
3886 assert_eq!(references.len(), 3);
3887 assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
3888
3889 let two_buffer = references[0].buffer.read(cx);
3890 let three_buffer = references[2].buffer.read(cx);
3891 assert_eq!(
3892 two_buffer.file().unwrap().path().as_ref(),
3893 Path::new("two.rs")
3894 );
3895 assert_eq!(references[1].buffer, references[0].buffer);
3896 assert_eq!(
3897 three_buffer.file().unwrap().full_path(cx),
3898 Path::new("/root/dir-2/three.rs")
3899 );
3900
3901 assert_eq!(references[0].range.to_offset(two_buffer), 24..27);
3902 assert_eq!(references[1].range.to_offset(two_buffer), 35..38);
3903 assert_eq!(references[2].range.to_offset(three_buffer), 37..40);
3904 });
3905}
3906
3907#[gpui::test(iterations = 10)]
3908async fn test_project_search(
3909 deterministic: Arc<Deterministic>,
3910 cx_a: &mut TestAppContext,
3911 cx_b: &mut TestAppContext,
3912) {
3913 deterministic.forbid_parking();
3914 let mut server = TestServer::start(&deterministic).await;
3915 let client_a = server.create_client(cx_a, "user_a").await;
3916 let client_b = server.create_client(cx_b, "user_b").await;
3917 server
3918 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3919 .await;
3920 let active_call_a = cx_a.read(ActiveCall::global);
3921
3922 client_a
3923 .fs
3924 .insert_tree(
3925 "/root",
3926 json!({
3927 "dir-1": {
3928 "a": "hello world",
3929 "b": "goodnight moon",
3930 "c": "a world of goo",
3931 "d": "world champion of clown world",
3932 },
3933 "dir-2": {
3934 "e": "disney world is fun",
3935 }
3936 }),
3937 )
3938 .await;
3939 let (project_a, _) = client_a.build_local_project("/root/dir-1", cx_a).await;
3940 let (worktree_2, _) = project_a
3941 .update(cx_a, |p, cx| {
3942 p.find_or_create_local_worktree("/root/dir-2", true, cx)
3943 })
3944 .await
3945 .unwrap();
3946 worktree_2
3947 .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
3948 .await;
3949 let project_id = active_call_a
3950 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3951 .await
3952 .unwrap();
3953
3954 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3955
3956 // Perform a search as the guest.
3957 let results = project_b
3958 .update(cx_b, |project, cx| {
3959 project.search(SearchQuery::text("world", false, false), cx)
3960 })
3961 .await
3962 .unwrap();
3963
3964 let mut ranges_by_path = results
3965 .into_iter()
3966 .map(|(buffer, ranges)| {
3967 buffer.read_with(cx_b, |buffer, cx| {
3968 let path = buffer.file().unwrap().full_path(cx);
3969 let offset_ranges = ranges
3970 .into_iter()
3971 .map(|range| range.to_offset(buffer))
3972 .collect::<Vec<_>>();
3973 (path, offset_ranges)
3974 })
3975 })
3976 .collect::<Vec<_>>();
3977 ranges_by_path.sort_by_key(|(path, _)| path.clone());
3978
3979 assert_eq!(
3980 ranges_by_path,
3981 &[
3982 (PathBuf::from("dir-1/a"), vec![6..11]),
3983 (PathBuf::from("dir-1/c"), vec![2..7]),
3984 (PathBuf::from("dir-1/d"), vec![0..5, 24..29]),
3985 (PathBuf::from("dir-2/e"), vec![7..12]),
3986 ]
3987 );
3988}
3989
3990#[gpui::test(iterations = 10)]
3991async fn test_document_highlights(
3992 deterministic: Arc<Deterministic>,
3993 cx_a: &mut TestAppContext,
3994 cx_b: &mut TestAppContext,
3995) {
3996 deterministic.forbid_parking();
3997 let mut server = TestServer::start(&deterministic).await;
3998 let client_a = server.create_client(cx_a, "user_a").await;
3999 let client_b = server.create_client(cx_b, "user_b").await;
4000 server
4001 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4002 .await;
4003 let active_call_a = cx_a.read(ActiveCall::global);
4004
4005 client_a
4006 .fs
4007 .insert_tree(
4008 "/root-1",
4009 json!({
4010 "main.rs": "fn double(number: i32) -> i32 { number + number }",
4011 }),
4012 )
4013 .await;
4014
4015 // Set up a fake language server.
4016 let mut language = Language::new(
4017 LanguageConfig {
4018 name: "Rust".into(),
4019 path_suffixes: vec!["rs".to_string()],
4020 ..Default::default()
4021 },
4022 Some(tree_sitter_rust::language()),
4023 );
4024 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4025 client_a.language_registry.add(Arc::new(language));
4026
4027 let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
4028 let project_id = active_call_a
4029 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4030 .await
4031 .unwrap();
4032 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4033
4034 // Open the file on client B.
4035 let buffer_b = cx_b
4036 .background()
4037 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)))
4038 .await
4039 .unwrap();
4040
4041 // Request document highlights as the guest.
4042 let fake_language_server = fake_language_servers.next().await.unwrap();
4043 fake_language_server.handle_request::<lsp::request::DocumentHighlightRequest, _, _>(
4044 |params, _| async move {
4045 assert_eq!(
4046 params
4047 .text_document_position_params
4048 .text_document
4049 .uri
4050 .as_str(),
4051 "file:///root-1/main.rs"
4052 );
4053 assert_eq!(
4054 params.text_document_position_params.position,
4055 lsp::Position::new(0, 34)
4056 );
4057 Ok(Some(vec![
4058 lsp::DocumentHighlight {
4059 kind: Some(lsp::DocumentHighlightKind::WRITE),
4060 range: lsp::Range::new(lsp::Position::new(0, 10), lsp::Position::new(0, 16)),
4061 },
4062 lsp::DocumentHighlight {
4063 kind: Some(lsp::DocumentHighlightKind::READ),
4064 range: lsp::Range::new(lsp::Position::new(0, 32), lsp::Position::new(0, 38)),
4065 },
4066 lsp::DocumentHighlight {
4067 kind: Some(lsp::DocumentHighlightKind::READ),
4068 range: lsp::Range::new(lsp::Position::new(0, 41), lsp::Position::new(0, 47)),
4069 },
4070 ]))
4071 },
4072 );
4073
4074 let highlights = project_b
4075 .update(cx_b, |p, cx| p.document_highlights(&buffer_b, 34, cx))
4076 .await
4077 .unwrap();
4078 buffer_b.read_with(cx_b, |buffer, _| {
4079 let snapshot = buffer.snapshot();
4080
4081 let highlights = highlights
4082 .into_iter()
4083 .map(|highlight| (highlight.kind, highlight.range.to_offset(&snapshot)))
4084 .collect::<Vec<_>>();
4085 assert_eq!(
4086 highlights,
4087 &[
4088 (lsp::DocumentHighlightKind::WRITE, 10..16),
4089 (lsp::DocumentHighlightKind::READ, 32..38),
4090 (lsp::DocumentHighlightKind::READ, 41..47)
4091 ]
4092 )
4093 });
4094}
4095
4096#[gpui::test(iterations = 10)]
4097async fn test_lsp_hover(
4098 deterministic: Arc<Deterministic>,
4099 cx_a: &mut TestAppContext,
4100 cx_b: &mut TestAppContext,
4101) {
4102 deterministic.forbid_parking();
4103 let mut server = TestServer::start(&deterministic).await;
4104 let client_a = server.create_client(cx_a, "user_a").await;
4105 let client_b = server.create_client(cx_b, "user_b").await;
4106 server
4107 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4108 .await;
4109 let active_call_a = cx_a.read(ActiveCall::global);
4110
4111 client_a
4112 .fs
4113 .insert_tree(
4114 "/root-1",
4115 json!({
4116 "main.rs": "use std::collections::HashMap;",
4117 }),
4118 )
4119 .await;
4120
4121 // Set up a fake language server.
4122 let mut language = Language::new(
4123 LanguageConfig {
4124 name: "Rust".into(),
4125 path_suffixes: vec!["rs".to_string()],
4126 ..Default::default()
4127 },
4128 Some(tree_sitter_rust::language()),
4129 );
4130 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4131 client_a.language_registry.add(Arc::new(language));
4132
4133 let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
4134 let project_id = active_call_a
4135 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4136 .await
4137 .unwrap();
4138 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4139
4140 // Open the file as the guest
4141 let buffer_b = cx_b
4142 .background()
4143 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)))
4144 .await
4145 .unwrap();
4146
4147 // Request hover information as the guest.
4148 let fake_language_server = fake_language_servers.next().await.unwrap();
4149 fake_language_server.handle_request::<lsp::request::HoverRequest, _, _>(
4150 |params, _| async move {
4151 assert_eq!(
4152 params
4153 .text_document_position_params
4154 .text_document
4155 .uri
4156 .as_str(),
4157 "file:///root-1/main.rs"
4158 );
4159 assert_eq!(
4160 params.text_document_position_params.position,
4161 lsp::Position::new(0, 22)
4162 );
4163 Ok(Some(lsp::Hover {
4164 contents: lsp::HoverContents::Array(vec![
4165 lsp::MarkedString::String("Test hover content.".to_string()),
4166 lsp::MarkedString::LanguageString(lsp::LanguageString {
4167 language: "Rust".to_string(),
4168 value: "let foo = 42;".to_string(),
4169 }),
4170 ]),
4171 range: Some(lsp::Range::new(
4172 lsp::Position::new(0, 22),
4173 lsp::Position::new(0, 29),
4174 )),
4175 }))
4176 },
4177 );
4178
4179 let hover_info = project_b
4180 .update(cx_b, |p, cx| p.hover(&buffer_b, 22, cx))
4181 .await
4182 .unwrap()
4183 .unwrap();
4184 buffer_b.read_with(cx_b, |buffer, _| {
4185 let snapshot = buffer.snapshot();
4186 assert_eq!(hover_info.range.unwrap().to_offset(&snapshot), 22..29);
4187 assert_eq!(
4188 hover_info.contents,
4189 vec![
4190 project::HoverBlock {
4191 text: "Test hover content.".to_string(),
4192 language: None,
4193 },
4194 project::HoverBlock {
4195 text: "let foo = 42;".to_string(),
4196 language: Some("Rust".to_string()),
4197 }
4198 ]
4199 );
4200 });
4201}
4202
4203#[gpui::test(iterations = 10)]
4204async fn test_project_symbols(
4205 deterministic: Arc<Deterministic>,
4206 cx_a: &mut TestAppContext,
4207 cx_b: &mut TestAppContext,
4208) {
4209 deterministic.forbid_parking();
4210 let mut server = TestServer::start(&deterministic).await;
4211 let client_a = server.create_client(cx_a, "user_a").await;
4212 let client_b = server.create_client(cx_b, "user_b").await;
4213 server
4214 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4215 .await;
4216 let active_call_a = cx_a.read(ActiveCall::global);
4217
4218 // Set up a fake language server.
4219 let mut language = Language::new(
4220 LanguageConfig {
4221 name: "Rust".into(),
4222 path_suffixes: vec!["rs".to_string()],
4223 ..Default::default()
4224 },
4225 Some(tree_sitter_rust::language()),
4226 );
4227 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4228 client_a.language_registry.add(Arc::new(language));
4229
4230 client_a
4231 .fs
4232 .insert_tree(
4233 "/code",
4234 json!({
4235 "crate-1": {
4236 "one.rs": "const ONE: usize = 1;",
4237 },
4238 "crate-2": {
4239 "two.rs": "const TWO: usize = 2; const THREE: usize = 3;",
4240 },
4241 "private": {
4242 "passwords.txt": "the-password",
4243 }
4244 }),
4245 )
4246 .await;
4247 let (project_a, worktree_id) = client_a.build_local_project("/code/crate-1", cx_a).await;
4248 let project_id = active_call_a
4249 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4250 .await
4251 .unwrap();
4252 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4253
4254 // Cause the language server to start.
4255 let _buffer = cx_b
4256 .background()
4257 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx)))
4258 .await
4259 .unwrap();
4260
4261 let fake_language_server = fake_language_servers.next().await.unwrap();
4262 fake_language_server.handle_request::<lsp::request::WorkspaceSymbol, _, _>(|_, _| async move {
4263 #[allow(deprecated)]
4264 Ok(Some(vec![lsp::SymbolInformation {
4265 name: "TWO".into(),
4266 location: lsp::Location {
4267 uri: lsp::Url::from_file_path("/code/crate-2/two.rs").unwrap(),
4268 range: lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
4269 },
4270 kind: lsp::SymbolKind::CONSTANT,
4271 tags: None,
4272 container_name: None,
4273 deprecated: None,
4274 }]))
4275 });
4276
4277 // Request the definition of a symbol as the guest.
4278 let symbols = project_b
4279 .update(cx_b, |p, cx| p.symbols("two", cx))
4280 .await
4281 .unwrap();
4282 assert_eq!(symbols.len(), 1);
4283 assert_eq!(symbols[0].name, "TWO");
4284
4285 // Open one of the returned symbols.
4286 let buffer_b_2 = project_b
4287 .update(cx_b, |project, cx| {
4288 project.open_buffer_for_symbol(&symbols[0], cx)
4289 })
4290 .await
4291 .unwrap();
4292 buffer_b_2.read_with(cx_b, |buffer, _| {
4293 assert_eq!(
4294 buffer.file().unwrap().path().as_ref(),
4295 Path::new("../crate-2/two.rs")
4296 );
4297 });
4298
4299 // Attempt to craft a symbol and violate host's privacy by opening an arbitrary file.
4300 let mut fake_symbol = symbols[0].clone();
4301 fake_symbol.path.path = Path::new("/code/secrets").into();
4302 let error = project_b
4303 .update(cx_b, |project, cx| {
4304 project.open_buffer_for_symbol(&fake_symbol, cx)
4305 })
4306 .await
4307 .unwrap_err();
4308 assert!(error.to_string().contains("invalid symbol signature"));
4309}
4310
4311#[gpui::test(iterations = 10)]
4312async fn test_open_buffer_while_getting_definition_pointing_to_it(
4313 deterministic: Arc<Deterministic>,
4314 cx_a: &mut TestAppContext,
4315 cx_b: &mut TestAppContext,
4316 mut rng: StdRng,
4317) {
4318 deterministic.forbid_parking();
4319 let mut server = TestServer::start(&deterministic).await;
4320 let client_a = server.create_client(cx_a, "user_a").await;
4321 let client_b = server.create_client(cx_b, "user_b").await;
4322 server
4323 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4324 .await;
4325 let active_call_a = cx_a.read(ActiveCall::global);
4326
4327 // Set up a fake language server.
4328 let mut language = Language::new(
4329 LanguageConfig {
4330 name: "Rust".into(),
4331 path_suffixes: vec!["rs".to_string()],
4332 ..Default::default()
4333 },
4334 Some(tree_sitter_rust::language()),
4335 );
4336 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4337 client_a.language_registry.add(Arc::new(language));
4338
4339 client_a
4340 .fs
4341 .insert_tree(
4342 "/root",
4343 json!({
4344 "a.rs": "const ONE: usize = b::TWO;",
4345 "b.rs": "const TWO: usize = 2",
4346 }),
4347 )
4348 .await;
4349 let (project_a, worktree_id) = client_a.build_local_project("/root", cx_a).await;
4350 let project_id = active_call_a
4351 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4352 .await
4353 .unwrap();
4354 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4355
4356 let buffer_b1 = cx_b
4357 .background()
4358 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
4359 .await
4360 .unwrap();
4361
4362 let fake_language_server = fake_language_servers.next().await.unwrap();
4363 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
4364 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
4365 lsp::Location::new(
4366 lsp::Url::from_file_path("/root/b.rs").unwrap(),
4367 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
4368 ),
4369 )))
4370 });
4371
4372 let definitions;
4373 let buffer_b2;
4374 if rng.gen() {
4375 definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
4376 buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx));
4377 } else {
4378 buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx));
4379 definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
4380 }
4381
4382 let buffer_b2 = buffer_b2.await.unwrap();
4383 let definitions = definitions.await.unwrap();
4384 assert_eq!(definitions.len(), 1);
4385 assert_eq!(definitions[0].target.buffer, buffer_b2);
4386}
4387
4388#[gpui::test(iterations = 10)]
4389async fn test_collaborating_with_code_actions(
4390 deterministic: Arc<Deterministic>,
4391 cx_a: &mut TestAppContext,
4392 cx_b: &mut TestAppContext,
4393) {
4394 deterministic.forbid_parking();
4395 cx_b.update(editor::init);
4396 let mut server = TestServer::start(&deterministic).await;
4397 let client_a = server.create_client(cx_a, "user_a").await;
4398 let client_b = server.create_client(cx_b, "user_b").await;
4399 server
4400 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4401 .await;
4402 let active_call_a = cx_a.read(ActiveCall::global);
4403
4404 // Set up a fake language server.
4405 let mut language = Language::new(
4406 LanguageConfig {
4407 name: "Rust".into(),
4408 path_suffixes: vec!["rs".to_string()],
4409 ..Default::default()
4410 },
4411 Some(tree_sitter_rust::language()),
4412 );
4413 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
4414 client_a.language_registry.add(Arc::new(language));
4415
4416 client_a
4417 .fs
4418 .insert_tree(
4419 "/a",
4420 json!({
4421 "main.rs": "mod other;\nfn main() { let foo = other::foo(); }",
4422 "other.rs": "pub fn foo() -> usize { 4 }",
4423 }),
4424 )
4425 .await;
4426 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
4427 let project_id = active_call_a
4428 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4429 .await
4430 .unwrap();
4431
4432 // Join the project as client B.
4433 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4434 let (_window_b, workspace_b) = cx_b.add_window(|cx| {
4435 Workspace::new(
4436 Default::default(),
4437 0,
4438 project_b.clone(),
4439 |_, _| unimplemented!(),
4440 cx,
4441 )
4442 });
4443 let editor_b = workspace_b
4444 .update(cx_b, |workspace, cx| {
4445 workspace.open_path((worktree_id, "main.rs"), None, true, cx)
4446 })
4447 .await
4448 .unwrap()
4449 .downcast::<Editor>()
4450 .unwrap();
4451
4452 let mut fake_language_server = fake_language_servers.next().await.unwrap();
4453 fake_language_server
4454 .handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
4455 assert_eq!(
4456 params.text_document.uri,
4457 lsp::Url::from_file_path("/a/main.rs").unwrap(),
4458 );
4459 assert_eq!(params.range.start, lsp::Position::new(0, 0));
4460 assert_eq!(params.range.end, lsp::Position::new(0, 0));
4461 Ok(None)
4462 })
4463 .next()
4464 .await;
4465
4466 // Move cursor to a location that contains code actions.
4467 editor_b.update(cx_b, |editor, cx| {
4468 editor.change_selections(None, cx, |s| {
4469 s.select_ranges([Point::new(1, 31)..Point::new(1, 31)])
4470 });
4471 cx.focus(&editor_b);
4472 });
4473
4474 fake_language_server
4475 .handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
4476 assert_eq!(
4477 params.text_document.uri,
4478 lsp::Url::from_file_path("/a/main.rs").unwrap(),
4479 );
4480 assert_eq!(params.range.start, lsp::Position::new(1, 31));
4481 assert_eq!(params.range.end, lsp::Position::new(1, 31));
4482
4483 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
4484 lsp::CodeAction {
4485 title: "Inline into all callers".to_string(),
4486 edit: Some(lsp::WorkspaceEdit {
4487 changes: Some(
4488 [
4489 (
4490 lsp::Url::from_file_path("/a/main.rs").unwrap(),
4491 vec![lsp::TextEdit::new(
4492 lsp::Range::new(
4493 lsp::Position::new(1, 22),
4494 lsp::Position::new(1, 34),
4495 ),
4496 "4".to_string(),
4497 )],
4498 ),
4499 (
4500 lsp::Url::from_file_path("/a/other.rs").unwrap(),
4501 vec![lsp::TextEdit::new(
4502 lsp::Range::new(
4503 lsp::Position::new(0, 0),
4504 lsp::Position::new(0, 27),
4505 ),
4506 "".to_string(),
4507 )],
4508 ),
4509 ]
4510 .into_iter()
4511 .collect(),
4512 ),
4513 ..Default::default()
4514 }),
4515 data: Some(json!({
4516 "codeActionParams": {
4517 "range": {
4518 "start": {"line": 1, "column": 31},
4519 "end": {"line": 1, "column": 31},
4520 }
4521 }
4522 })),
4523 ..Default::default()
4524 },
4525 )]))
4526 })
4527 .next()
4528 .await;
4529
4530 // Toggle code actions and wait for them to display.
4531 editor_b.update(cx_b, |editor, cx| {
4532 editor.toggle_code_actions(
4533 &ToggleCodeActions {
4534 deployed_from_indicator: false,
4535 },
4536 cx,
4537 );
4538 });
4539 cx_a.foreground().run_until_parked();
4540 editor_b.read_with(cx_b, |editor, _| assert!(editor.context_menu_visible()));
4541
4542 fake_language_server.remove_request_handler::<lsp::request::CodeActionRequest>();
4543
4544 // Confirming the code action will trigger a resolve request.
4545 let confirm_action = workspace_b
4546 .update(cx_b, |workspace, cx| {
4547 Editor::confirm_code_action(workspace, &ConfirmCodeAction { item_ix: Some(0) }, cx)
4548 })
4549 .unwrap();
4550 fake_language_server.handle_request::<lsp::request::CodeActionResolveRequest, _, _>(
4551 |_, _| async move {
4552 Ok(lsp::CodeAction {
4553 title: "Inline into all callers".to_string(),
4554 edit: Some(lsp::WorkspaceEdit {
4555 changes: Some(
4556 [
4557 (
4558 lsp::Url::from_file_path("/a/main.rs").unwrap(),
4559 vec![lsp::TextEdit::new(
4560 lsp::Range::new(
4561 lsp::Position::new(1, 22),
4562 lsp::Position::new(1, 34),
4563 ),
4564 "4".to_string(),
4565 )],
4566 ),
4567 (
4568 lsp::Url::from_file_path("/a/other.rs").unwrap(),
4569 vec![lsp::TextEdit::new(
4570 lsp::Range::new(
4571 lsp::Position::new(0, 0),
4572 lsp::Position::new(0, 27),
4573 ),
4574 "".to_string(),
4575 )],
4576 ),
4577 ]
4578 .into_iter()
4579 .collect(),
4580 ),
4581 ..Default::default()
4582 }),
4583 ..Default::default()
4584 })
4585 },
4586 );
4587
4588 // After the action is confirmed, an editor containing both modified files is opened.
4589 confirm_action.await.unwrap();
4590 let code_action_editor = workspace_b.read_with(cx_b, |workspace, cx| {
4591 workspace
4592 .active_item(cx)
4593 .unwrap()
4594 .downcast::<Editor>()
4595 .unwrap()
4596 });
4597 code_action_editor.update(cx_b, |editor, cx| {
4598 assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
4599 editor.undo(&Undo, cx);
4600 assert_eq!(
4601 editor.text(cx),
4602 "mod other;\nfn main() { let foo = other::foo(); }\npub fn foo() -> usize { 4 }"
4603 );
4604 editor.redo(&Redo, cx);
4605 assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
4606 });
4607}
4608
4609#[gpui::test(iterations = 10)]
4610async fn test_collaborating_with_renames(
4611 deterministic: Arc<Deterministic>,
4612 cx_a: &mut TestAppContext,
4613 cx_b: &mut TestAppContext,
4614) {
4615 deterministic.forbid_parking();
4616 cx_b.update(editor::init);
4617 let mut server = TestServer::start(&deterministic).await;
4618 let client_a = server.create_client(cx_a, "user_a").await;
4619 let client_b = server.create_client(cx_b, "user_b").await;
4620 server
4621 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4622 .await;
4623 let active_call_a = cx_a.read(ActiveCall::global);
4624
4625 // Set up a fake language server.
4626 let mut language = Language::new(
4627 LanguageConfig {
4628 name: "Rust".into(),
4629 path_suffixes: vec!["rs".to_string()],
4630 ..Default::default()
4631 },
4632 Some(tree_sitter_rust::language()),
4633 );
4634 let mut fake_language_servers = language
4635 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
4636 capabilities: lsp::ServerCapabilities {
4637 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
4638 prepare_provider: Some(true),
4639 work_done_progress_options: Default::default(),
4640 })),
4641 ..Default::default()
4642 },
4643 ..Default::default()
4644 }))
4645 .await;
4646 client_a.language_registry.add(Arc::new(language));
4647
4648 client_a
4649 .fs
4650 .insert_tree(
4651 "/dir",
4652 json!({
4653 "one.rs": "const ONE: usize = 1;",
4654 "two.rs": "const TWO: usize = one::ONE + one::ONE;"
4655 }),
4656 )
4657 .await;
4658 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
4659 let project_id = active_call_a
4660 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4661 .await
4662 .unwrap();
4663 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4664
4665 let (_window_b, workspace_b) = cx_b.add_window(|cx| {
4666 Workspace::new(
4667 Default::default(),
4668 0,
4669 project_b.clone(),
4670 |_, _| unimplemented!(),
4671 cx,
4672 )
4673 });
4674 let editor_b = workspace_b
4675 .update(cx_b, |workspace, cx| {
4676 workspace.open_path((worktree_id, "one.rs"), None, true, cx)
4677 })
4678 .await
4679 .unwrap()
4680 .downcast::<Editor>()
4681 .unwrap();
4682 let fake_language_server = fake_language_servers.next().await.unwrap();
4683
4684 // Move cursor to a location that can be renamed.
4685 let prepare_rename = editor_b.update(cx_b, |editor, cx| {
4686 editor.change_selections(None, cx, |s| s.select_ranges([7..7]));
4687 editor.rename(&Rename, cx).unwrap()
4688 });
4689
4690 fake_language_server
4691 .handle_request::<lsp::request::PrepareRenameRequest, _, _>(|params, _| async move {
4692 assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs");
4693 assert_eq!(params.position, lsp::Position::new(0, 7));
4694 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
4695 lsp::Position::new(0, 6),
4696 lsp::Position::new(0, 9),
4697 ))))
4698 })
4699 .next()
4700 .await
4701 .unwrap();
4702 prepare_rename.await.unwrap();
4703 editor_b.update(cx_b, |editor, cx| {
4704 let rename = editor.pending_rename().unwrap();
4705 let buffer = editor.buffer().read(cx).snapshot(cx);
4706 assert_eq!(
4707 rename.range.start.to_offset(&buffer)..rename.range.end.to_offset(&buffer),
4708 6..9
4709 );
4710 rename.editor.update(cx, |rename_editor, cx| {
4711 rename_editor.buffer().update(cx, |rename_buffer, cx| {
4712 rename_buffer.edit([(0..3, "THREE")], None, cx);
4713 });
4714 });
4715 });
4716
4717 let confirm_rename = workspace_b.update(cx_b, |workspace, cx| {
4718 Editor::confirm_rename(workspace, &ConfirmRename, cx).unwrap()
4719 });
4720 fake_language_server
4721 .handle_request::<lsp::request::Rename, _, _>(|params, _| async move {
4722 assert_eq!(
4723 params.text_document_position.text_document.uri.as_str(),
4724 "file:///dir/one.rs"
4725 );
4726 assert_eq!(
4727 params.text_document_position.position,
4728 lsp::Position::new(0, 6)
4729 );
4730 assert_eq!(params.new_name, "THREE");
4731 Ok(Some(lsp::WorkspaceEdit {
4732 changes: Some(
4733 [
4734 (
4735 lsp::Url::from_file_path("/dir/one.rs").unwrap(),
4736 vec![lsp::TextEdit::new(
4737 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
4738 "THREE".to_string(),
4739 )],
4740 ),
4741 (
4742 lsp::Url::from_file_path("/dir/two.rs").unwrap(),
4743 vec![
4744 lsp::TextEdit::new(
4745 lsp::Range::new(
4746 lsp::Position::new(0, 24),
4747 lsp::Position::new(0, 27),
4748 ),
4749 "THREE".to_string(),
4750 ),
4751 lsp::TextEdit::new(
4752 lsp::Range::new(
4753 lsp::Position::new(0, 35),
4754 lsp::Position::new(0, 38),
4755 ),
4756 "THREE".to_string(),
4757 ),
4758 ],
4759 ),
4760 ]
4761 .into_iter()
4762 .collect(),
4763 ),
4764 ..Default::default()
4765 }))
4766 })
4767 .next()
4768 .await
4769 .unwrap();
4770 confirm_rename.await.unwrap();
4771
4772 let rename_editor = workspace_b.read_with(cx_b, |workspace, cx| {
4773 workspace
4774 .active_item(cx)
4775 .unwrap()
4776 .downcast::<Editor>()
4777 .unwrap()
4778 });
4779 rename_editor.update(cx_b, |editor, cx| {
4780 assert_eq!(
4781 editor.text(cx),
4782 "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
4783 );
4784 editor.undo(&Undo, cx);
4785 assert_eq!(
4786 editor.text(cx),
4787 "const ONE: usize = 1;\nconst TWO: usize = one::ONE + one::ONE;"
4788 );
4789 editor.redo(&Redo, cx);
4790 assert_eq!(
4791 editor.text(cx),
4792 "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
4793 );
4794 });
4795
4796 // Ensure temporary rename edits cannot be undone/redone.
4797 editor_b.update(cx_b, |editor, cx| {
4798 editor.undo(&Undo, cx);
4799 assert_eq!(editor.text(cx), "const ONE: usize = 1;");
4800 editor.undo(&Undo, cx);
4801 assert_eq!(editor.text(cx), "const ONE: usize = 1;");
4802 editor.redo(&Redo, cx);
4803 assert_eq!(editor.text(cx), "const THREE: usize = 1;");
4804 })
4805}
4806
4807#[gpui::test(iterations = 10)]
4808async fn test_language_server_statuses(
4809 deterministic: Arc<Deterministic>,
4810 cx_a: &mut TestAppContext,
4811 cx_b: &mut TestAppContext,
4812) {
4813 deterministic.forbid_parking();
4814
4815 cx_b.update(editor::init);
4816 let mut server = TestServer::start(&deterministic).await;
4817 let client_a = server.create_client(cx_a, "user_a").await;
4818 let client_b = server.create_client(cx_b, "user_b").await;
4819 server
4820 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4821 .await;
4822 let active_call_a = cx_a.read(ActiveCall::global);
4823
4824 // Set up a fake language server.
4825 let mut language = Language::new(
4826 LanguageConfig {
4827 name: "Rust".into(),
4828 path_suffixes: vec!["rs".to_string()],
4829 ..Default::default()
4830 },
4831 Some(tree_sitter_rust::language()),
4832 );
4833 let mut fake_language_servers = language
4834 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
4835 name: "the-language-server",
4836 ..Default::default()
4837 }))
4838 .await;
4839 client_a.language_registry.add(Arc::new(language));
4840
4841 client_a
4842 .fs
4843 .insert_tree(
4844 "/dir",
4845 json!({
4846 "main.rs": "const ONE: usize = 1;",
4847 }),
4848 )
4849 .await;
4850 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
4851
4852 let _buffer_a = project_a
4853 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
4854 .await
4855 .unwrap();
4856
4857 let fake_language_server = fake_language_servers.next().await.unwrap();
4858 fake_language_server.start_progress("the-token").await;
4859 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
4860 token: lsp::NumberOrString::String("the-token".to_string()),
4861 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
4862 lsp::WorkDoneProgressReport {
4863 message: Some("the-message".to_string()),
4864 ..Default::default()
4865 },
4866 )),
4867 });
4868 deterministic.run_until_parked();
4869 project_a.read_with(cx_a, |project, _| {
4870 let status = project.language_server_statuses().next().unwrap();
4871 assert_eq!(status.name, "the-language-server");
4872 assert_eq!(status.pending_work.len(), 1);
4873 assert_eq!(
4874 status.pending_work["the-token"].message.as_ref().unwrap(),
4875 "the-message"
4876 );
4877 });
4878
4879 let project_id = active_call_a
4880 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4881 .await
4882 .unwrap();
4883 deterministic.run_until_parked();
4884 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4885 project_b.read_with(cx_b, |project, _| {
4886 let status = project.language_server_statuses().next().unwrap();
4887 assert_eq!(status.name, "the-language-server");
4888 });
4889
4890 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
4891 token: lsp::NumberOrString::String("the-token".to_string()),
4892 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
4893 lsp::WorkDoneProgressReport {
4894 message: Some("the-message-2".to_string()),
4895 ..Default::default()
4896 },
4897 )),
4898 });
4899 deterministic.run_until_parked();
4900 project_a.read_with(cx_a, |project, _| {
4901 let status = project.language_server_statuses().next().unwrap();
4902 assert_eq!(status.name, "the-language-server");
4903 assert_eq!(status.pending_work.len(), 1);
4904 assert_eq!(
4905 status.pending_work["the-token"].message.as_ref().unwrap(),
4906 "the-message-2"
4907 );
4908 });
4909 project_b.read_with(cx_b, |project, _| {
4910 let status = project.language_server_statuses().next().unwrap();
4911 assert_eq!(status.name, "the-language-server");
4912 assert_eq!(status.pending_work.len(), 1);
4913 assert_eq!(
4914 status.pending_work["the-token"].message.as_ref().unwrap(),
4915 "the-message-2"
4916 );
4917 });
4918}
4919
4920#[gpui::test(iterations = 10)]
4921async fn test_contacts(
4922 deterministic: Arc<Deterministic>,
4923 cx_a: &mut TestAppContext,
4924 cx_b: &mut TestAppContext,
4925 cx_c: &mut TestAppContext,
4926 cx_d: &mut TestAppContext,
4927) {
4928 deterministic.forbid_parking();
4929 let mut server = TestServer::start(&deterministic).await;
4930 let client_a = server.create_client(cx_a, "user_a").await;
4931 let client_b = server.create_client(cx_b, "user_b").await;
4932 let client_c = server.create_client(cx_c, "user_c").await;
4933 let client_d = server.create_client(cx_d, "user_d").await;
4934 server
4935 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
4936 .await;
4937 let active_call_a = cx_a.read(ActiveCall::global);
4938 let active_call_b = cx_b.read(ActiveCall::global);
4939 let active_call_c = cx_c.read(ActiveCall::global);
4940 let _active_call_d = cx_d.read(ActiveCall::global);
4941
4942 deterministic.run_until_parked();
4943 assert_eq!(
4944 contacts(&client_a, cx_a),
4945 [
4946 ("user_b".to_string(), "online", "free"),
4947 ("user_c".to_string(), "online", "free")
4948 ]
4949 );
4950 assert_eq!(
4951 contacts(&client_b, cx_b),
4952 [
4953 ("user_a".to_string(), "online", "free"),
4954 ("user_c".to_string(), "online", "free")
4955 ]
4956 );
4957 assert_eq!(
4958 contacts(&client_c, cx_c),
4959 [
4960 ("user_a".to_string(), "online", "free"),
4961 ("user_b".to_string(), "online", "free")
4962 ]
4963 );
4964 assert_eq!(contacts(&client_d, cx_d), []);
4965
4966 server.disconnect_client(client_c.peer_id().unwrap());
4967 server.forbid_connections();
4968 deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
4969 assert_eq!(
4970 contacts(&client_a, cx_a),
4971 [
4972 ("user_b".to_string(), "online", "free"),
4973 ("user_c".to_string(), "offline", "free")
4974 ]
4975 );
4976 assert_eq!(
4977 contacts(&client_b, cx_b),
4978 [
4979 ("user_a".to_string(), "online", "free"),
4980 ("user_c".to_string(), "offline", "free")
4981 ]
4982 );
4983 assert_eq!(contacts(&client_c, cx_c), []);
4984 assert_eq!(contacts(&client_d, cx_d), []);
4985
4986 server.allow_connections();
4987 client_c
4988 .authenticate_and_connect(false, &cx_c.to_async())
4989 .await
4990 .unwrap();
4991
4992 deterministic.run_until_parked();
4993 assert_eq!(
4994 contacts(&client_a, cx_a),
4995 [
4996 ("user_b".to_string(), "online", "free"),
4997 ("user_c".to_string(), "online", "free")
4998 ]
4999 );
5000 assert_eq!(
5001 contacts(&client_b, cx_b),
5002 [
5003 ("user_a".to_string(), "online", "free"),
5004 ("user_c".to_string(), "online", "free")
5005 ]
5006 );
5007 assert_eq!(
5008 contacts(&client_c, cx_c),
5009 [
5010 ("user_a".to_string(), "online", "free"),
5011 ("user_b".to_string(), "online", "free")
5012 ]
5013 );
5014 assert_eq!(contacts(&client_d, cx_d), []);
5015
5016 active_call_a
5017 .update(cx_a, |call, cx| {
5018 call.invite(client_b.user_id().unwrap(), None, cx)
5019 })
5020 .await
5021 .unwrap();
5022 deterministic.run_until_parked();
5023 assert_eq!(
5024 contacts(&client_a, cx_a),
5025 [
5026 ("user_b".to_string(), "online", "busy"),
5027 ("user_c".to_string(), "online", "free")
5028 ]
5029 );
5030 assert_eq!(
5031 contacts(&client_b, cx_b),
5032 [
5033 ("user_a".to_string(), "online", "busy"),
5034 ("user_c".to_string(), "online", "free")
5035 ]
5036 );
5037 assert_eq!(
5038 contacts(&client_c, cx_c),
5039 [
5040 ("user_a".to_string(), "online", "busy"),
5041 ("user_b".to_string(), "online", "busy")
5042 ]
5043 );
5044 assert_eq!(contacts(&client_d, cx_d), []);
5045
5046 // Client B and client D become contacts while client B is being called.
5047 server
5048 .make_contacts(&mut [(&client_b, cx_b), (&client_d, cx_d)])
5049 .await;
5050 deterministic.run_until_parked();
5051 assert_eq!(
5052 contacts(&client_a, cx_a),
5053 [
5054 ("user_b".to_string(), "online", "busy"),
5055 ("user_c".to_string(), "online", "free")
5056 ]
5057 );
5058 assert_eq!(
5059 contacts(&client_b, cx_b),
5060 [
5061 ("user_a".to_string(), "online", "busy"),
5062 ("user_c".to_string(), "online", "free"),
5063 ("user_d".to_string(), "online", "free"),
5064 ]
5065 );
5066 assert_eq!(
5067 contacts(&client_c, cx_c),
5068 [
5069 ("user_a".to_string(), "online", "busy"),
5070 ("user_b".to_string(), "online", "busy")
5071 ]
5072 );
5073 assert_eq!(
5074 contacts(&client_d, cx_d),
5075 [("user_b".to_string(), "online", "busy")]
5076 );
5077
5078 active_call_b.update(cx_b, |call, _| call.decline_incoming().unwrap());
5079 deterministic.run_until_parked();
5080 assert_eq!(
5081 contacts(&client_a, cx_a),
5082 [
5083 ("user_b".to_string(), "online", "free"),
5084 ("user_c".to_string(), "online", "free")
5085 ]
5086 );
5087 assert_eq!(
5088 contacts(&client_b, cx_b),
5089 [
5090 ("user_a".to_string(), "online", "free"),
5091 ("user_c".to_string(), "online", "free"),
5092 ("user_d".to_string(), "online", "free")
5093 ]
5094 );
5095 assert_eq!(
5096 contacts(&client_c, cx_c),
5097 [
5098 ("user_a".to_string(), "online", "free"),
5099 ("user_b".to_string(), "online", "free")
5100 ]
5101 );
5102 assert_eq!(
5103 contacts(&client_d, cx_d),
5104 [("user_b".to_string(), "online", "free")]
5105 );
5106
5107 active_call_c
5108 .update(cx_c, |call, cx| {
5109 call.invite(client_a.user_id().unwrap(), None, cx)
5110 })
5111 .await
5112 .unwrap();
5113 deterministic.run_until_parked();
5114 assert_eq!(
5115 contacts(&client_a, cx_a),
5116 [
5117 ("user_b".to_string(), "online", "free"),
5118 ("user_c".to_string(), "online", "busy")
5119 ]
5120 );
5121 assert_eq!(
5122 contacts(&client_b, cx_b),
5123 [
5124 ("user_a".to_string(), "online", "busy"),
5125 ("user_c".to_string(), "online", "busy"),
5126 ("user_d".to_string(), "online", "free")
5127 ]
5128 );
5129 assert_eq!(
5130 contacts(&client_c, cx_c),
5131 [
5132 ("user_a".to_string(), "online", "busy"),
5133 ("user_b".to_string(), "online", "free")
5134 ]
5135 );
5136 assert_eq!(
5137 contacts(&client_d, cx_d),
5138 [("user_b".to_string(), "online", "free")]
5139 );
5140
5141 active_call_a
5142 .update(cx_a, |call, cx| call.accept_incoming(cx))
5143 .await
5144 .unwrap();
5145 deterministic.run_until_parked();
5146 assert_eq!(
5147 contacts(&client_a, cx_a),
5148 [
5149 ("user_b".to_string(), "online", "free"),
5150 ("user_c".to_string(), "online", "busy")
5151 ]
5152 );
5153 assert_eq!(
5154 contacts(&client_b, cx_b),
5155 [
5156 ("user_a".to_string(), "online", "busy"),
5157 ("user_c".to_string(), "online", "busy"),
5158 ("user_d".to_string(), "online", "free")
5159 ]
5160 );
5161 assert_eq!(
5162 contacts(&client_c, cx_c),
5163 [
5164 ("user_a".to_string(), "online", "busy"),
5165 ("user_b".to_string(), "online", "free")
5166 ]
5167 );
5168 assert_eq!(
5169 contacts(&client_d, cx_d),
5170 [("user_b".to_string(), "online", "free")]
5171 );
5172
5173 active_call_a
5174 .update(cx_a, |call, cx| {
5175 call.invite(client_b.user_id().unwrap(), None, cx)
5176 })
5177 .await
5178 .unwrap();
5179 deterministic.run_until_parked();
5180 assert_eq!(
5181 contacts(&client_a, cx_a),
5182 [
5183 ("user_b".to_string(), "online", "busy"),
5184 ("user_c".to_string(), "online", "busy")
5185 ]
5186 );
5187 assert_eq!(
5188 contacts(&client_b, cx_b),
5189 [
5190 ("user_a".to_string(), "online", "busy"),
5191 ("user_c".to_string(), "online", "busy"),
5192 ("user_d".to_string(), "online", "free")
5193 ]
5194 );
5195 assert_eq!(
5196 contacts(&client_c, cx_c),
5197 [
5198 ("user_a".to_string(), "online", "busy"),
5199 ("user_b".to_string(), "online", "busy")
5200 ]
5201 );
5202 assert_eq!(
5203 contacts(&client_d, cx_d),
5204 [("user_b".to_string(), "online", "busy")]
5205 );
5206
5207 active_call_a.update(cx_a, |call, cx| call.hang_up(cx).unwrap());
5208 deterministic.run_until_parked();
5209 assert_eq!(
5210 contacts(&client_a, cx_a),
5211 [
5212 ("user_b".to_string(), "online", "free"),
5213 ("user_c".to_string(), "online", "free")
5214 ]
5215 );
5216 assert_eq!(
5217 contacts(&client_b, cx_b),
5218 [
5219 ("user_a".to_string(), "online", "free"),
5220 ("user_c".to_string(), "online", "free"),
5221 ("user_d".to_string(), "online", "free")
5222 ]
5223 );
5224 assert_eq!(
5225 contacts(&client_c, cx_c),
5226 [
5227 ("user_a".to_string(), "online", "free"),
5228 ("user_b".to_string(), "online", "free")
5229 ]
5230 );
5231 assert_eq!(
5232 contacts(&client_d, cx_d),
5233 [("user_b".to_string(), "online", "free")]
5234 );
5235
5236 active_call_a
5237 .update(cx_a, |call, cx| {
5238 call.invite(client_b.user_id().unwrap(), None, cx)
5239 })
5240 .await
5241 .unwrap();
5242 deterministic.run_until_parked();
5243 assert_eq!(
5244 contacts(&client_a, cx_a),
5245 [
5246 ("user_b".to_string(), "online", "busy"),
5247 ("user_c".to_string(), "online", "free")
5248 ]
5249 );
5250 assert_eq!(
5251 contacts(&client_b, cx_b),
5252 [
5253 ("user_a".to_string(), "online", "busy"),
5254 ("user_c".to_string(), "online", "free"),
5255 ("user_d".to_string(), "online", "free")
5256 ]
5257 );
5258 assert_eq!(
5259 contacts(&client_c, cx_c),
5260 [
5261 ("user_a".to_string(), "online", "busy"),
5262 ("user_b".to_string(), "online", "busy")
5263 ]
5264 );
5265 assert_eq!(
5266 contacts(&client_d, cx_d),
5267 [("user_b".to_string(), "online", "busy")]
5268 );
5269
5270 server.forbid_connections();
5271 server.disconnect_client(client_a.peer_id().unwrap());
5272 deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
5273 assert_eq!(contacts(&client_a, cx_a), []);
5274 assert_eq!(
5275 contacts(&client_b, cx_b),
5276 [
5277 ("user_a".to_string(), "offline", "free"),
5278 ("user_c".to_string(), "online", "free"),
5279 ("user_d".to_string(), "online", "free")
5280 ]
5281 );
5282 assert_eq!(
5283 contacts(&client_c, cx_c),
5284 [
5285 ("user_a".to_string(), "offline", "free"),
5286 ("user_b".to_string(), "online", "free")
5287 ]
5288 );
5289 assert_eq!(
5290 contacts(&client_d, cx_d),
5291 [("user_b".to_string(), "online", "free")]
5292 );
5293
5294 fn contacts(
5295 client: &TestClient,
5296 cx: &TestAppContext,
5297 ) -> Vec<(String, &'static str, &'static str)> {
5298 client.user_store.read_with(cx, |store, _| {
5299 store
5300 .contacts()
5301 .iter()
5302 .map(|contact| {
5303 (
5304 contact.user.github_login.clone(),
5305 if contact.online { "online" } else { "offline" },
5306 if contact.busy { "busy" } else { "free" },
5307 )
5308 })
5309 .collect()
5310 })
5311 }
5312}
5313
5314#[gpui::test(iterations = 10)]
5315async fn test_contact_requests(
5316 deterministic: Arc<Deterministic>,
5317 cx_a: &mut TestAppContext,
5318 cx_a2: &mut TestAppContext,
5319 cx_b: &mut TestAppContext,
5320 cx_b2: &mut TestAppContext,
5321 cx_c: &mut TestAppContext,
5322 cx_c2: &mut TestAppContext,
5323) {
5324 deterministic.forbid_parking();
5325
5326 // Connect to a server as 3 clients.
5327 let mut server = TestServer::start(&deterministic).await;
5328 let client_a = server.create_client(cx_a, "user_a").await;
5329 let client_a2 = server.create_client(cx_a2, "user_a").await;
5330 let client_b = server.create_client(cx_b, "user_b").await;
5331 let client_b2 = server.create_client(cx_b2, "user_b").await;
5332 let client_c = server.create_client(cx_c, "user_c").await;
5333 let client_c2 = server.create_client(cx_c2, "user_c").await;
5334
5335 assert_eq!(client_a.user_id().unwrap(), client_a2.user_id().unwrap());
5336 assert_eq!(client_b.user_id().unwrap(), client_b2.user_id().unwrap());
5337 assert_eq!(client_c.user_id().unwrap(), client_c2.user_id().unwrap());
5338
5339 // User A and User C request that user B become their contact.
5340 client_a
5341 .user_store
5342 .update(cx_a, |store, cx| {
5343 store.request_contact(client_b.user_id().unwrap(), cx)
5344 })
5345 .await
5346 .unwrap();
5347 client_c
5348 .user_store
5349 .update(cx_c, |store, cx| {
5350 store.request_contact(client_b.user_id().unwrap(), cx)
5351 })
5352 .await
5353 .unwrap();
5354 deterministic.run_until_parked();
5355
5356 // All users see the pending request appear in all their clients.
5357 assert_eq!(
5358 client_a.summarize_contacts(cx_a).outgoing_requests,
5359 &["user_b"]
5360 );
5361 assert_eq!(
5362 client_a2.summarize_contacts(cx_a2).outgoing_requests,
5363 &["user_b"]
5364 );
5365 assert_eq!(
5366 client_b.summarize_contacts(cx_b).incoming_requests,
5367 &["user_a", "user_c"]
5368 );
5369 assert_eq!(
5370 client_b2.summarize_contacts(cx_b2).incoming_requests,
5371 &["user_a", "user_c"]
5372 );
5373 assert_eq!(
5374 client_c.summarize_contacts(cx_c).outgoing_requests,
5375 &["user_b"]
5376 );
5377 assert_eq!(
5378 client_c2.summarize_contacts(cx_c2).outgoing_requests,
5379 &["user_b"]
5380 );
5381
5382 // Contact requests are present upon connecting (tested here via disconnect/reconnect)
5383 disconnect_and_reconnect(&client_a, cx_a).await;
5384 disconnect_and_reconnect(&client_b, cx_b).await;
5385 disconnect_and_reconnect(&client_c, cx_c).await;
5386 deterministic.run_until_parked();
5387 assert_eq!(
5388 client_a.summarize_contacts(cx_a).outgoing_requests,
5389 &["user_b"]
5390 );
5391 assert_eq!(
5392 client_b.summarize_contacts(cx_b).incoming_requests,
5393 &["user_a", "user_c"]
5394 );
5395 assert_eq!(
5396 client_c.summarize_contacts(cx_c).outgoing_requests,
5397 &["user_b"]
5398 );
5399
5400 // User B accepts the request from user A.
5401 client_b
5402 .user_store
5403 .update(cx_b, |store, cx| {
5404 store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx)
5405 })
5406 .await
5407 .unwrap();
5408
5409 deterministic.run_until_parked();
5410
5411 // User B sees user A as their contact now in all client, and the incoming request from them is removed.
5412 let contacts_b = client_b.summarize_contacts(cx_b);
5413 assert_eq!(contacts_b.current, &["user_a"]);
5414 assert_eq!(contacts_b.incoming_requests, &["user_c"]);
5415 let contacts_b2 = client_b2.summarize_contacts(cx_b2);
5416 assert_eq!(contacts_b2.current, &["user_a"]);
5417 assert_eq!(contacts_b2.incoming_requests, &["user_c"]);
5418
5419 // User A sees user B as their contact now in all clients, and the outgoing request to them is removed.
5420 let contacts_a = client_a.summarize_contacts(cx_a);
5421 assert_eq!(contacts_a.current, &["user_b"]);
5422 assert!(contacts_a.outgoing_requests.is_empty());
5423 let contacts_a2 = client_a2.summarize_contacts(cx_a2);
5424 assert_eq!(contacts_a2.current, &["user_b"]);
5425 assert!(contacts_a2.outgoing_requests.is_empty());
5426
5427 // Contacts are present upon connecting (tested here via disconnect/reconnect)
5428 disconnect_and_reconnect(&client_a, cx_a).await;
5429 disconnect_and_reconnect(&client_b, cx_b).await;
5430 disconnect_and_reconnect(&client_c, cx_c).await;
5431 deterministic.run_until_parked();
5432 assert_eq!(client_a.summarize_contacts(cx_a).current, &["user_b"]);
5433 assert_eq!(client_b.summarize_contacts(cx_b).current, &["user_a"]);
5434 assert_eq!(
5435 client_b.summarize_contacts(cx_b).incoming_requests,
5436 &["user_c"]
5437 );
5438 assert!(client_c.summarize_contacts(cx_c).current.is_empty());
5439 assert_eq!(
5440 client_c.summarize_contacts(cx_c).outgoing_requests,
5441 &["user_b"]
5442 );
5443
5444 // User B rejects the request from user C.
5445 client_b
5446 .user_store
5447 .update(cx_b, |store, cx| {
5448 store.respond_to_contact_request(client_c.user_id().unwrap(), false, cx)
5449 })
5450 .await
5451 .unwrap();
5452
5453 deterministic.run_until_parked();
5454
5455 // User B doesn't see user C as their contact, and the incoming request from them is removed.
5456 let contacts_b = client_b.summarize_contacts(cx_b);
5457 assert_eq!(contacts_b.current, &["user_a"]);
5458 assert!(contacts_b.incoming_requests.is_empty());
5459 let contacts_b2 = client_b2.summarize_contacts(cx_b2);
5460 assert_eq!(contacts_b2.current, &["user_a"]);
5461 assert!(contacts_b2.incoming_requests.is_empty());
5462
5463 // User C doesn't see user B as their contact, and the outgoing request to them is removed.
5464 let contacts_c = client_c.summarize_contacts(cx_c);
5465 assert!(contacts_c.current.is_empty());
5466 assert!(contacts_c.outgoing_requests.is_empty());
5467 let contacts_c2 = client_c2.summarize_contacts(cx_c2);
5468 assert!(contacts_c2.current.is_empty());
5469 assert!(contacts_c2.outgoing_requests.is_empty());
5470
5471 // Incoming/outgoing requests are not present upon connecting (tested here via disconnect/reconnect)
5472 disconnect_and_reconnect(&client_a, cx_a).await;
5473 disconnect_and_reconnect(&client_b, cx_b).await;
5474 disconnect_and_reconnect(&client_c, cx_c).await;
5475 deterministic.run_until_parked();
5476 assert_eq!(client_a.summarize_contacts(cx_a).current, &["user_b"]);
5477 assert_eq!(client_b.summarize_contacts(cx_b).current, &["user_a"]);
5478 assert!(client_b
5479 .summarize_contacts(cx_b)
5480 .incoming_requests
5481 .is_empty());
5482 assert!(client_c.summarize_contacts(cx_c).current.is_empty());
5483 assert!(client_c
5484 .summarize_contacts(cx_c)
5485 .outgoing_requests
5486 .is_empty());
5487
5488 async fn disconnect_and_reconnect(client: &TestClient, cx: &mut TestAppContext) {
5489 client.disconnect(&cx.to_async()).unwrap();
5490 client.clear_contacts(cx).await;
5491 client
5492 .authenticate_and_connect(false, &cx.to_async())
5493 .await
5494 .unwrap();
5495 }
5496}
5497
5498#[gpui::test(iterations = 10)]
5499async fn test_following(
5500 deterministic: Arc<Deterministic>,
5501 cx_a: &mut TestAppContext,
5502 cx_b: &mut TestAppContext,
5503) {
5504 deterministic.forbid_parking();
5505 cx_a.update(editor::init);
5506 cx_b.update(editor::init);
5507
5508 let mut server = TestServer::start(&deterministic).await;
5509 let client_a = server.create_client(cx_a, "user_a").await;
5510 let client_b = server.create_client(cx_b, "user_b").await;
5511 server
5512 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5513 .await;
5514 let active_call_a = cx_a.read(ActiveCall::global);
5515 let active_call_b = cx_b.read(ActiveCall::global);
5516
5517 client_a
5518 .fs
5519 .insert_tree(
5520 "/a",
5521 json!({
5522 "1.txt": "one\none\none",
5523 "2.txt": "two\ntwo\ntwo",
5524 "3.txt": "three\nthree\nthree",
5525 }),
5526 )
5527 .await;
5528 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
5529 active_call_a
5530 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
5531 .await
5532 .unwrap();
5533
5534 let project_id = active_call_a
5535 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5536 .await
5537 .unwrap();
5538 let project_b = client_b.build_remote_project(project_id, cx_b).await;
5539 active_call_b
5540 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
5541 .await
5542 .unwrap();
5543
5544 // Client A opens some editors.
5545 let workspace_a = client_a.build_workspace(&project_a, cx_a);
5546 let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
5547 let editor_a1 = workspace_a
5548 .update(cx_a, |workspace, cx| {
5549 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
5550 })
5551 .await
5552 .unwrap()
5553 .downcast::<Editor>()
5554 .unwrap();
5555 let editor_a2 = workspace_a
5556 .update(cx_a, |workspace, cx| {
5557 workspace.open_path((worktree_id, "2.txt"), None, true, cx)
5558 })
5559 .await
5560 .unwrap()
5561 .downcast::<Editor>()
5562 .unwrap();
5563
5564 // Client B opens an editor.
5565 let workspace_b = client_b.build_workspace(&project_b, cx_b);
5566 let editor_b1 = workspace_b
5567 .update(cx_b, |workspace, cx| {
5568 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
5569 })
5570 .await
5571 .unwrap()
5572 .downcast::<Editor>()
5573 .unwrap();
5574
5575 let client_a_id = project_b.read_with(cx_b, |project, _| {
5576 project.collaborators().values().next().unwrap().peer_id
5577 });
5578 let client_b_id = project_a.read_with(cx_a, |project, _| {
5579 project.collaborators().values().next().unwrap().peer_id
5580 });
5581
5582 // When client B starts following client A, all visible view states are replicated to client B.
5583 editor_a1.update(cx_a, |editor, cx| {
5584 editor.change_selections(None, cx, |s| s.select_ranges([0..1]))
5585 });
5586 editor_a2.update(cx_a, |editor, cx| {
5587 editor.change_selections(None, cx, |s| s.select_ranges([2..3]))
5588 });
5589 workspace_b
5590 .update(cx_b, |workspace, cx| {
5591 workspace
5592 .toggle_follow(&ToggleFollow(client_a_id), cx)
5593 .unwrap()
5594 })
5595 .await
5596 .unwrap();
5597
5598 let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
5599 workspace
5600 .active_item(cx)
5601 .unwrap()
5602 .downcast::<Editor>()
5603 .unwrap()
5604 });
5605 assert!(cx_b.read(|cx| editor_b2.is_focused(cx)));
5606 assert_eq!(
5607 cx_b.read(|cx| editor_b2.project_path(cx)),
5608 Some((worktree_id, "2.txt").into())
5609 );
5610 assert_eq!(
5611 editor_b2.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
5612 vec![2..3]
5613 );
5614 assert_eq!(
5615 editor_b1.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
5616 vec![0..1]
5617 );
5618
5619 // When client A activates a different editor, client B does so as well.
5620 workspace_a.update(cx_a, |workspace, cx| {
5621 workspace.activate_item(&editor_a1, cx)
5622 });
5623 deterministic.run_until_parked();
5624 workspace_b.read_with(cx_b, |workspace, cx| {
5625 assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
5626 });
5627
5628 // When client A opens a multibuffer, client B does so as well.
5629 let multibuffer_a = cx_a.add_model(|cx| {
5630 let buffer_a1 = project_a.update(cx, |project, cx| {
5631 project
5632 .get_open_buffer(&(worktree_id, "1.txt").into(), cx)
5633 .unwrap()
5634 });
5635 let buffer_a2 = project_a.update(cx, |project, cx| {
5636 project
5637 .get_open_buffer(&(worktree_id, "2.txt").into(), cx)
5638 .unwrap()
5639 });
5640 let mut result = MultiBuffer::new(0);
5641 result.push_excerpts(
5642 buffer_a1,
5643 [ExcerptRange {
5644 context: 0..3,
5645 primary: None,
5646 }],
5647 cx,
5648 );
5649 result.push_excerpts(
5650 buffer_a2,
5651 [ExcerptRange {
5652 context: 4..7,
5653 primary: None,
5654 }],
5655 cx,
5656 );
5657 result
5658 });
5659 let multibuffer_editor_a = workspace_a.update(cx_a, |workspace, cx| {
5660 let editor =
5661 cx.add_view(|cx| Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), cx));
5662 workspace.add_item(Box::new(editor.clone()), cx);
5663 editor
5664 });
5665 deterministic.run_until_parked();
5666 let multibuffer_editor_b = workspace_b.read_with(cx_b, |workspace, cx| {
5667 workspace
5668 .active_item(cx)
5669 .unwrap()
5670 .downcast::<Editor>()
5671 .unwrap()
5672 });
5673 assert_eq!(
5674 multibuffer_editor_a.read_with(cx_a, |editor, cx| editor.text(cx)),
5675 multibuffer_editor_b.read_with(cx_b, |editor, cx| editor.text(cx)),
5676 );
5677
5678 // When client A navigates back and forth, client B does so as well.
5679 workspace_a
5680 .update(cx_a, |workspace, cx| {
5681 workspace::Pane::go_back(workspace, None, cx)
5682 })
5683 .await;
5684 deterministic.run_until_parked();
5685 workspace_b.read_with(cx_b, |workspace, cx| {
5686 assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
5687 });
5688
5689 workspace_a
5690 .update(cx_a, |workspace, cx| {
5691 workspace::Pane::go_back(workspace, None, cx)
5692 })
5693 .await;
5694 deterministic.run_until_parked();
5695 workspace_b.read_with(cx_b, |workspace, cx| {
5696 assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b2.id());
5697 });
5698
5699 workspace_a
5700 .update(cx_a, |workspace, cx| {
5701 workspace::Pane::go_forward(workspace, None, cx)
5702 })
5703 .await;
5704 deterministic.run_until_parked();
5705 workspace_b.read_with(cx_b, |workspace, cx| {
5706 assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
5707 });
5708
5709 // Changes to client A's editor are reflected on client B.
5710 editor_a1.update(cx_a, |editor, cx| {
5711 editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2]));
5712 });
5713 deterministic.run_until_parked();
5714 editor_b1.read_with(cx_b, |editor, cx| {
5715 assert_eq!(editor.selections.ranges(cx), &[1..1, 2..2]);
5716 });
5717
5718 editor_a1.update(cx_a, |editor, cx| editor.set_text("TWO", cx));
5719 deterministic.run_until_parked();
5720 editor_b1.read_with(cx_b, |editor, cx| assert_eq!(editor.text(cx), "TWO"));
5721
5722 editor_a1.update(cx_a, |editor, cx| {
5723 editor.change_selections(None, cx, |s| s.select_ranges([3..3]));
5724 editor.set_scroll_position(vec2f(0., 100.), cx);
5725 });
5726 deterministic.run_until_parked();
5727 editor_b1.read_with(cx_b, |editor, cx| {
5728 assert_eq!(editor.selections.ranges(cx), &[3..3]);
5729 });
5730
5731 // After unfollowing, client B stops receiving updates from client A.
5732 workspace_b.update(cx_b, |workspace, cx| {
5733 workspace.unfollow(&workspace.active_pane().clone(), cx)
5734 });
5735 workspace_a.update(cx_a, |workspace, cx| {
5736 workspace.activate_item(&editor_a2, cx)
5737 });
5738 deterministic.run_until_parked();
5739 assert_eq!(
5740 workspace_b.read_with(cx_b, |workspace, cx| workspace
5741 .active_item(cx)
5742 .unwrap()
5743 .id()),
5744 editor_b1.id()
5745 );
5746
5747 // Client A starts following client B.
5748 workspace_a
5749 .update(cx_a, |workspace, cx| {
5750 workspace
5751 .toggle_follow(&ToggleFollow(client_b_id), cx)
5752 .unwrap()
5753 })
5754 .await
5755 .unwrap();
5756 assert_eq!(
5757 workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
5758 Some(client_b_id)
5759 );
5760 assert_eq!(
5761 workspace_a.read_with(cx_a, |workspace, cx| workspace
5762 .active_item(cx)
5763 .unwrap()
5764 .id()),
5765 editor_a1.id()
5766 );
5767
5768 // Client B activates an external window, which causes a new screen-sharing item to be added to the pane.
5769 let display = MacOSDisplay::new();
5770 active_call_b
5771 .update(cx_b, |call, cx| call.set_location(None, cx))
5772 .await
5773 .unwrap();
5774 active_call_b
5775 .update(cx_b, |call, cx| {
5776 call.room().unwrap().update(cx, |room, cx| {
5777 room.set_display_sources(vec![display.clone()]);
5778 room.share_screen(cx)
5779 })
5780 })
5781 .await
5782 .unwrap();
5783 deterministic.run_until_parked();
5784 let shared_screen = workspace_a.read_with(cx_a, |workspace, cx| {
5785 workspace
5786 .active_item(cx)
5787 .unwrap()
5788 .downcast::<SharedScreen>()
5789 .unwrap()
5790 });
5791
5792 // Client B activates Zed again, which causes the previous editor to become focused again.
5793 active_call_b
5794 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
5795 .await
5796 .unwrap();
5797 deterministic.run_until_parked();
5798 workspace_a.read_with(cx_a, |workspace, cx| {
5799 assert_eq!(workspace.active_item(cx).unwrap().id(), editor_a1.id())
5800 });
5801
5802 // Client B activates a multibuffer that was created by following client A. Client A returns to that multibuffer.
5803 workspace_b.update(cx_b, |workspace, cx| {
5804 workspace.activate_item(&multibuffer_editor_b, cx)
5805 });
5806 deterministic.run_until_parked();
5807 workspace_a.read_with(cx_a, |workspace, cx| {
5808 assert_eq!(
5809 workspace.active_item(cx).unwrap().id(),
5810 multibuffer_editor_a.id()
5811 )
5812 });
5813
5814 // Client B activates an external window again, and the previously-opened screen-sharing item
5815 // gets activated.
5816 active_call_b
5817 .update(cx_b, |call, cx| call.set_location(None, cx))
5818 .await
5819 .unwrap();
5820 deterministic.run_until_parked();
5821 assert_eq!(
5822 workspace_a.read_with(cx_a, |workspace, cx| workspace
5823 .active_item(cx)
5824 .unwrap()
5825 .id()),
5826 shared_screen.id()
5827 );
5828
5829 // Following interrupts when client B disconnects.
5830 client_b.disconnect(&cx_b.to_async()).unwrap();
5831 deterministic.advance_clock(RECONNECT_TIMEOUT);
5832 assert_eq!(
5833 workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
5834 None
5835 );
5836}
5837
5838#[gpui::test]
5839async fn test_following_tab_order(
5840 deterministic: Arc<Deterministic>,
5841 cx_a: &mut TestAppContext,
5842 cx_b: &mut TestAppContext,
5843) {
5844 cx_a.update(editor::init);
5845 cx_b.update(editor::init);
5846
5847 let mut server = TestServer::start(&deterministic).await;
5848 let client_a = server.create_client(cx_a, "user_a").await;
5849 let client_b = server.create_client(cx_b, "user_b").await;
5850 server
5851 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5852 .await;
5853 let active_call_a = cx_a.read(ActiveCall::global);
5854 let active_call_b = cx_b.read(ActiveCall::global);
5855
5856 client_a
5857 .fs
5858 .insert_tree(
5859 "/a",
5860 json!({
5861 "1.txt": "one",
5862 "2.txt": "two",
5863 "3.txt": "three",
5864 }),
5865 )
5866 .await;
5867 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
5868 active_call_a
5869 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
5870 .await
5871 .unwrap();
5872
5873 let project_id = active_call_a
5874 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5875 .await
5876 .unwrap();
5877 let project_b = client_b.build_remote_project(project_id, cx_b).await;
5878 active_call_b
5879 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
5880 .await
5881 .unwrap();
5882
5883 let workspace_a = client_a.build_workspace(&project_a, cx_a);
5884 let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
5885
5886 let workspace_b = client_b.build_workspace(&project_b, cx_b);
5887 let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
5888
5889 let client_b_id = project_a.read_with(cx_a, |project, _| {
5890 project.collaborators().values().next().unwrap().peer_id
5891 });
5892
5893 //Open 1, 3 in that order on client A
5894 workspace_a
5895 .update(cx_a, |workspace, cx| {
5896 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
5897 })
5898 .await
5899 .unwrap();
5900 workspace_a
5901 .update(cx_a, |workspace, cx| {
5902 workspace.open_path((worktree_id, "3.txt"), None, true, cx)
5903 })
5904 .await
5905 .unwrap();
5906
5907 let pane_paths = |pane: &ViewHandle<workspace::Pane>, cx: &mut TestAppContext| {
5908 pane.update(cx, |pane, cx| {
5909 pane.items()
5910 .map(|item| {
5911 item.project_path(cx)
5912 .unwrap()
5913 .path
5914 .to_str()
5915 .unwrap()
5916 .to_owned()
5917 })
5918 .collect::<Vec<_>>()
5919 })
5920 };
5921
5922 //Verify that the tabs opened in the order we expect
5923 assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt"]);
5924
5925 //Follow client B as client A
5926 workspace_a
5927 .update(cx_a, |workspace, cx| {
5928 workspace
5929 .toggle_follow(&ToggleFollow(client_b_id), cx)
5930 .unwrap()
5931 })
5932 .await
5933 .unwrap();
5934
5935 //Open just 2 on client B
5936 workspace_b
5937 .update(cx_b, |workspace, cx| {
5938 workspace.open_path((worktree_id, "2.txt"), None, true, cx)
5939 })
5940 .await
5941 .unwrap();
5942 deterministic.run_until_parked();
5943
5944 // Verify that newly opened followed file is at the end
5945 assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]);
5946
5947 //Open just 1 on client B
5948 workspace_b
5949 .update(cx_b, |workspace, cx| {
5950 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
5951 })
5952 .await
5953 .unwrap();
5954 assert_eq!(&pane_paths(&pane_b, cx_b), &["2.txt", "1.txt"]);
5955 deterministic.run_until_parked();
5956
5957 // Verify that following into 1 did not reorder
5958 assert_eq!(&pane_paths(&pane_a, cx_a), &["1.txt", "3.txt", "2.txt"]);
5959}
5960
5961#[gpui::test(iterations = 10)]
5962async fn test_peers_following_each_other(
5963 deterministic: Arc<Deterministic>,
5964 cx_a: &mut TestAppContext,
5965 cx_b: &mut TestAppContext,
5966) {
5967 deterministic.forbid_parking();
5968 cx_a.update(editor::init);
5969 cx_b.update(editor::init);
5970
5971 let mut server = TestServer::start(&deterministic).await;
5972 let client_a = server.create_client(cx_a, "user_a").await;
5973 let client_b = server.create_client(cx_b, "user_b").await;
5974 server
5975 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5976 .await;
5977 let active_call_a = cx_a.read(ActiveCall::global);
5978 let active_call_b = cx_b.read(ActiveCall::global);
5979
5980 // Client A shares a project.
5981 client_a
5982 .fs
5983 .insert_tree(
5984 "/a",
5985 json!({
5986 "1.txt": "one",
5987 "2.txt": "two",
5988 "3.txt": "three",
5989 "4.txt": "four",
5990 }),
5991 )
5992 .await;
5993 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
5994 active_call_a
5995 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
5996 .await
5997 .unwrap();
5998 let project_id = active_call_a
5999 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
6000 .await
6001 .unwrap();
6002
6003 // Client B joins the project.
6004 let project_b = client_b.build_remote_project(project_id, cx_b).await;
6005 active_call_b
6006 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
6007 .await
6008 .unwrap();
6009
6010 // Client A opens some editors.
6011 let workspace_a = client_a.build_workspace(&project_a, cx_a);
6012 let pane_a1 = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
6013 let _editor_a1 = workspace_a
6014 .update(cx_a, |workspace, cx| {
6015 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
6016 })
6017 .await
6018 .unwrap()
6019 .downcast::<Editor>()
6020 .unwrap();
6021
6022 // Client B opens an editor.
6023 let workspace_b = client_b.build_workspace(&project_b, cx_b);
6024 let pane_b1 = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
6025 let _editor_b1 = workspace_b
6026 .update(cx_b, |workspace, cx| {
6027 workspace.open_path((worktree_id, "2.txt"), None, true, cx)
6028 })
6029 .await
6030 .unwrap()
6031 .downcast::<Editor>()
6032 .unwrap();
6033
6034 // Clients A and B follow each other in split panes
6035 workspace_a.update(cx_a, |workspace, cx| {
6036 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
6037 let pane_a1 = pane_a1.clone();
6038 cx.defer(move |workspace, _| {
6039 assert_ne!(*workspace.active_pane(), pane_a1);
6040 });
6041 });
6042 workspace_a
6043 .update(cx_a, |workspace, cx| {
6044 let leader_id = *project_a.read(cx).collaborators().keys().next().unwrap();
6045 workspace
6046 .toggle_follow(&workspace::ToggleFollow(leader_id), cx)
6047 .unwrap()
6048 })
6049 .await
6050 .unwrap();
6051 workspace_b.update(cx_b, |workspace, cx| {
6052 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
6053 let pane_b1 = pane_b1.clone();
6054 cx.defer(move |workspace, _| {
6055 assert_ne!(*workspace.active_pane(), pane_b1);
6056 });
6057 });
6058 workspace_b
6059 .update(cx_b, |workspace, cx| {
6060 let leader_id = *project_b.read(cx).collaborators().keys().next().unwrap();
6061 workspace
6062 .toggle_follow(&workspace::ToggleFollow(leader_id), cx)
6063 .unwrap()
6064 })
6065 .await
6066 .unwrap();
6067
6068 workspace_a.update(cx_a, |workspace, cx| {
6069 workspace.activate_next_pane(cx);
6070 });
6071 // Wait for focus effects to be fully flushed
6072 workspace_a.update(cx_a, |workspace, _| {
6073 assert_eq!(*workspace.active_pane(), pane_a1);
6074 });
6075
6076 workspace_a
6077 .update(cx_a, |workspace, cx| {
6078 workspace.open_path((worktree_id, "3.txt"), None, true, cx)
6079 })
6080 .await
6081 .unwrap();
6082 workspace_b.update(cx_b, |workspace, cx| {
6083 workspace.activate_next_pane(cx);
6084 });
6085
6086 workspace_b
6087 .update(cx_b, |workspace, cx| {
6088 assert_eq!(*workspace.active_pane(), pane_b1);
6089 workspace.open_path((worktree_id, "4.txt"), None, true, cx)
6090 })
6091 .await
6092 .unwrap();
6093 cx_a.foreground().run_until_parked();
6094
6095 // Ensure leader updates don't change the active pane of followers
6096 workspace_a.read_with(cx_a, |workspace, _| {
6097 assert_eq!(*workspace.active_pane(), pane_a1);
6098 });
6099 workspace_b.read_with(cx_b, |workspace, _| {
6100 assert_eq!(*workspace.active_pane(), pane_b1);
6101 });
6102
6103 // Ensure peers following each other doesn't cause an infinite loop.
6104 assert_eq!(
6105 workspace_a.read_with(cx_a, |workspace, cx| workspace
6106 .active_item(cx)
6107 .unwrap()
6108 .project_path(cx)),
6109 Some((worktree_id, "3.txt").into())
6110 );
6111 workspace_a.update(cx_a, |workspace, cx| {
6112 assert_eq!(
6113 workspace.active_item(cx).unwrap().project_path(cx),
6114 Some((worktree_id, "3.txt").into())
6115 );
6116 workspace.activate_next_pane(cx);
6117 });
6118
6119 workspace_a.update(cx_a, |workspace, cx| {
6120 assert_eq!(
6121 workspace.active_item(cx).unwrap().project_path(cx),
6122 Some((worktree_id, "4.txt").into())
6123 );
6124 });
6125
6126 workspace_b.update(cx_b, |workspace, cx| {
6127 assert_eq!(
6128 workspace.active_item(cx).unwrap().project_path(cx),
6129 Some((worktree_id, "4.txt").into())
6130 );
6131 workspace.activate_next_pane(cx);
6132 });
6133
6134 workspace_b.update(cx_b, |workspace, cx| {
6135 assert_eq!(
6136 workspace.active_item(cx).unwrap().project_path(cx),
6137 Some((worktree_id, "3.txt").into())
6138 );
6139 });
6140}
6141
6142#[gpui::test(iterations = 10)]
6143async fn test_auto_unfollowing(
6144 deterministic: Arc<Deterministic>,
6145 cx_a: &mut TestAppContext,
6146 cx_b: &mut TestAppContext,
6147) {
6148 deterministic.forbid_parking();
6149 cx_a.update(editor::init);
6150 cx_b.update(editor::init);
6151
6152 // 2 clients connect to a server.
6153 let mut server = TestServer::start(&deterministic).await;
6154 let client_a = server.create_client(cx_a, "user_a").await;
6155 let client_b = server.create_client(cx_b, "user_b").await;
6156 server
6157 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
6158 .await;
6159 let active_call_a = cx_a.read(ActiveCall::global);
6160 let active_call_b = cx_b.read(ActiveCall::global);
6161
6162 // Client A shares a project.
6163 client_a
6164 .fs
6165 .insert_tree(
6166 "/a",
6167 json!({
6168 "1.txt": "one",
6169 "2.txt": "two",
6170 "3.txt": "three",
6171 }),
6172 )
6173 .await;
6174 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
6175 active_call_a
6176 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
6177 .await
6178 .unwrap();
6179
6180 let project_id = active_call_a
6181 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
6182 .await
6183 .unwrap();
6184 let project_b = client_b.build_remote_project(project_id, cx_b).await;
6185 active_call_b
6186 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
6187 .await
6188 .unwrap();
6189
6190 // Client A opens some editors.
6191 let workspace_a = client_a.build_workspace(&project_a, cx_a);
6192 let _editor_a1 = workspace_a
6193 .update(cx_a, |workspace, cx| {
6194 workspace.open_path((worktree_id, "1.txt"), None, true, cx)
6195 })
6196 .await
6197 .unwrap()
6198 .downcast::<Editor>()
6199 .unwrap();
6200
6201 // Client B starts following client A.
6202 let workspace_b = client_b.build_workspace(&project_b, cx_b);
6203 let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
6204 let leader_id = project_b.read_with(cx_b, |project, _| {
6205 project.collaborators().values().next().unwrap().peer_id
6206 });
6207 workspace_b
6208 .update(cx_b, |workspace, cx| {
6209 workspace
6210 .toggle_follow(&ToggleFollow(leader_id), cx)
6211 .unwrap()
6212 })
6213 .await
6214 .unwrap();
6215 assert_eq!(
6216 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
6217 Some(leader_id)
6218 );
6219 let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
6220 workspace
6221 .active_item(cx)
6222 .unwrap()
6223 .downcast::<Editor>()
6224 .unwrap()
6225 });
6226
6227 // When client B moves, it automatically stops following client A.
6228 editor_b2.update(cx_b, |editor, cx| editor.move_right(&editor::MoveRight, cx));
6229 assert_eq!(
6230 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
6231 None
6232 );
6233
6234 workspace_b
6235 .update(cx_b, |workspace, cx| {
6236 workspace
6237 .toggle_follow(&ToggleFollow(leader_id), cx)
6238 .unwrap()
6239 })
6240 .await
6241 .unwrap();
6242 assert_eq!(
6243 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
6244 Some(leader_id)
6245 );
6246
6247 // When client B edits, it automatically stops following client A.
6248 editor_b2.update(cx_b, |editor, cx| editor.insert("X", cx));
6249 assert_eq!(
6250 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
6251 None
6252 );
6253
6254 workspace_b
6255 .update(cx_b, |workspace, cx| {
6256 workspace
6257 .toggle_follow(&ToggleFollow(leader_id), cx)
6258 .unwrap()
6259 })
6260 .await
6261 .unwrap();
6262 assert_eq!(
6263 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
6264 Some(leader_id)
6265 );
6266
6267 // When client B scrolls, it automatically stops following client A.
6268 editor_b2.update(cx_b, |editor, cx| {
6269 editor.set_scroll_position(vec2f(0., 3.), cx)
6270 });
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 activates a different pane, it continues following client A in the original pane.
6290 workspace_b.update(cx_b, |workspace, cx| {
6291 workspace.split_pane(pane_b.clone(), SplitDirection::Right, cx)
6292 });
6293 assert_eq!(
6294 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
6295 Some(leader_id)
6296 );
6297
6298 workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
6299 assert_eq!(
6300 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
6301 Some(leader_id)
6302 );
6303
6304 // When client B activates a different item in the original pane, it automatically stops following client A.
6305 workspace_b
6306 .update(cx_b, |workspace, cx| {
6307 workspace.open_path((worktree_id, "2.txt"), None, true, cx)
6308 })
6309 .await
6310 .unwrap();
6311 assert_eq!(
6312 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
6313 None
6314 );
6315}
6316
6317#[gpui::test(iterations = 10)]
6318async fn test_peers_simultaneously_following_each_other(
6319 deterministic: Arc<Deterministic>,
6320 cx_a: &mut TestAppContext,
6321 cx_b: &mut TestAppContext,
6322) {
6323 deterministic.forbid_parking();
6324 cx_a.update(editor::init);
6325 cx_b.update(editor::init);
6326
6327 let mut server = TestServer::start(&deterministic).await;
6328 let client_a = server.create_client(cx_a, "user_a").await;
6329 let client_b = server.create_client(cx_b, "user_b").await;
6330 server
6331 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
6332 .await;
6333 let active_call_a = cx_a.read(ActiveCall::global);
6334
6335 client_a.fs.insert_tree("/a", json!({})).await;
6336 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
6337 let workspace_a = client_a.build_workspace(&project_a, cx_a);
6338 let project_id = active_call_a
6339 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
6340 .await
6341 .unwrap();
6342
6343 let project_b = client_b.build_remote_project(project_id, cx_b).await;
6344 let workspace_b = client_b.build_workspace(&project_b, cx_b);
6345
6346 deterministic.run_until_parked();
6347 let client_a_id = project_b.read_with(cx_b, |project, _| {
6348 project.collaborators().values().next().unwrap().peer_id
6349 });
6350 let client_b_id = project_a.read_with(cx_a, |project, _| {
6351 project.collaborators().values().next().unwrap().peer_id
6352 });
6353
6354 let a_follow_b = workspace_a.update(cx_a, |workspace, cx| {
6355 workspace
6356 .toggle_follow(&ToggleFollow(client_b_id), cx)
6357 .unwrap()
6358 });
6359 let b_follow_a = workspace_b.update(cx_b, |workspace, cx| {
6360 workspace
6361 .toggle_follow(&ToggleFollow(client_a_id), cx)
6362 .unwrap()
6363 });
6364
6365 futures::try_join!(a_follow_b, b_follow_a).unwrap();
6366 workspace_a.read_with(cx_a, |workspace, _| {
6367 assert_eq!(
6368 workspace.leader_for_pane(workspace.active_pane()),
6369 Some(client_b_id)
6370 );
6371 });
6372 workspace_b.read_with(cx_b, |workspace, _| {
6373 assert_eq!(
6374 workspace.leader_for_pane(workspace.active_pane()),
6375 Some(client_a_id)
6376 );
6377 });
6378}
6379
6380#[derive(Debug, Eq, PartialEq)]
6381struct RoomParticipants {
6382 remote: Vec<String>,
6383 pending: Vec<String>,
6384}
6385
6386fn room_participants(room: &ModelHandle<Room>, cx: &mut TestAppContext) -> RoomParticipants {
6387 room.read_with(cx, |room, _| {
6388 let mut remote = room
6389 .remote_participants()
6390 .iter()
6391 .map(|(_, participant)| participant.user.github_login.clone())
6392 .collect::<Vec<_>>();
6393 let mut pending = room
6394 .pending_participants()
6395 .iter()
6396 .map(|user| user.github_login.clone())
6397 .collect::<Vec<_>>();
6398 remote.sort();
6399 pending.sort();
6400 RoomParticipants { remote, pending }
6401 })
6402}