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