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