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