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