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_b1 = 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_b1.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 // Client B opens a buffer.
2036 let buffer_b1 = project_b1
2037 .update(cx_b, |project, cx| {
2038 let worktree_id = project.worktrees(cx).next().unwrap().read(cx).id();
2039 project.open_buffer((worktree_id, "a.txt"), cx)
2040 })
2041 .await
2042 .unwrap();
2043 buffer_b1.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "a-contents"));
2044
2045 // Drop client B's project and ensure client A and client C observe client B leaving.
2046 cx_b.update(|_| drop(project_b1));
2047 deterministic.run_until_parked();
2048 project_a.read_with(cx_a, |project, _| {
2049 assert_eq!(project.collaborators().len(), 1);
2050 });
2051 project_c.read_with(cx_c, |project, _| {
2052 assert_eq!(project.collaborators().len(), 1);
2053 });
2054
2055 // Client B re-joins the project and can open buffers as before.
2056 let project_b2 = client_b.build_remote_project(project_id, cx_b).await;
2057 deterministic.run_until_parked();
2058 project_a.read_with(cx_a, |project, _| {
2059 assert_eq!(project.collaborators().len(), 2);
2060 });
2061 project_b2.read_with(cx_b, |project, _| {
2062 assert_eq!(project.collaborators().len(), 2);
2063 });
2064 project_c.read_with(cx_c, |project, _| {
2065 assert_eq!(project.collaborators().len(), 2);
2066 });
2067
2068 let buffer_b2 = project_b2
2069 .update(cx_b, |project, cx| {
2070 let worktree_id = project.worktrees(cx).next().unwrap().read(cx).id();
2071 project.open_buffer((worktree_id, "a.txt"), cx)
2072 })
2073 .await
2074 .unwrap();
2075 buffer_b2.read_with(cx_b, |buffer, _| assert_eq!(buffer.text(), "a-contents"));
2076
2077 // Drop client B's connection and ensure client A and client C observe client B leaving.
2078 client_b.disconnect(&cx_b.to_async()).unwrap();
2079 deterministic.run_until_parked();
2080 project_a.read_with(cx_a, |project, _| {
2081 assert_eq!(project.collaborators().len(), 1);
2082 });
2083 project_b2.read_with(cx_b, |project, _| {
2084 assert!(project.is_read_only());
2085 });
2086 project_c.read_with(cx_c, |project, _| {
2087 assert_eq!(project.collaborators().len(), 1);
2088 });
2089
2090 // Client B can't join the project, unless they re-join the room.
2091 cx_b.spawn(|cx| {
2092 Project::remote(
2093 project_id,
2094 client_b.client.clone(),
2095 client_b.user_store.clone(),
2096 client_b.project_store.clone(),
2097 client_b.language_registry.clone(),
2098 FakeFs::new(cx.background()),
2099 cx,
2100 )
2101 })
2102 .await
2103 .unwrap_err();
2104
2105 // Simulate connection loss for client C and ensure client A observes client C leaving the project.
2106 client_c.wait_for_current_user(cx_c).await;
2107 server.disconnect_client(client_c.current_user_id(cx_c));
2108 cx_a.foreground().advance_clock(rpc::RECEIVE_TIMEOUT);
2109 deterministic.run_until_parked();
2110 project_a.read_with(cx_a, |project, _| {
2111 assert_eq!(project.collaborators().len(), 0);
2112 });
2113 project_b2.read_with(cx_b, |project, _| {
2114 assert!(project.is_read_only());
2115 });
2116 project_c.read_with(cx_c, |project, _| {
2117 assert!(project.is_read_only());
2118 });
2119}
2120
2121#[gpui::test(iterations = 10)]
2122async fn test_collaborating_with_diagnostics(
2123 deterministic: Arc<Deterministic>,
2124 cx_a: &mut TestAppContext,
2125 cx_b: &mut TestAppContext,
2126 cx_c: &mut TestAppContext,
2127) {
2128 deterministic.forbid_parking();
2129 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
2130 let client_a = server.create_client(cx_a, "user_a").await;
2131 let client_b = server.create_client(cx_b, "user_b").await;
2132 let client_c = server.create_client(cx_c, "user_c").await;
2133 server
2134 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
2135 .await;
2136 let active_call_a = cx_a.read(ActiveCall::global);
2137
2138 // Set up a fake language server.
2139 let mut language = Language::new(
2140 LanguageConfig {
2141 name: "Rust".into(),
2142 path_suffixes: vec!["rs".to_string()],
2143 ..Default::default()
2144 },
2145 Some(tree_sitter_rust::language()),
2146 );
2147 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
2148 client_a.language_registry.add(Arc::new(language));
2149
2150 // Share a project as client A
2151 client_a
2152 .fs
2153 .insert_tree(
2154 "/a",
2155 json!({
2156 "a.rs": "let one = two",
2157 "other.rs": "",
2158 }),
2159 )
2160 .await;
2161 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
2162 let project_id = active_call_a
2163 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2164 .await
2165 .unwrap();
2166
2167 // Cause the language server to start.
2168 let _buffer = cx_a
2169 .background()
2170 .spawn(project_a.update(cx_a, |project, cx| {
2171 project.open_buffer(
2172 ProjectPath {
2173 worktree_id,
2174 path: Path::new("other.rs").into(),
2175 },
2176 cx,
2177 )
2178 }))
2179 .await
2180 .unwrap();
2181
2182 // Join the worktree as client B.
2183 let project_b = client_b.build_remote_project(project_id, cx_b).await;
2184
2185 // Simulate a language server reporting errors for a file.
2186 let mut fake_language_server = fake_language_servers.next().await.unwrap();
2187 fake_language_server
2188 .receive_notification::<lsp::notification::DidOpenTextDocument>()
2189 .await;
2190 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
2191 lsp::PublishDiagnosticsParams {
2192 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
2193 version: None,
2194 diagnostics: vec![lsp::Diagnostic {
2195 severity: Some(lsp::DiagnosticSeverity::ERROR),
2196 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
2197 message: "message 1".to_string(),
2198 ..Default::default()
2199 }],
2200 },
2201 );
2202
2203 // Wait for server to see the diagnostics update.
2204 deterministic.run_until_parked();
2205 {
2206 let store = server.store.lock().await;
2207 let project = store.project(ProjectId::from_proto(project_id)).unwrap();
2208 let worktree = project.worktrees.get(&worktree_id.to_proto()).unwrap();
2209 assert!(!worktree.diagnostic_summaries.is_empty());
2210 }
2211
2212 // Ensure client B observes the new diagnostics.
2213 project_b.read_with(cx_b, |project, cx| {
2214 assert_eq!(
2215 project.diagnostic_summaries(cx).collect::<Vec<_>>(),
2216 &[(
2217 ProjectPath {
2218 worktree_id,
2219 path: Arc::from(Path::new("a.rs")),
2220 },
2221 DiagnosticSummary {
2222 error_count: 1,
2223 warning_count: 0,
2224 ..Default::default()
2225 },
2226 )]
2227 )
2228 });
2229
2230 // Join project as client C and observe the diagnostics.
2231 let project_c = client_c.build_remote_project(project_id, cx_c).await;
2232 deterministic.run_until_parked();
2233 project_c.read_with(cx_c, |project, cx| {
2234 assert_eq!(
2235 project.diagnostic_summaries(cx).collect::<Vec<_>>(),
2236 &[(
2237 ProjectPath {
2238 worktree_id,
2239 path: Arc::from(Path::new("a.rs")),
2240 },
2241 DiagnosticSummary {
2242 error_count: 1,
2243 warning_count: 0,
2244 ..Default::default()
2245 },
2246 )]
2247 )
2248 });
2249
2250 // Simulate a language server reporting more errors for a file.
2251 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
2252 lsp::PublishDiagnosticsParams {
2253 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
2254 version: None,
2255 diagnostics: vec![
2256 lsp::Diagnostic {
2257 severity: Some(lsp::DiagnosticSeverity::ERROR),
2258 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
2259 message: "message 1".to_string(),
2260 ..Default::default()
2261 },
2262 lsp::Diagnostic {
2263 severity: Some(lsp::DiagnosticSeverity::WARNING),
2264 range: lsp::Range::new(lsp::Position::new(0, 10), lsp::Position::new(0, 13)),
2265 message: "message 2".to_string(),
2266 ..Default::default()
2267 },
2268 ],
2269 },
2270 );
2271
2272 // Clients B and C get the updated summaries
2273 deterministic.run_until_parked();
2274 project_b.read_with(cx_b, |project, cx| {
2275 assert_eq!(
2276 project.diagnostic_summaries(cx).collect::<Vec<_>>(),
2277 [(
2278 ProjectPath {
2279 worktree_id,
2280 path: Arc::from(Path::new("a.rs")),
2281 },
2282 DiagnosticSummary {
2283 error_count: 1,
2284 warning_count: 1,
2285 ..Default::default()
2286 },
2287 )]
2288 );
2289 });
2290 project_c.read_with(cx_c, |project, cx| {
2291 assert_eq!(
2292 project.diagnostic_summaries(cx).collect::<Vec<_>>(),
2293 [(
2294 ProjectPath {
2295 worktree_id,
2296 path: Arc::from(Path::new("a.rs")),
2297 },
2298 DiagnosticSummary {
2299 error_count: 1,
2300 warning_count: 1,
2301 ..Default::default()
2302 },
2303 )]
2304 );
2305 });
2306
2307 // Open the file with the errors on client B. They should be present.
2308 let buffer_b = cx_b
2309 .background()
2310 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
2311 .await
2312 .unwrap();
2313
2314 buffer_b.read_with(cx_b, |buffer, _| {
2315 assert_eq!(
2316 buffer
2317 .snapshot()
2318 .diagnostics_in_range::<_, Point>(0..buffer.len(), false)
2319 .collect::<Vec<_>>(),
2320 &[
2321 DiagnosticEntry {
2322 range: Point::new(0, 4)..Point::new(0, 7),
2323 diagnostic: Diagnostic {
2324 group_id: 1,
2325 message: "message 1".to_string(),
2326 severity: lsp::DiagnosticSeverity::ERROR,
2327 is_primary: true,
2328 ..Default::default()
2329 }
2330 },
2331 DiagnosticEntry {
2332 range: Point::new(0, 10)..Point::new(0, 13),
2333 diagnostic: Diagnostic {
2334 group_id: 2,
2335 severity: lsp::DiagnosticSeverity::WARNING,
2336 message: "message 2".to_string(),
2337 is_primary: true,
2338 ..Default::default()
2339 }
2340 }
2341 ]
2342 );
2343 });
2344
2345 // Simulate a language server reporting no errors for a file.
2346 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
2347 lsp::PublishDiagnosticsParams {
2348 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
2349 version: None,
2350 diagnostics: vec![],
2351 },
2352 );
2353 deterministic.run_until_parked();
2354 project_a.read_with(cx_a, |project, cx| {
2355 assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
2356 });
2357 project_b.read_with(cx_b, |project, cx| {
2358 assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
2359 });
2360 project_c.read_with(cx_c, |project, cx| {
2361 assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
2362 });
2363}
2364
2365#[gpui::test(iterations = 10)]
2366async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2367 cx_a.foreground().forbid_parking();
2368 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
2369 let client_a = server.create_client(cx_a, "user_a").await;
2370 let client_b = server.create_client(cx_b, "user_b").await;
2371 server
2372 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2373 .await;
2374 let active_call_a = cx_a.read(ActiveCall::global);
2375
2376 // Set up a fake language server.
2377 let mut language = Language::new(
2378 LanguageConfig {
2379 name: "Rust".into(),
2380 path_suffixes: vec!["rs".to_string()],
2381 ..Default::default()
2382 },
2383 Some(tree_sitter_rust::language()),
2384 );
2385 let mut fake_language_servers = language
2386 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
2387 capabilities: lsp::ServerCapabilities {
2388 completion_provider: Some(lsp::CompletionOptions {
2389 trigger_characters: Some(vec![".".to_string()]),
2390 ..Default::default()
2391 }),
2392 ..Default::default()
2393 },
2394 ..Default::default()
2395 }))
2396 .await;
2397 client_a.language_registry.add(Arc::new(language));
2398
2399 client_a
2400 .fs
2401 .insert_tree(
2402 "/a",
2403 json!({
2404 "main.rs": "fn main() { a }",
2405 "other.rs": "",
2406 }),
2407 )
2408 .await;
2409 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
2410 let project_id = active_call_a
2411 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2412 .await
2413 .unwrap();
2414 let project_b = client_b.build_remote_project(project_id, cx_b).await;
2415
2416 // Open a file in an editor as the guest.
2417 let buffer_b = project_b
2418 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
2419 .await
2420 .unwrap();
2421 let (_, window_b) = cx_b.add_window(|_| EmptyView);
2422 let editor_b = cx_b.add_view(&window_b, |cx| {
2423 Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), cx)
2424 });
2425
2426 let fake_language_server = fake_language_servers.next().await.unwrap();
2427 buffer_b
2428 .condition(cx_b, |buffer, _| !buffer.completion_triggers().is_empty())
2429 .await;
2430
2431 // Type a completion trigger character as the guest.
2432 editor_b.update(cx_b, |editor, cx| {
2433 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
2434 editor.handle_input(".", cx);
2435 cx.focus(&editor_b);
2436 });
2437
2438 // Receive a completion request as the host's language server.
2439 // Return some completions from the host's language server.
2440 cx_a.foreground().start_waiting();
2441 fake_language_server
2442 .handle_request::<lsp::request::Completion, _, _>(|params, _| async move {
2443 assert_eq!(
2444 params.text_document_position.text_document.uri,
2445 lsp::Url::from_file_path("/a/main.rs").unwrap(),
2446 );
2447 assert_eq!(
2448 params.text_document_position.position,
2449 lsp::Position::new(0, 14),
2450 );
2451
2452 Ok(Some(lsp::CompletionResponse::Array(vec![
2453 lsp::CompletionItem {
2454 label: "first_method(…)".into(),
2455 detail: Some("fn(&mut self, B) -> C".into()),
2456 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
2457 new_text: "first_method($1)".to_string(),
2458 range: lsp::Range::new(
2459 lsp::Position::new(0, 14),
2460 lsp::Position::new(0, 14),
2461 ),
2462 })),
2463 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
2464 ..Default::default()
2465 },
2466 lsp::CompletionItem {
2467 label: "second_method(…)".into(),
2468 detail: Some("fn(&mut self, C) -> D<E>".into()),
2469 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
2470 new_text: "second_method()".to_string(),
2471 range: lsp::Range::new(
2472 lsp::Position::new(0, 14),
2473 lsp::Position::new(0, 14),
2474 ),
2475 })),
2476 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
2477 ..Default::default()
2478 },
2479 ])))
2480 })
2481 .next()
2482 .await
2483 .unwrap();
2484 cx_a.foreground().finish_waiting();
2485
2486 // Open the buffer on the host.
2487 let buffer_a = project_a
2488 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
2489 .await
2490 .unwrap();
2491 buffer_a
2492 .condition(cx_a, |buffer, _| buffer.text() == "fn main() { a. }")
2493 .await;
2494
2495 // Confirm a completion on the guest.
2496 editor_b
2497 .condition(cx_b, |editor, _| editor.context_menu_visible())
2498 .await;
2499 editor_b.update(cx_b, |editor, cx| {
2500 editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx);
2501 assert_eq!(editor.text(cx), "fn main() { a.first_method() }");
2502 });
2503
2504 // Return a resolved completion from the host's language server.
2505 // The resolved completion has an additional text edit.
2506 fake_language_server.handle_request::<lsp::request::ResolveCompletionItem, _, _>(
2507 |params, _| async move {
2508 assert_eq!(params.label, "first_method(…)");
2509 Ok(lsp::CompletionItem {
2510 label: "first_method(…)".into(),
2511 detail: Some("fn(&mut self, B) -> C".into()),
2512 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
2513 new_text: "first_method($1)".to_string(),
2514 range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
2515 })),
2516 additional_text_edits: Some(vec![lsp::TextEdit {
2517 new_text: "use d::SomeTrait;\n".to_string(),
2518 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
2519 }]),
2520 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
2521 ..Default::default()
2522 })
2523 },
2524 );
2525
2526 // The additional edit is applied.
2527 buffer_a
2528 .condition(cx_a, |buffer, _| {
2529 buffer.text() == "use d::SomeTrait;\nfn main() { a.first_method() }"
2530 })
2531 .await;
2532 buffer_b
2533 .condition(cx_b, |buffer, _| {
2534 buffer.text() == "use d::SomeTrait;\nfn main() { a.first_method() }"
2535 })
2536 .await;
2537}
2538
2539#[gpui::test(iterations = 10)]
2540async fn test_reloading_buffer_manually(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2541 cx_a.foreground().forbid_parking();
2542 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
2543 let client_a = server.create_client(cx_a, "user_a").await;
2544 let client_b = server.create_client(cx_b, "user_b").await;
2545 server
2546 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2547 .await;
2548 let active_call_a = cx_a.read(ActiveCall::global);
2549
2550 client_a
2551 .fs
2552 .insert_tree("/a", json!({ "a.rs": "let one = 1;" }))
2553 .await;
2554 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
2555 let buffer_a = project_a
2556 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))
2557 .await
2558 .unwrap();
2559 let project_id = active_call_a
2560 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2561 .await
2562 .unwrap();
2563
2564 let project_b = client_b.build_remote_project(project_id, cx_b).await;
2565
2566 let buffer_b = cx_b
2567 .background()
2568 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
2569 .await
2570 .unwrap();
2571 buffer_b.update(cx_b, |buffer, cx| {
2572 buffer.edit([(4..7, "six")], None, cx);
2573 buffer.edit([(10..11, "6")], None, cx);
2574 assert_eq!(buffer.text(), "let six = 6;");
2575 assert!(buffer.is_dirty());
2576 assert!(!buffer.has_conflict());
2577 });
2578 buffer_a
2579 .condition(cx_a, |buffer, _| buffer.text() == "let six = 6;")
2580 .await;
2581
2582 client_a
2583 .fs
2584 .save(
2585 "/a/a.rs".as_ref(),
2586 &Rope::from("let seven = 7;"),
2587 LineEnding::Unix,
2588 )
2589 .await
2590 .unwrap();
2591 buffer_a
2592 .condition(cx_a, |buffer, _| buffer.has_conflict())
2593 .await;
2594 buffer_b
2595 .condition(cx_b, |buffer, _| buffer.has_conflict())
2596 .await;
2597
2598 project_b
2599 .update(cx_b, |project, cx| {
2600 project.reload_buffers(HashSet::from_iter([buffer_b.clone()]), true, cx)
2601 })
2602 .await
2603 .unwrap();
2604 buffer_a.read_with(cx_a, |buffer, _| {
2605 assert_eq!(buffer.text(), "let seven = 7;");
2606 assert!(!buffer.is_dirty());
2607 assert!(!buffer.has_conflict());
2608 });
2609 buffer_b.read_with(cx_b, |buffer, _| {
2610 assert_eq!(buffer.text(), "let seven = 7;");
2611 assert!(!buffer.is_dirty());
2612 assert!(!buffer.has_conflict());
2613 });
2614
2615 buffer_a.update(cx_a, |buffer, cx| {
2616 // Undoing on the host is a no-op when the reload was initiated by the guest.
2617 buffer.undo(cx);
2618 assert_eq!(buffer.text(), "let seven = 7;");
2619 assert!(!buffer.is_dirty());
2620 assert!(!buffer.has_conflict());
2621 });
2622 buffer_b.update(cx_b, |buffer, cx| {
2623 // Undoing on the guest rolls back the buffer to before it was reloaded but the conflict gets cleared.
2624 buffer.undo(cx);
2625 assert_eq!(buffer.text(), "let six = 6;");
2626 assert!(buffer.is_dirty());
2627 assert!(!buffer.has_conflict());
2628 });
2629}
2630
2631#[gpui::test(iterations = 10)]
2632async fn test_formatting_buffer(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2633 use project::FormatTrigger;
2634
2635 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
2636 let client_a = server.create_client(cx_a, "user_a").await;
2637 let client_b = server.create_client(cx_b, "user_b").await;
2638 server
2639 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2640 .await;
2641 let active_call_a = cx_a.read(ActiveCall::global);
2642
2643 // Set up a fake language server.
2644 let mut language = Language::new(
2645 LanguageConfig {
2646 name: "Rust".into(),
2647 path_suffixes: vec!["rs".to_string()],
2648 ..Default::default()
2649 },
2650 Some(tree_sitter_rust::language()),
2651 );
2652 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
2653 client_a.language_registry.add(Arc::new(language));
2654
2655 // Here we insert a fake tree with a directory that exists on disk. This is needed
2656 // because later we'll invoke a command, which requires passing a working directory
2657 // that points to a valid location on disk.
2658 let directory = env::current_dir().unwrap();
2659 client_a
2660 .fs
2661 .insert_tree(&directory, json!({ "a.rs": "let one = \"two\"" }))
2662 .await;
2663 let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await;
2664 let project_id = active_call_a
2665 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2666 .await
2667 .unwrap();
2668 let project_b = client_b.build_remote_project(project_id, cx_b).await;
2669
2670 let buffer_b = cx_b
2671 .background()
2672 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
2673 .await
2674 .unwrap();
2675
2676 let fake_language_server = fake_language_servers.next().await.unwrap();
2677 fake_language_server.handle_request::<lsp::request::Formatting, _, _>(|_, _| async move {
2678 Ok(Some(vec![
2679 lsp::TextEdit {
2680 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 4)),
2681 new_text: "h".to_string(),
2682 },
2683 lsp::TextEdit {
2684 range: lsp::Range::new(lsp::Position::new(0, 7), lsp::Position::new(0, 7)),
2685 new_text: "y".to_string(),
2686 },
2687 ]))
2688 });
2689
2690 project_b
2691 .update(cx_b, |project, cx| {
2692 project.format(
2693 HashSet::from_iter([buffer_b.clone()]),
2694 true,
2695 FormatTrigger::Save,
2696 cx,
2697 )
2698 })
2699 .await
2700 .unwrap();
2701 assert_eq!(
2702 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
2703 "let honey = \"two\""
2704 );
2705
2706 // Ensure buffer can be formatted using an external command. Notice how the
2707 // host's configuration is honored as opposed to using the guest's settings.
2708 cx_a.update(|cx| {
2709 cx.update_global(|settings: &mut Settings, _| {
2710 settings.editor_defaults.formatter = Some(Formatter::External {
2711 command: "awk".to_string(),
2712 arguments: vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()],
2713 });
2714 });
2715 });
2716 project_b
2717 .update(cx_b, |project, cx| {
2718 project.format(
2719 HashSet::from_iter([buffer_b.clone()]),
2720 true,
2721 FormatTrigger::Save,
2722 cx,
2723 )
2724 })
2725 .await
2726 .unwrap();
2727 assert_eq!(
2728 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
2729 format!("let honey = \"{}/a.rs\"\n", directory.to_str().unwrap())
2730 );
2731}
2732
2733#[gpui::test(iterations = 10)]
2734async fn test_definition(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2735 cx_a.foreground().forbid_parking();
2736 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
2737 let client_a = server.create_client(cx_a, "user_a").await;
2738 let client_b = server.create_client(cx_b, "user_b").await;
2739 server
2740 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2741 .await;
2742 let active_call_a = cx_a.read(ActiveCall::global);
2743
2744 // Set up a fake language server.
2745 let mut language = Language::new(
2746 LanguageConfig {
2747 name: "Rust".into(),
2748 path_suffixes: vec!["rs".to_string()],
2749 ..Default::default()
2750 },
2751 Some(tree_sitter_rust::language()),
2752 );
2753 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
2754 client_a.language_registry.add(Arc::new(language));
2755
2756 client_a
2757 .fs
2758 .insert_tree(
2759 "/root",
2760 json!({
2761 "dir-1": {
2762 "a.rs": "const ONE: usize = b::TWO + b::THREE;",
2763 },
2764 "dir-2": {
2765 "b.rs": "const TWO: c::T2 = 2;\nconst THREE: usize = 3;",
2766 "c.rs": "type T2 = usize;",
2767 }
2768 }),
2769 )
2770 .await;
2771 let (project_a, worktree_id) = client_a.build_local_project("/root/dir-1", cx_a).await;
2772 let project_id = active_call_a
2773 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2774 .await
2775 .unwrap();
2776 let project_b = client_b.build_remote_project(project_id, cx_b).await;
2777
2778 // Open the file on client B.
2779 let buffer_b = cx_b
2780 .background()
2781 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
2782 .await
2783 .unwrap();
2784
2785 // Request the definition of a symbol as the guest.
2786 let fake_language_server = fake_language_servers.next().await.unwrap();
2787 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
2788 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
2789 lsp::Location::new(
2790 lsp::Url::from_file_path("/root/dir-2/b.rs").unwrap(),
2791 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
2792 ),
2793 )))
2794 });
2795
2796 let definitions_1 = project_b
2797 .update(cx_b, |p, cx| p.definition(&buffer_b, 23, cx))
2798 .await
2799 .unwrap();
2800 cx_b.read(|cx| {
2801 assert_eq!(definitions_1.len(), 1);
2802 assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
2803 let target_buffer = definitions_1[0].target.buffer.read(cx);
2804 assert_eq!(
2805 target_buffer.text(),
2806 "const TWO: c::T2 = 2;\nconst THREE: usize = 3;"
2807 );
2808 assert_eq!(
2809 definitions_1[0].target.range.to_point(target_buffer),
2810 Point::new(0, 6)..Point::new(0, 9)
2811 );
2812 });
2813
2814 // Try getting more definitions for the same buffer, ensuring the buffer gets reused from
2815 // the previous call to `definition`.
2816 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
2817 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
2818 lsp::Location::new(
2819 lsp::Url::from_file_path("/root/dir-2/b.rs").unwrap(),
2820 lsp::Range::new(lsp::Position::new(1, 6), lsp::Position::new(1, 11)),
2821 ),
2822 )))
2823 });
2824
2825 let definitions_2 = project_b
2826 .update(cx_b, |p, cx| p.definition(&buffer_b, 33, cx))
2827 .await
2828 .unwrap();
2829 cx_b.read(|cx| {
2830 assert_eq!(definitions_2.len(), 1);
2831 assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
2832 let target_buffer = definitions_2[0].target.buffer.read(cx);
2833 assert_eq!(
2834 target_buffer.text(),
2835 "const TWO: c::T2 = 2;\nconst THREE: usize = 3;"
2836 );
2837 assert_eq!(
2838 definitions_2[0].target.range.to_point(target_buffer),
2839 Point::new(1, 6)..Point::new(1, 11)
2840 );
2841 });
2842 assert_eq!(
2843 definitions_1[0].target.buffer,
2844 definitions_2[0].target.buffer
2845 );
2846
2847 fake_language_server.handle_request::<lsp::request::GotoTypeDefinition, _, _>(
2848 |req, _| async move {
2849 assert_eq!(
2850 req.text_document_position_params.position,
2851 lsp::Position::new(0, 7)
2852 );
2853 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
2854 lsp::Location::new(
2855 lsp::Url::from_file_path("/root/dir-2/c.rs").unwrap(),
2856 lsp::Range::new(lsp::Position::new(0, 5), lsp::Position::new(0, 7)),
2857 ),
2858 )))
2859 },
2860 );
2861
2862 let type_definitions = project_b
2863 .update(cx_b, |p, cx| p.type_definition(&buffer_b, 7, cx))
2864 .await
2865 .unwrap();
2866 cx_b.read(|cx| {
2867 assert_eq!(type_definitions.len(), 1);
2868 let target_buffer = type_definitions[0].target.buffer.read(cx);
2869 assert_eq!(target_buffer.text(), "type T2 = usize;");
2870 assert_eq!(
2871 type_definitions[0].target.range.to_point(target_buffer),
2872 Point::new(0, 5)..Point::new(0, 7)
2873 );
2874 });
2875}
2876
2877#[gpui::test(iterations = 10)]
2878async fn test_references(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2879 cx_a.foreground().forbid_parking();
2880 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
2881 let client_a = server.create_client(cx_a, "user_a").await;
2882 let client_b = server.create_client(cx_b, "user_b").await;
2883 server
2884 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2885 .await;
2886 let active_call_a = cx_a.read(ActiveCall::global);
2887
2888 // Set up a fake language server.
2889 let mut language = Language::new(
2890 LanguageConfig {
2891 name: "Rust".into(),
2892 path_suffixes: vec!["rs".to_string()],
2893 ..Default::default()
2894 },
2895 Some(tree_sitter_rust::language()),
2896 );
2897 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
2898 client_a.language_registry.add(Arc::new(language));
2899
2900 client_a
2901 .fs
2902 .insert_tree(
2903 "/root",
2904 json!({
2905 "dir-1": {
2906 "one.rs": "const ONE: usize = 1;",
2907 "two.rs": "const TWO: usize = one::ONE + one::ONE;",
2908 },
2909 "dir-2": {
2910 "three.rs": "const THREE: usize = two::TWO + one::ONE;",
2911 }
2912 }),
2913 )
2914 .await;
2915 let (project_a, worktree_id) = client_a.build_local_project("/root/dir-1", cx_a).await;
2916 let project_id = active_call_a
2917 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2918 .await
2919 .unwrap();
2920 let project_b = client_b.build_remote_project(project_id, cx_b).await;
2921
2922 // Open the file on client B.
2923 let buffer_b = cx_b
2924 .background()
2925 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx)))
2926 .await
2927 .unwrap();
2928
2929 // Request references to a symbol as the guest.
2930 let fake_language_server = fake_language_servers.next().await.unwrap();
2931 fake_language_server.handle_request::<lsp::request::References, _, _>(|params, _| async move {
2932 assert_eq!(
2933 params.text_document_position.text_document.uri.as_str(),
2934 "file:///root/dir-1/one.rs"
2935 );
2936 Ok(Some(vec![
2937 lsp::Location {
2938 uri: lsp::Url::from_file_path("/root/dir-1/two.rs").unwrap(),
2939 range: lsp::Range::new(lsp::Position::new(0, 24), lsp::Position::new(0, 27)),
2940 },
2941 lsp::Location {
2942 uri: lsp::Url::from_file_path("/root/dir-1/two.rs").unwrap(),
2943 range: lsp::Range::new(lsp::Position::new(0, 35), lsp::Position::new(0, 38)),
2944 },
2945 lsp::Location {
2946 uri: lsp::Url::from_file_path("/root/dir-2/three.rs").unwrap(),
2947 range: lsp::Range::new(lsp::Position::new(0, 37), lsp::Position::new(0, 40)),
2948 },
2949 ]))
2950 });
2951
2952 let references = project_b
2953 .update(cx_b, |p, cx| p.references(&buffer_b, 7, cx))
2954 .await
2955 .unwrap();
2956 cx_b.read(|cx| {
2957 assert_eq!(references.len(), 3);
2958 assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
2959
2960 let two_buffer = references[0].buffer.read(cx);
2961 let three_buffer = references[2].buffer.read(cx);
2962 assert_eq!(
2963 two_buffer.file().unwrap().path().as_ref(),
2964 Path::new("two.rs")
2965 );
2966 assert_eq!(references[1].buffer, references[0].buffer);
2967 assert_eq!(
2968 three_buffer.file().unwrap().full_path(cx),
2969 Path::new("three.rs")
2970 );
2971
2972 assert_eq!(references[0].range.to_offset(two_buffer), 24..27);
2973 assert_eq!(references[1].range.to_offset(two_buffer), 35..38);
2974 assert_eq!(references[2].range.to_offset(three_buffer), 37..40);
2975 });
2976}
2977
2978#[gpui::test(iterations = 10)]
2979async fn test_project_search(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2980 cx_a.foreground().forbid_parking();
2981 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
2982 let client_a = server.create_client(cx_a, "user_a").await;
2983 let client_b = server.create_client(cx_b, "user_b").await;
2984 server
2985 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2986 .await;
2987 let active_call_a = cx_a.read(ActiveCall::global);
2988
2989 client_a
2990 .fs
2991 .insert_tree(
2992 "/root",
2993 json!({
2994 "dir-1": {
2995 "a": "hello world",
2996 "b": "goodnight moon",
2997 "c": "a world of goo",
2998 "d": "world champion of clown world",
2999 },
3000 "dir-2": {
3001 "e": "disney world is fun",
3002 }
3003 }),
3004 )
3005 .await;
3006 let (project_a, _) = client_a.build_local_project("/root/dir-1", cx_a).await;
3007 let (worktree_2, _) = project_a
3008 .update(cx_a, |p, cx| {
3009 p.find_or_create_local_worktree("/root/dir-2", true, cx)
3010 })
3011 .await
3012 .unwrap();
3013 worktree_2
3014 .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
3015 .await;
3016 let project_id = active_call_a
3017 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3018 .await
3019 .unwrap();
3020
3021 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3022
3023 // Perform a search as the guest.
3024 let results = project_b
3025 .update(cx_b, |project, cx| {
3026 project.search(SearchQuery::text("world", false, false), cx)
3027 })
3028 .await
3029 .unwrap();
3030
3031 let mut ranges_by_path = results
3032 .into_iter()
3033 .map(|(buffer, ranges)| {
3034 buffer.read_with(cx_b, |buffer, cx| {
3035 let path = buffer.file().unwrap().full_path(cx);
3036 let offset_ranges = ranges
3037 .into_iter()
3038 .map(|range| range.to_offset(buffer))
3039 .collect::<Vec<_>>();
3040 (path, offset_ranges)
3041 })
3042 })
3043 .collect::<Vec<_>>();
3044 ranges_by_path.sort_by_key(|(path, _)| path.clone());
3045
3046 assert_eq!(
3047 ranges_by_path,
3048 &[
3049 (PathBuf::from("dir-1/a"), vec![6..11]),
3050 (PathBuf::from("dir-1/c"), vec![2..7]),
3051 (PathBuf::from("dir-1/d"), vec![0..5, 24..29]),
3052 (PathBuf::from("dir-2/e"), vec![7..12]),
3053 ]
3054 );
3055}
3056
3057#[gpui::test(iterations = 10)]
3058async fn test_document_highlights(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
3059 cx_a.foreground().forbid_parking();
3060 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
3061 let client_a = server.create_client(cx_a, "user_a").await;
3062 let client_b = server.create_client(cx_b, "user_b").await;
3063 server
3064 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3065 .await;
3066 let active_call_a = cx_a.read(ActiveCall::global);
3067
3068 client_a
3069 .fs
3070 .insert_tree(
3071 "/root-1",
3072 json!({
3073 "main.rs": "fn double(number: i32) -> i32 { number + number }",
3074 }),
3075 )
3076 .await;
3077
3078 // Set up a fake language server.
3079 let mut language = Language::new(
3080 LanguageConfig {
3081 name: "Rust".into(),
3082 path_suffixes: vec!["rs".to_string()],
3083 ..Default::default()
3084 },
3085 Some(tree_sitter_rust::language()),
3086 );
3087 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
3088 client_a.language_registry.add(Arc::new(language));
3089
3090 let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
3091 let project_id = active_call_a
3092 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3093 .await
3094 .unwrap();
3095 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3096
3097 // Open the file on client B.
3098 let buffer_b = cx_b
3099 .background()
3100 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)))
3101 .await
3102 .unwrap();
3103
3104 // Request document highlights as the guest.
3105 let fake_language_server = fake_language_servers.next().await.unwrap();
3106 fake_language_server.handle_request::<lsp::request::DocumentHighlightRequest, _, _>(
3107 |params, _| async move {
3108 assert_eq!(
3109 params
3110 .text_document_position_params
3111 .text_document
3112 .uri
3113 .as_str(),
3114 "file:///root-1/main.rs"
3115 );
3116 assert_eq!(
3117 params.text_document_position_params.position,
3118 lsp::Position::new(0, 34)
3119 );
3120 Ok(Some(vec![
3121 lsp::DocumentHighlight {
3122 kind: Some(lsp::DocumentHighlightKind::WRITE),
3123 range: lsp::Range::new(lsp::Position::new(0, 10), lsp::Position::new(0, 16)),
3124 },
3125 lsp::DocumentHighlight {
3126 kind: Some(lsp::DocumentHighlightKind::READ),
3127 range: lsp::Range::new(lsp::Position::new(0, 32), lsp::Position::new(0, 38)),
3128 },
3129 lsp::DocumentHighlight {
3130 kind: Some(lsp::DocumentHighlightKind::READ),
3131 range: lsp::Range::new(lsp::Position::new(0, 41), lsp::Position::new(0, 47)),
3132 },
3133 ]))
3134 },
3135 );
3136
3137 let highlights = project_b
3138 .update(cx_b, |p, cx| p.document_highlights(&buffer_b, 34, cx))
3139 .await
3140 .unwrap();
3141 buffer_b.read_with(cx_b, |buffer, _| {
3142 let snapshot = buffer.snapshot();
3143
3144 let highlights = highlights
3145 .into_iter()
3146 .map(|highlight| (highlight.kind, highlight.range.to_offset(&snapshot)))
3147 .collect::<Vec<_>>();
3148 assert_eq!(
3149 highlights,
3150 &[
3151 (lsp::DocumentHighlightKind::WRITE, 10..16),
3152 (lsp::DocumentHighlightKind::READ, 32..38),
3153 (lsp::DocumentHighlightKind::READ, 41..47)
3154 ]
3155 )
3156 });
3157}
3158
3159#[gpui::test(iterations = 10)]
3160async fn test_lsp_hover(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
3161 cx_a.foreground().forbid_parking();
3162 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
3163 let client_a = server.create_client(cx_a, "user_a").await;
3164 let client_b = server.create_client(cx_b, "user_b").await;
3165 server
3166 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3167 .await;
3168 let active_call_a = cx_a.read(ActiveCall::global);
3169
3170 client_a
3171 .fs
3172 .insert_tree(
3173 "/root-1",
3174 json!({
3175 "main.rs": "use std::collections::HashMap;",
3176 }),
3177 )
3178 .await;
3179
3180 // Set up a fake language server.
3181 let mut language = Language::new(
3182 LanguageConfig {
3183 name: "Rust".into(),
3184 path_suffixes: vec!["rs".to_string()],
3185 ..Default::default()
3186 },
3187 Some(tree_sitter_rust::language()),
3188 );
3189 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
3190 client_a.language_registry.add(Arc::new(language));
3191
3192 let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
3193 let project_id = active_call_a
3194 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3195 .await
3196 .unwrap();
3197 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3198
3199 // Open the file as the guest
3200 let buffer_b = cx_b
3201 .background()
3202 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)))
3203 .await
3204 .unwrap();
3205
3206 // Request hover information as the guest.
3207 let fake_language_server = fake_language_servers.next().await.unwrap();
3208 fake_language_server.handle_request::<lsp::request::HoverRequest, _, _>(
3209 |params, _| async move {
3210 assert_eq!(
3211 params
3212 .text_document_position_params
3213 .text_document
3214 .uri
3215 .as_str(),
3216 "file:///root-1/main.rs"
3217 );
3218 assert_eq!(
3219 params.text_document_position_params.position,
3220 lsp::Position::new(0, 22)
3221 );
3222 Ok(Some(lsp::Hover {
3223 contents: lsp::HoverContents::Array(vec![
3224 lsp::MarkedString::String("Test hover content.".to_string()),
3225 lsp::MarkedString::LanguageString(lsp::LanguageString {
3226 language: "Rust".to_string(),
3227 value: "let foo = 42;".to_string(),
3228 }),
3229 ]),
3230 range: Some(lsp::Range::new(
3231 lsp::Position::new(0, 22),
3232 lsp::Position::new(0, 29),
3233 )),
3234 }))
3235 },
3236 );
3237
3238 let hover_info = project_b
3239 .update(cx_b, |p, cx| p.hover(&buffer_b, 22, cx))
3240 .await
3241 .unwrap()
3242 .unwrap();
3243 buffer_b.read_with(cx_b, |buffer, _| {
3244 let snapshot = buffer.snapshot();
3245 assert_eq!(hover_info.range.unwrap().to_offset(&snapshot), 22..29);
3246 assert_eq!(
3247 hover_info.contents,
3248 vec![
3249 project::HoverBlock {
3250 text: "Test hover content.".to_string(),
3251 language: None,
3252 },
3253 project::HoverBlock {
3254 text: "let foo = 42;".to_string(),
3255 language: Some("Rust".to_string()),
3256 }
3257 ]
3258 );
3259 });
3260}
3261
3262#[gpui::test(iterations = 10)]
3263async fn test_project_symbols(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
3264 cx_a.foreground().forbid_parking();
3265 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
3266 let client_a = server.create_client(cx_a, "user_a").await;
3267 let client_b = server.create_client(cx_b, "user_b").await;
3268 server
3269 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3270 .await;
3271 let active_call_a = cx_a.read(ActiveCall::global);
3272
3273 // Set up a fake language server.
3274 let mut language = Language::new(
3275 LanguageConfig {
3276 name: "Rust".into(),
3277 path_suffixes: vec!["rs".to_string()],
3278 ..Default::default()
3279 },
3280 Some(tree_sitter_rust::language()),
3281 );
3282 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
3283 client_a.language_registry.add(Arc::new(language));
3284
3285 client_a
3286 .fs
3287 .insert_tree(
3288 "/code",
3289 json!({
3290 "crate-1": {
3291 "one.rs": "const ONE: usize = 1;",
3292 },
3293 "crate-2": {
3294 "two.rs": "const TWO: usize = 2; const THREE: usize = 3;",
3295 },
3296 "private": {
3297 "passwords.txt": "the-password",
3298 }
3299 }),
3300 )
3301 .await;
3302 let (project_a, worktree_id) = client_a.build_local_project("/code/crate-1", cx_a).await;
3303 let project_id = active_call_a
3304 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3305 .await
3306 .unwrap();
3307 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3308
3309 // Cause the language server to start.
3310 let _buffer = cx_b
3311 .background()
3312 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx)))
3313 .await
3314 .unwrap();
3315
3316 let fake_language_server = fake_language_servers.next().await.unwrap();
3317 fake_language_server.handle_request::<lsp::request::WorkspaceSymbol, _, _>(|_, _| async move {
3318 #[allow(deprecated)]
3319 Ok(Some(vec![lsp::SymbolInformation {
3320 name: "TWO".into(),
3321 location: lsp::Location {
3322 uri: lsp::Url::from_file_path("/code/crate-2/two.rs").unwrap(),
3323 range: lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
3324 },
3325 kind: lsp::SymbolKind::CONSTANT,
3326 tags: None,
3327 container_name: None,
3328 deprecated: None,
3329 }]))
3330 });
3331
3332 // Request the definition of a symbol as the guest.
3333 let symbols = project_b
3334 .update(cx_b, |p, cx| p.symbols("two", cx))
3335 .await
3336 .unwrap();
3337 assert_eq!(symbols.len(), 1);
3338 assert_eq!(symbols[0].name, "TWO");
3339
3340 // Open one of the returned symbols.
3341 let buffer_b_2 = project_b
3342 .update(cx_b, |project, cx| {
3343 project.open_buffer_for_symbol(&symbols[0], cx)
3344 })
3345 .await
3346 .unwrap();
3347 buffer_b_2.read_with(cx_b, |buffer, _| {
3348 assert_eq!(
3349 buffer.file().unwrap().path().as_ref(),
3350 Path::new("../crate-2/two.rs")
3351 );
3352 });
3353
3354 // Attempt to craft a symbol and violate host's privacy by opening an arbitrary file.
3355 let mut fake_symbol = symbols[0].clone();
3356 fake_symbol.path.path = Path::new("/code/secrets").into();
3357 let error = project_b
3358 .update(cx_b, |project, cx| {
3359 project.open_buffer_for_symbol(&fake_symbol, cx)
3360 })
3361 .await
3362 .unwrap_err();
3363 assert!(error.to_string().contains("invalid symbol signature"));
3364}
3365
3366#[gpui::test(iterations = 10)]
3367async fn test_open_buffer_while_getting_definition_pointing_to_it(
3368 cx_a: &mut TestAppContext,
3369 cx_b: &mut TestAppContext,
3370 mut rng: StdRng,
3371) {
3372 cx_a.foreground().forbid_parking();
3373 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
3374 let client_a = server.create_client(cx_a, "user_a").await;
3375 let client_b = server.create_client(cx_b, "user_b").await;
3376 server
3377 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3378 .await;
3379 let active_call_a = cx_a.read(ActiveCall::global);
3380
3381 // Set up a fake language server.
3382 let mut language = Language::new(
3383 LanguageConfig {
3384 name: "Rust".into(),
3385 path_suffixes: vec!["rs".to_string()],
3386 ..Default::default()
3387 },
3388 Some(tree_sitter_rust::language()),
3389 );
3390 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
3391 client_a.language_registry.add(Arc::new(language));
3392
3393 client_a
3394 .fs
3395 .insert_tree(
3396 "/root",
3397 json!({
3398 "a.rs": "const ONE: usize = b::TWO;",
3399 "b.rs": "const TWO: usize = 2",
3400 }),
3401 )
3402 .await;
3403 let (project_a, worktree_id) = client_a.build_local_project("/root", cx_a).await;
3404 let project_id = active_call_a
3405 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3406 .await
3407 .unwrap();
3408 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3409
3410 let buffer_b1 = cx_b
3411 .background()
3412 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
3413 .await
3414 .unwrap();
3415
3416 let fake_language_server = fake_language_servers.next().await.unwrap();
3417 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
3418 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
3419 lsp::Location::new(
3420 lsp::Url::from_file_path("/root/b.rs").unwrap(),
3421 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
3422 ),
3423 )))
3424 });
3425
3426 let definitions;
3427 let buffer_b2;
3428 if rng.gen() {
3429 definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
3430 buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx));
3431 } else {
3432 buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx));
3433 definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
3434 }
3435
3436 let buffer_b2 = buffer_b2.await.unwrap();
3437 let definitions = definitions.await.unwrap();
3438 assert_eq!(definitions.len(), 1);
3439 assert_eq!(definitions[0].target.buffer, buffer_b2);
3440}
3441
3442#[gpui::test(iterations = 10)]
3443async fn test_collaborating_with_code_actions(
3444 cx_a: &mut TestAppContext,
3445 cx_b: &mut TestAppContext,
3446) {
3447 cx_a.foreground().forbid_parking();
3448 cx_b.update(editor::init);
3449 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
3450 let client_a = server.create_client(cx_a, "user_a").await;
3451 let client_b = server.create_client(cx_b, "user_b").await;
3452 server
3453 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3454 .await;
3455 let active_call_a = cx_a.read(ActiveCall::global);
3456
3457 // Set up a fake language server.
3458 let mut language = Language::new(
3459 LanguageConfig {
3460 name: "Rust".into(),
3461 path_suffixes: vec!["rs".to_string()],
3462 ..Default::default()
3463 },
3464 Some(tree_sitter_rust::language()),
3465 );
3466 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
3467 client_a.language_registry.add(Arc::new(language));
3468
3469 client_a
3470 .fs
3471 .insert_tree(
3472 "/a",
3473 json!({
3474 "main.rs": "mod other;\nfn main() { let foo = other::foo(); }",
3475 "other.rs": "pub fn foo() -> usize { 4 }",
3476 }),
3477 )
3478 .await;
3479 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
3480 let project_id = active_call_a
3481 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3482 .await
3483 .unwrap();
3484
3485 // Join the project as client B.
3486 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3487 let (_window_b, workspace_b) =
3488 cx_b.add_window(|cx| Workspace::new(project_b.clone(), |_, _| unimplemented!(), cx));
3489 let editor_b = workspace_b
3490 .update(cx_b, |workspace, cx| {
3491 workspace.open_path((worktree_id, "main.rs"), true, cx)
3492 })
3493 .await
3494 .unwrap()
3495 .downcast::<Editor>()
3496 .unwrap();
3497
3498 let mut fake_language_server = fake_language_servers.next().await.unwrap();
3499 fake_language_server
3500 .handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
3501 assert_eq!(
3502 params.text_document.uri,
3503 lsp::Url::from_file_path("/a/main.rs").unwrap(),
3504 );
3505 assert_eq!(params.range.start, lsp::Position::new(0, 0));
3506 assert_eq!(params.range.end, lsp::Position::new(0, 0));
3507 Ok(None)
3508 })
3509 .next()
3510 .await;
3511
3512 // Move cursor to a location that contains code actions.
3513 editor_b.update(cx_b, |editor, cx| {
3514 editor.change_selections(None, cx, |s| {
3515 s.select_ranges([Point::new(1, 31)..Point::new(1, 31)])
3516 });
3517 cx.focus(&editor_b);
3518 });
3519
3520 fake_language_server
3521 .handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
3522 assert_eq!(
3523 params.text_document.uri,
3524 lsp::Url::from_file_path("/a/main.rs").unwrap(),
3525 );
3526 assert_eq!(params.range.start, lsp::Position::new(1, 31));
3527 assert_eq!(params.range.end, lsp::Position::new(1, 31));
3528
3529 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
3530 lsp::CodeAction {
3531 title: "Inline into all callers".to_string(),
3532 edit: Some(lsp::WorkspaceEdit {
3533 changes: Some(
3534 [
3535 (
3536 lsp::Url::from_file_path("/a/main.rs").unwrap(),
3537 vec![lsp::TextEdit::new(
3538 lsp::Range::new(
3539 lsp::Position::new(1, 22),
3540 lsp::Position::new(1, 34),
3541 ),
3542 "4".to_string(),
3543 )],
3544 ),
3545 (
3546 lsp::Url::from_file_path("/a/other.rs").unwrap(),
3547 vec![lsp::TextEdit::new(
3548 lsp::Range::new(
3549 lsp::Position::new(0, 0),
3550 lsp::Position::new(0, 27),
3551 ),
3552 "".to_string(),
3553 )],
3554 ),
3555 ]
3556 .into_iter()
3557 .collect(),
3558 ),
3559 ..Default::default()
3560 }),
3561 data: Some(json!({
3562 "codeActionParams": {
3563 "range": {
3564 "start": {"line": 1, "column": 31},
3565 "end": {"line": 1, "column": 31},
3566 }
3567 }
3568 })),
3569 ..Default::default()
3570 },
3571 )]))
3572 })
3573 .next()
3574 .await;
3575
3576 // Toggle code actions and wait for them to display.
3577 editor_b.update(cx_b, |editor, cx| {
3578 editor.toggle_code_actions(
3579 &ToggleCodeActions {
3580 deployed_from_indicator: false,
3581 },
3582 cx,
3583 );
3584 });
3585 editor_b
3586 .condition(cx_b, |editor, _| editor.context_menu_visible())
3587 .await;
3588
3589 fake_language_server.remove_request_handler::<lsp::request::CodeActionRequest>();
3590
3591 // Confirming the code action will trigger a resolve request.
3592 let confirm_action = workspace_b
3593 .update(cx_b, |workspace, cx| {
3594 Editor::confirm_code_action(workspace, &ConfirmCodeAction { item_ix: Some(0) }, cx)
3595 })
3596 .unwrap();
3597 fake_language_server.handle_request::<lsp::request::CodeActionResolveRequest, _, _>(
3598 |_, _| async move {
3599 Ok(lsp::CodeAction {
3600 title: "Inline into all callers".to_string(),
3601 edit: Some(lsp::WorkspaceEdit {
3602 changes: Some(
3603 [
3604 (
3605 lsp::Url::from_file_path("/a/main.rs").unwrap(),
3606 vec![lsp::TextEdit::new(
3607 lsp::Range::new(
3608 lsp::Position::new(1, 22),
3609 lsp::Position::new(1, 34),
3610 ),
3611 "4".to_string(),
3612 )],
3613 ),
3614 (
3615 lsp::Url::from_file_path("/a/other.rs").unwrap(),
3616 vec![lsp::TextEdit::new(
3617 lsp::Range::new(
3618 lsp::Position::new(0, 0),
3619 lsp::Position::new(0, 27),
3620 ),
3621 "".to_string(),
3622 )],
3623 ),
3624 ]
3625 .into_iter()
3626 .collect(),
3627 ),
3628 ..Default::default()
3629 }),
3630 ..Default::default()
3631 })
3632 },
3633 );
3634
3635 // After the action is confirmed, an editor containing both modified files is opened.
3636 confirm_action.await.unwrap();
3637 let code_action_editor = workspace_b.read_with(cx_b, |workspace, cx| {
3638 workspace
3639 .active_item(cx)
3640 .unwrap()
3641 .downcast::<Editor>()
3642 .unwrap()
3643 });
3644 code_action_editor.update(cx_b, |editor, cx| {
3645 assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
3646 editor.undo(&Undo, cx);
3647 assert_eq!(
3648 editor.text(cx),
3649 "mod other;\nfn main() { let foo = other::foo(); }\npub fn foo() -> usize { 4 }"
3650 );
3651 editor.redo(&Redo, cx);
3652 assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
3653 });
3654}
3655
3656#[gpui::test(iterations = 10)]
3657async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
3658 cx_a.foreground().forbid_parking();
3659 cx_b.update(editor::init);
3660 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
3661 let client_a = server.create_client(cx_a, "user_a").await;
3662 let client_b = server.create_client(cx_b, "user_b").await;
3663 server
3664 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3665 .await;
3666 let active_call_a = cx_a.read(ActiveCall::global);
3667
3668 // Set up a fake language server.
3669 let mut language = Language::new(
3670 LanguageConfig {
3671 name: "Rust".into(),
3672 path_suffixes: vec!["rs".to_string()],
3673 ..Default::default()
3674 },
3675 Some(tree_sitter_rust::language()),
3676 );
3677 let mut fake_language_servers = language
3678 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
3679 capabilities: lsp::ServerCapabilities {
3680 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
3681 prepare_provider: Some(true),
3682 work_done_progress_options: Default::default(),
3683 })),
3684 ..Default::default()
3685 },
3686 ..Default::default()
3687 }))
3688 .await;
3689 client_a.language_registry.add(Arc::new(language));
3690
3691 client_a
3692 .fs
3693 .insert_tree(
3694 "/dir",
3695 json!({
3696 "one.rs": "const ONE: usize = 1;",
3697 "two.rs": "const TWO: usize = one::ONE + one::ONE;"
3698 }),
3699 )
3700 .await;
3701 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3702 let project_id = active_call_a
3703 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3704 .await
3705 .unwrap();
3706 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3707
3708 let (_window_b, workspace_b) =
3709 cx_b.add_window(|cx| Workspace::new(project_b.clone(), |_, _| unimplemented!(), cx));
3710 let editor_b = workspace_b
3711 .update(cx_b, |workspace, cx| {
3712 workspace.open_path((worktree_id, "one.rs"), true, cx)
3713 })
3714 .await
3715 .unwrap()
3716 .downcast::<Editor>()
3717 .unwrap();
3718 let fake_language_server = fake_language_servers.next().await.unwrap();
3719
3720 // Move cursor to a location that can be renamed.
3721 let prepare_rename = editor_b.update(cx_b, |editor, cx| {
3722 editor.change_selections(None, cx, |s| s.select_ranges([7..7]));
3723 editor.rename(&Rename, cx).unwrap()
3724 });
3725
3726 fake_language_server
3727 .handle_request::<lsp::request::PrepareRenameRequest, _, _>(|params, _| async move {
3728 assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs");
3729 assert_eq!(params.position, lsp::Position::new(0, 7));
3730 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
3731 lsp::Position::new(0, 6),
3732 lsp::Position::new(0, 9),
3733 ))))
3734 })
3735 .next()
3736 .await
3737 .unwrap();
3738 prepare_rename.await.unwrap();
3739 editor_b.update(cx_b, |editor, cx| {
3740 let rename = editor.pending_rename().unwrap();
3741 let buffer = editor.buffer().read(cx).snapshot(cx);
3742 assert_eq!(
3743 rename.range.start.to_offset(&buffer)..rename.range.end.to_offset(&buffer),
3744 6..9
3745 );
3746 rename.editor.update(cx, |rename_editor, cx| {
3747 rename_editor.buffer().update(cx, |rename_buffer, cx| {
3748 rename_buffer.edit([(0..3, "THREE")], None, cx);
3749 });
3750 });
3751 });
3752
3753 let confirm_rename = workspace_b.update(cx_b, |workspace, cx| {
3754 Editor::confirm_rename(workspace, &ConfirmRename, cx).unwrap()
3755 });
3756 fake_language_server
3757 .handle_request::<lsp::request::Rename, _, _>(|params, _| async move {
3758 assert_eq!(
3759 params.text_document_position.text_document.uri.as_str(),
3760 "file:///dir/one.rs"
3761 );
3762 assert_eq!(
3763 params.text_document_position.position,
3764 lsp::Position::new(0, 6)
3765 );
3766 assert_eq!(params.new_name, "THREE");
3767 Ok(Some(lsp::WorkspaceEdit {
3768 changes: Some(
3769 [
3770 (
3771 lsp::Url::from_file_path("/dir/one.rs").unwrap(),
3772 vec![lsp::TextEdit::new(
3773 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
3774 "THREE".to_string(),
3775 )],
3776 ),
3777 (
3778 lsp::Url::from_file_path("/dir/two.rs").unwrap(),
3779 vec![
3780 lsp::TextEdit::new(
3781 lsp::Range::new(
3782 lsp::Position::new(0, 24),
3783 lsp::Position::new(0, 27),
3784 ),
3785 "THREE".to_string(),
3786 ),
3787 lsp::TextEdit::new(
3788 lsp::Range::new(
3789 lsp::Position::new(0, 35),
3790 lsp::Position::new(0, 38),
3791 ),
3792 "THREE".to_string(),
3793 ),
3794 ],
3795 ),
3796 ]
3797 .into_iter()
3798 .collect(),
3799 ),
3800 ..Default::default()
3801 }))
3802 })
3803 .next()
3804 .await
3805 .unwrap();
3806 confirm_rename.await.unwrap();
3807
3808 let rename_editor = workspace_b.read_with(cx_b, |workspace, cx| {
3809 workspace
3810 .active_item(cx)
3811 .unwrap()
3812 .downcast::<Editor>()
3813 .unwrap()
3814 });
3815 rename_editor.update(cx_b, |editor, cx| {
3816 assert_eq!(
3817 editor.text(cx),
3818 "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
3819 );
3820 editor.undo(&Undo, cx);
3821 assert_eq!(
3822 editor.text(cx),
3823 "const ONE: usize = 1;\nconst TWO: usize = one::ONE + one::ONE;"
3824 );
3825 editor.redo(&Redo, cx);
3826 assert_eq!(
3827 editor.text(cx),
3828 "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
3829 );
3830 });
3831
3832 // Ensure temporary rename edits cannot be undone/redone.
3833 editor_b.update(cx_b, |editor, cx| {
3834 editor.undo(&Undo, cx);
3835 assert_eq!(editor.text(cx), "const ONE: usize = 1;");
3836 editor.undo(&Undo, cx);
3837 assert_eq!(editor.text(cx), "const ONE: usize = 1;");
3838 editor.redo(&Redo, cx);
3839 assert_eq!(editor.text(cx), "const THREE: usize = 1;");
3840 })
3841}
3842
3843#[gpui::test(iterations = 10)]
3844async fn test_language_server_statuses(
3845 deterministic: Arc<Deterministic>,
3846 cx_a: &mut TestAppContext,
3847 cx_b: &mut TestAppContext,
3848) {
3849 deterministic.forbid_parking();
3850
3851 cx_b.update(editor::init);
3852 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
3853 let client_a = server.create_client(cx_a, "user_a").await;
3854 let client_b = server.create_client(cx_b, "user_b").await;
3855 server
3856 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3857 .await;
3858 let active_call_a = cx_a.read(ActiveCall::global);
3859
3860 // Set up a fake language server.
3861 let mut language = Language::new(
3862 LanguageConfig {
3863 name: "Rust".into(),
3864 path_suffixes: vec!["rs".to_string()],
3865 ..Default::default()
3866 },
3867 Some(tree_sitter_rust::language()),
3868 );
3869 let mut fake_language_servers = language
3870 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
3871 name: "the-language-server",
3872 ..Default::default()
3873 }))
3874 .await;
3875 client_a.language_registry.add(Arc::new(language));
3876
3877 client_a
3878 .fs
3879 .insert_tree(
3880 "/dir",
3881 json!({
3882 "main.rs": "const ONE: usize = 1;",
3883 }),
3884 )
3885 .await;
3886 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
3887
3888 let _buffer_a = project_a
3889 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
3890 .await
3891 .unwrap();
3892
3893 let fake_language_server = fake_language_servers.next().await.unwrap();
3894 fake_language_server.start_progress("the-token").await;
3895 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
3896 token: lsp::NumberOrString::String("the-token".to_string()),
3897 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
3898 lsp::WorkDoneProgressReport {
3899 message: Some("the-message".to_string()),
3900 ..Default::default()
3901 },
3902 )),
3903 });
3904 deterministic.run_until_parked();
3905 project_a.read_with(cx_a, |project, _| {
3906 let status = project.language_server_statuses().next().unwrap();
3907 assert_eq!(status.name, "the-language-server");
3908 assert_eq!(status.pending_work.len(), 1);
3909 assert_eq!(
3910 status.pending_work["the-token"].message.as_ref().unwrap(),
3911 "the-message"
3912 );
3913 });
3914
3915 let project_id = active_call_a
3916 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3917 .await
3918 .unwrap();
3919 deterministic.run_until_parked();
3920 let project_b = client_b.build_remote_project(project_id, cx_b).await;
3921 project_b.read_with(cx_b, |project, _| {
3922 let status = project.language_server_statuses().next().unwrap();
3923 assert_eq!(status.name, "the-language-server");
3924 });
3925
3926 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
3927 token: lsp::NumberOrString::String("the-token".to_string()),
3928 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
3929 lsp::WorkDoneProgressReport {
3930 message: Some("the-message-2".to_string()),
3931 ..Default::default()
3932 },
3933 )),
3934 });
3935 deterministic.run_until_parked();
3936 project_a.read_with(cx_a, |project, _| {
3937 let status = project.language_server_statuses().next().unwrap();
3938 assert_eq!(status.name, "the-language-server");
3939 assert_eq!(status.pending_work.len(), 1);
3940 assert_eq!(
3941 status.pending_work["the-token"].message.as_ref().unwrap(),
3942 "the-message-2"
3943 );
3944 });
3945 project_b.read_with(cx_b, |project, _| {
3946 let status = project.language_server_statuses().next().unwrap();
3947 assert_eq!(status.name, "the-language-server");
3948 assert_eq!(status.pending_work.len(), 1);
3949 assert_eq!(
3950 status.pending_work["the-token"].message.as_ref().unwrap(),
3951 "the-message-2"
3952 );
3953 });
3954}
3955
3956#[gpui::test(iterations = 10)]
3957async fn test_basic_chat(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
3958 cx_a.foreground().forbid_parking();
3959 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
3960 let client_a = server.create_client(cx_a, "user_a").await;
3961 let client_b = server.create_client(cx_b, "user_b").await;
3962
3963 // Create an org that includes these 2 users.
3964 let db = &server.app_state.db;
3965 let org_id = db.create_org("Test Org", "test-org").await.unwrap();
3966 db.add_org_member(org_id, client_a.current_user_id(cx_a), false)
3967 .await
3968 .unwrap();
3969 db.add_org_member(org_id, client_b.current_user_id(cx_b), false)
3970 .await
3971 .unwrap();
3972
3973 // Create a channel that includes all the users.
3974 let channel_id = db.create_org_channel(org_id, "test-channel").await.unwrap();
3975 db.add_channel_member(channel_id, client_a.current_user_id(cx_a), false)
3976 .await
3977 .unwrap();
3978 db.add_channel_member(channel_id, client_b.current_user_id(cx_b), false)
3979 .await
3980 .unwrap();
3981 db.create_channel_message(
3982 channel_id,
3983 client_b.current_user_id(cx_b),
3984 "hello A, it's B.",
3985 OffsetDateTime::now_utc(),
3986 1,
3987 )
3988 .await
3989 .unwrap();
3990
3991 let channels_a =
3992 cx_a.add_model(|cx| ChannelList::new(client_a.user_store.clone(), client_a.clone(), cx));
3993 channels_a
3994 .condition(cx_a, |list, _| list.available_channels().is_some())
3995 .await;
3996 channels_a.read_with(cx_a, |list, _| {
3997 assert_eq!(
3998 list.available_channels().unwrap(),
3999 &[ChannelDetails {
4000 id: channel_id.to_proto(),
4001 name: "test-channel".to_string()
4002 }]
4003 )
4004 });
4005 let channel_a = channels_a.update(cx_a, |this, cx| {
4006 this.get_channel(channel_id.to_proto(), cx).unwrap()
4007 });
4008 channel_a.read_with(cx_a, |channel, _| assert!(channel.messages().is_empty()));
4009 channel_a
4010 .condition(cx_a, |channel, _| {
4011 channel_messages(channel)
4012 == [("user_b".to_string(), "hello A, it's B.".to_string(), false)]
4013 })
4014 .await;
4015
4016 let channels_b =
4017 cx_b.add_model(|cx| ChannelList::new(client_b.user_store.clone(), client_b.clone(), cx));
4018 channels_b
4019 .condition(cx_b, |list, _| list.available_channels().is_some())
4020 .await;
4021 channels_b.read_with(cx_b, |list, _| {
4022 assert_eq!(
4023 list.available_channels().unwrap(),
4024 &[ChannelDetails {
4025 id: channel_id.to_proto(),
4026 name: "test-channel".to_string()
4027 }]
4028 )
4029 });
4030
4031 let channel_b = channels_b.update(cx_b, |this, cx| {
4032 this.get_channel(channel_id.to_proto(), cx).unwrap()
4033 });
4034 channel_b.read_with(cx_b, |channel, _| assert!(channel.messages().is_empty()));
4035 channel_b
4036 .condition(cx_b, |channel, _| {
4037 channel_messages(channel)
4038 == [("user_b".to_string(), "hello A, it's B.".to_string(), false)]
4039 })
4040 .await;
4041
4042 channel_a
4043 .update(cx_a, |channel, cx| {
4044 channel
4045 .send_message("oh, hi B.".to_string(), cx)
4046 .unwrap()
4047 .detach();
4048 let task = channel.send_message("sup".to_string(), cx).unwrap();
4049 assert_eq!(
4050 channel_messages(channel),
4051 &[
4052 ("user_b".to_string(), "hello A, it's B.".to_string(), false),
4053 ("user_a".to_string(), "oh, hi B.".to_string(), true),
4054 ("user_a".to_string(), "sup".to_string(), true)
4055 ]
4056 );
4057 task
4058 })
4059 .await
4060 .unwrap();
4061
4062 channel_b
4063 .condition(cx_b, |channel, _| {
4064 channel_messages(channel)
4065 == [
4066 ("user_b".to_string(), "hello A, it's B.".to_string(), false),
4067 ("user_a".to_string(), "oh, hi B.".to_string(), false),
4068 ("user_a".to_string(), "sup".to_string(), false),
4069 ]
4070 })
4071 .await;
4072
4073 assert_eq!(
4074 server
4075 .store()
4076 .await
4077 .channel(channel_id)
4078 .unwrap()
4079 .connection_ids
4080 .len(),
4081 2
4082 );
4083 cx_b.update(|_| drop(channel_b));
4084 server
4085 .condition(|state| state.channel(channel_id).unwrap().connection_ids.len() == 1)
4086 .await;
4087
4088 cx_a.update(|_| drop(channel_a));
4089 server
4090 .condition(|state| state.channel(channel_id).is_none())
4091 .await;
4092}
4093
4094#[gpui::test(iterations = 10)]
4095async fn test_chat_message_validation(cx_a: &mut TestAppContext) {
4096 cx_a.foreground().forbid_parking();
4097 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
4098 let client_a = server.create_client(cx_a, "user_a").await;
4099
4100 let db = &server.app_state.db;
4101 let org_id = db.create_org("Test Org", "test-org").await.unwrap();
4102 let channel_id = db.create_org_channel(org_id, "test-channel").await.unwrap();
4103 db.add_org_member(org_id, client_a.current_user_id(cx_a), false)
4104 .await
4105 .unwrap();
4106 db.add_channel_member(channel_id, client_a.current_user_id(cx_a), false)
4107 .await
4108 .unwrap();
4109
4110 let channels_a =
4111 cx_a.add_model(|cx| ChannelList::new(client_a.user_store.clone(), client_a.clone(), cx));
4112 channels_a
4113 .condition(cx_a, |list, _| list.available_channels().is_some())
4114 .await;
4115 let channel_a = channels_a.update(cx_a, |this, cx| {
4116 this.get_channel(channel_id.to_proto(), cx).unwrap()
4117 });
4118
4119 // Messages aren't allowed to be too long.
4120 channel_a
4121 .update(cx_a, |channel, cx| {
4122 let long_body = "this is long.\n".repeat(1024);
4123 channel.send_message(long_body, cx).unwrap()
4124 })
4125 .await
4126 .unwrap_err();
4127
4128 // Messages aren't allowed to be blank.
4129 channel_a.update(cx_a, |channel, cx| {
4130 channel.send_message(String::new(), cx).unwrap_err()
4131 });
4132
4133 // Leading and trailing whitespace are trimmed.
4134 channel_a
4135 .update(cx_a, |channel, cx| {
4136 channel
4137 .send_message("\n surrounded by whitespace \n".to_string(), cx)
4138 .unwrap()
4139 })
4140 .await
4141 .unwrap();
4142 assert_eq!(
4143 db.get_channel_messages(channel_id, 10, None)
4144 .await
4145 .unwrap()
4146 .iter()
4147 .map(|m| &m.body)
4148 .collect::<Vec<_>>(),
4149 &["surrounded by whitespace"]
4150 );
4151}
4152
4153#[gpui::test(iterations = 10)]
4154async fn test_chat_reconnection(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
4155 cx_a.foreground().forbid_parking();
4156 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
4157 let client_a = server.create_client(cx_a, "user_a").await;
4158 let client_b = server.create_client(cx_b, "user_b").await;
4159
4160 let mut status_b = client_b.status();
4161
4162 // Create an org that includes these 2 users.
4163 let db = &server.app_state.db;
4164 let org_id = db.create_org("Test Org", "test-org").await.unwrap();
4165 db.add_org_member(org_id, client_a.current_user_id(cx_a), false)
4166 .await
4167 .unwrap();
4168 db.add_org_member(org_id, client_b.current_user_id(cx_b), false)
4169 .await
4170 .unwrap();
4171
4172 // Create a channel that includes all the users.
4173 let channel_id = db.create_org_channel(org_id, "test-channel").await.unwrap();
4174 db.add_channel_member(channel_id, client_a.current_user_id(cx_a), false)
4175 .await
4176 .unwrap();
4177 db.add_channel_member(channel_id, client_b.current_user_id(cx_b), false)
4178 .await
4179 .unwrap();
4180 db.create_channel_message(
4181 channel_id,
4182 client_b.current_user_id(cx_b),
4183 "hello A, it's B.",
4184 OffsetDateTime::now_utc(),
4185 2,
4186 )
4187 .await
4188 .unwrap();
4189
4190 let channels_a =
4191 cx_a.add_model(|cx| ChannelList::new(client_a.user_store.clone(), client_a.clone(), cx));
4192 channels_a
4193 .condition(cx_a, |list, _| list.available_channels().is_some())
4194 .await;
4195
4196 channels_a.read_with(cx_a, |list, _| {
4197 assert_eq!(
4198 list.available_channels().unwrap(),
4199 &[ChannelDetails {
4200 id: channel_id.to_proto(),
4201 name: "test-channel".to_string()
4202 }]
4203 )
4204 });
4205 let channel_a = channels_a.update(cx_a, |this, cx| {
4206 this.get_channel(channel_id.to_proto(), cx).unwrap()
4207 });
4208 channel_a.read_with(cx_a, |channel, _| assert!(channel.messages().is_empty()));
4209 channel_a
4210 .condition(cx_a, |channel, _| {
4211 channel_messages(channel)
4212 == [("user_b".to_string(), "hello A, it's B.".to_string(), false)]
4213 })
4214 .await;
4215
4216 let channels_b =
4217 cx_b.add_model(|cx| ChannelList::new(client_b.user_store.clone(), client_b.clone(), cx));
4218 channels_b
4219 .condition(cx_b, |list, _| list.available_channels().is_some())
4220 .await;
4221 channels_b.read_with(cx_b, |list, _| {
4222 assert_eq!(
4223 list.available_channels().unwrap(),
4224 &[ChannelDetails {
4225 id: channel_id.to_proto(),
4226 name: "test-channel".to_string()
4227 }]
4228 )
4229 });
4230
4231 let channel_b = channels_b.update(cx_b, |this, cx| {
4232 this.get_channel(channel_id.to_proto(), cx).unwrap()
4233 });
4234 channel_b.read_with(cx_b, |channel, _| assert!(channel.messages().is_empty()));
4235 channel_b
4236 .condition(cx_b, |channel, _| {
4237 channel_messages(channel)
4238 == [("user_b".to_string(), "hello A, it's B.".to_string(), false)]
4239 })
4240 .await;
4241
4242 // Disconnect client B, ensuring we can still access its cached channel data.
4243 server.forbid_connections();
4244 server.disconnect_client(client_b.current_user_id(cx_b));
4245 cx_b.foreground().advance_clock(rpc::RECEIVE_TIMEOUT);
4246 while !matches!(
4247 status_b.next().await,
4248 Some(client::Status::ReconnectionError { .. })
4249 ) {}
4250
4251 channels_b.read_with(cx_b, |channels, _| {
4252 assert_eq!(
4253 channels.available_channels().unwrap(),
4254 [ChannelDetails {
4255 id: channel_id.to_proto(),
4256 name: "test-channel".to_string()
4257 }]
4258 )
4259 });
4260 channel_b.read_with(cx_b, |channel, _| {
4261 assert_eq!(
4262 channel_messages(channel),
4263 [("user_b".to_string(), "hello A, it's B.".to_string(), false)]
4264 )
4265 });
4266
4267 // Send a message from client B while it is disconnected.
4268 channel_b
4269 .update(cx_b, |channel, cx| {
4270 let task = channel
4271 .send_message("can you see this?".to_string(), cx)
4272 .unwrap();
4273 assert_eq!(
4274 channel_messages(channel),
4275 &[
4276 ("user_b".to_string(), "hello A, it's B.".to_string(), false),
4277 ("user_b".to_string(), "can you see this?".to_string(), true)
4278 ]
4279 );
4280 task
4281 })
4282 .await
4283 .unwrap_err();
4284
4285 // Send a message from client A while B is disconnected.
4286 channel_a
4287 .update(cx_a, |channel, cx| {
4288 channel
4289 .send_message("oh, hi B.".to_string(), cx)
4290 .unwrap()
4291 .detach();
4292 let task = channel.send_message("sup".to_string(), cx).unwrap();
4293 assert_eq!(
4294 channel_messages(channel),
4295 &[
4296 ("user_b".to_string(), "hello A, it's B.".to_string(), false),
4297 ("user_a".to_string(), "oh, hi B.".to_string(), true),
4298 ("user_a".to_string(), "sup".to_string(), true)
4299 ]
4300 );
4301 task
4302 })
4303 .await
4304 .unwrap();
4305
4306 // Give client B a chance to reconnect.
4307 server.allow_connections();
4308 cx_b.foreground().advance_clock(Duration::from_secs(10));
4309
4310 // Verify that B sees the new messages upon reconnection, as well as the message client B
4311 // sent while offline.
4312 channel_b
4313 .condition(cx_b, |channel, _| {
4314 channel_messages(channel)
4315 == [
4316 ("user_b".to_string(), "hello A, it's B.".to_string(), false),
4317 ("user_a".to_string(), "oh, hi B.".to_string(), false),
4318 ("user_a".to_string(), "sup".to_string(), false),
4319 ("user_b".to_string(), "can you see this?".to_string(), false),
4320 ]
4321 })
4322 .await;
4323
4324 // Ensure client A and B can communicate normally after reconnection.
4325 channel_a
4326 .update(cx_a, |channel, cx| {
4327 channel.send_message("you online?".to_string(), cx).unwrap()
4328 })
4329 .await
4330 .unwrap();
4331 channel_b
4332 .condition(cx_b, |channel, _| {
4333 channel_messages(channel)
4334 == [
4335 ("user_b".to_string(), "hello A, it's B.".to_string(), false),
4336 ("user_a".to_string(), "oh, hi B.".to_string(), false),
4337 ("user_a".to_string(), "sup".to_string(), false),
4338 ("user_b".to_string(), "can you see this?".to_string(), false),
4339 ("user_a".to_string(), "you online?".to_string(), false),
4340 ]
4341 })
4342 .await;
4343
4344 channel_b
4345 .update(cx_b, |channel, cx| {
4346 channel.send_message("yep".to_string(), cx).unwrap()
4347 })
4348 .await
4349 .unwrap();
4350 channel_a
4351 .condition(cx_a, |channel, _| {
4352 channel_messages(channel)
4353 == [
4354 ("user_b".to_string(), "hello A, it's B.".to_string(), false),
4355 ("user_a".to_string(), "oh, hi B.".to_string(), false),
4356 ("user_a".to_string(), "sup".to_string(), false),
4357 ("user_b".to_string(), "can you see this?".to_string(), false),
4358 ("user_a".to_string(), "you online?".to_string(), false),
4359 ("user_b".to_string(), "yep".to_string(), false),
4360 ]
4361 })
4362 .await;
4363}
4364
4365#[gpui::test(iterations = 10)]
4366async fn test_contacts(
4367 deterministic: Arc<Deterministic>,
4368 cx_a: &mut TestAppContext,
4369 cx_b: &mut TestAppContext,
4370 cx_c: &mut TestAppContext,
4371) {
4372 cx_a.foreground().forbid_parking();
4373 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
4374 let client_a = server.create_client(cx_a, "user_a").await;
4375 let client_b = server.create_client(cx_b, "user_b").await;
4376 let client_c = server.create_client(cx_c, "user_c").await;
4377 server
4378 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
4379 .await;
4380 let active_call_a = cx_a.read(ActiveCall::global);
4381 let active_call_b = cx_b.read(ActiveCall::global);
4382 let active_call_c = cx_c.read(ActiveCall::global);
4383
4384 deterministic.run_until_parked();
4385 assert_eq!(
4386 contacts(&client_a, cx_a),
4387 [
4388 ("user_b".to_string(), "online", "free"),
4389 ("user_c".to_string(), "online", "free")
4390 ]
4391 );
4392 assert_eq!(
4393 contacts(&client_b, cx_b),
4394 [
4395 ("user_a".to_string(), "online", "free"),
4396 ("user_c".to_string(), "online", "free")
4397 ]
4398 );
4399 assert_eq!(
4400 contacts(&client_c, cx_c),
4401 [
4402 ("user_a".to_string(), "online", "free"),
4403 ("user_b".to_string(), "online", "free")
4404 ]
4405 );
4406
4407 server.disconnect_client(client_c.current_user_id(cx_c));
4408 server.forbid_connections();
4409 deterministic.advance_clock(rpc::RECEIVE_TIMEOUT);
4410 assert_eq!(
4411 contacts(&client_a, cx_a),
4412 [
4413 ("user_b".to_string(), "online", "free"),
4414 ("user_c".to_string(), "offline", "free")
4415 ]
4416 );
4417 assert_eq!(
4418 contacts(&client_b, cx_b),
4419 [
4420 ("user_a".to_string(), "online", "free"),
4421 ("user_c".to_string(), "offline", "free")
4422 ]
4423 );
4424 assert_eq!(contacts(&client_c, cx_c), []);
4425
4426 server.allow_connections();
4427 client_c
4428 .authenticate_and_connect(false, &cx_c.to_async())
4429 .await
4430 .unwrap();
4431
4432 deterministic.run_until_parked();
4433 assert_eq!(
4434 contacts(&client_a, cx_a),
4435 [
4436 ("user_b".to_string(), "online", "free"),
4437 ("user_c".to_string(), "online", "free")
4438 ]
4439 );
4440 assert_eq!(
4441 contacts(&client_b, cx_b),
4442 [
4443 ("user_a".to_string(), "online", "free"),
4444 ("user_c".to_string(), "online", "free")
4445 ]
4446 );
4447 assert_eq!(
4448 contacts(&client_c, cx_c),
4449 [
4450 ("user_a".to_string(), "online", "free"),
4451 ("user_b".to_string(), "online", "free")
4452 ]
4453 );
4454
4455 active_call_a
4456 .update(cx_a, |call, cx| {
4457 call.invite(client_b.user_id().unwrap(), None, cx)
4458 })
4459 .await
4460 .unwrap();
4461 deterministic.run_until_parked();
4462 assert_eq!(
4463 contacts(&client_a, cx_a),
4464 [
4465 ("user_b".to_string(), "online", "busy"),
4466 ("user_c".to_string(), "online", "free")
4467 ]
4468 );
4469 assert_eq!(
4470 contacts(&client_b, cx_b),
4471 [
4472 ("user_a".to_string(), "online", "busy"),
4473 ("user_c".to_string(), "online", "free")
4474 ]
4475 );
4476 assert_eq!(
4477 contacts(&client_c, cx_c),
4478 [
4479 ("user_a".to_string(), "online", "busy"),
4480 ("user_b".to_string(), "online", "busy")
4481 ]
4482 );
4483
4484 active_call_b.update(cx_b, |call, _| call.decline_incoming().unwrap());
4485 deterministic.run_until_parked();
4486 assert_eq!(
4487 contacts(&client_a, cx_a),
4488 [
4489 ("user_b".to_string(), "online", "free"),
4490 ("user_c".to_string(), "online", "free")
4491 ]
4492 );
4493 assert_eq!(
4494 contacts(&client_b, cx_b),
4495 [
4496 ("user_a".to_string(), "online", "free"),
4497 ("user_c".to_string(), "online", "free")
4498 ]
4499 );
4500 assert_eq!(
4501 contacts(&client_c, cx_c),
4502 [
4503 ("user_a".to_string(), "online", "free"),
4504 ("user_b".to_string(), "online", "free")
4505 ]
4506 );
4507
4508 active_call_c
4509 .update(cx_c, |call, cx| {
4510 call.invite(client_a.user_id().unwrap(), None, cx)
4511 })
4512 .await
4513 .unwrap();
4514 deterministic.run_until_parked();
4515 assert_eq!(
4516 contacts(&client_a, cx_a),
4517 [
4518 ("user_b".to_string(), "online", "free"),
4519 ("user_c".to_string(), "online", "busy")
4520 ]
4521 );
4522 assert_eq!(
4523 contacts(&client_b, cx_b),
4524 [
4525 ("user_a".to_string(), "online", "busy"),
4526 ("user_c".to_string(), "online", "busy")
4527 ]
4528 );
4529 assert_eq!(
4530 contacts(&client_c, cx_c),
4531 [
4532 ("user_a".to_string(), "online", "busy"),
4533 ("user_b".to_string(), "online", "free")
4534 ]
4535 );
4536
4537 active_call_a
4538 .update(cx_a, |call, cx| call.accept_incoming(cx))
4539 .await
4540 .unwrap();
4541 deterministic.run_until_parked();
4542 assert_eq!(
4543 contacts(&client_a, cx_a),
4544 [
4545 ("user_b".to_string(), "online", "free"),
4546 ("user_c".to_string(), "online", "busy")
4547 ]
4548 );
4549 assert_eq!(
4550 contacts(&client_b, cx_b),
4551 [
4552 ("user_a".to_string(), "online", "busy"),
4553 ("user_c".to_string(), "online", "busy")
4554 ]
4555 );
4556 assert_eq!(
4557 contacts(&client_c, cx_c),
4558 [
4559 ("user_a".to_string(), "online", "busy"),
4560 ("user_b".to_string(), "online", "free")
4561 ]
4562 );
4563
4564 active_call_a
4565 .update(cx_a, |call, cx| {
4566 call.invite(client_b.user_id().unwrap(), None, cx)
4567 })
4568 .await
4569 .unwrap();
4570 deterministic.run_until_parked();
4571 assert_eq!(
4572 contacts(&client_a, cx_a),
4573 [
4574 ("user_b".to_string(), "online", "busy"),
4575 ("user_c".to_string(), "online", "busy")
4576 ]
4577 );
4578 assert_eq!(
4579 contacts(&client_b, cx_b),
4580 [
4581 ("user_a".to_string(), "online", "busy"),
4582 ("user_c".to_string(), "online", "busy")
4583 ]
4584 );
4585 assert_eq!(
4586 contacts(&client_c, cx_c),
4587 [
4588 ("user_a".to_string(), "online", "busy"),
4589 ("user_b".to_string(), "online", "busy")
4590 ]
4591 );
4592
4593 active_call_a.update(cx_a, |call, cx| call.hang_up(cx).unwrap());
4594 deterministic.run_until_parked();
4595 assert_eq!(
4596 contacts(&client_a, cx_a),
4597 [
4598 ("user_b".to_string(), "online", "free"),
4599 ("user_c".to_string(), "online", "free")
4600 ]
4601 );
4602 assert_eq!(
4603 contacts(&client_b, cx_b),
4604 [
4605 ("user_a".to_string(), "online", "free"),
4606 ("user_c".to_string(), "online", "free")
4607 ]
4608 );
4609 assert_eq!(
4610 contacts(&client_c, cx_c),
4611 [
4612 ("user_a".to_string(), "online", "free"),
4613 ("user_b".to_string(), "online", "free")
4614 ]
4615 );
4616
4617 active_call_a
4618 .update(cx_a, |call, cx| {
4619 call.invite(client_b.user_id().unwrap(), None, cx)
4620 })
4621 .await
4622 .unwrap();
4623 deterministic.run_until_parked();
4624 assert_eq!(
4625 contacts(&client_a, cx_a),
4626 [
4627 ("user_b".to_string(), "online", "busy"),
4628 ("user_c".to_string(), "online", "free")
4629 ]
4630 );
4631 assert_eq!(
4632 contacts(&client_b, cx_b),
4633 [
4634 ("user_a".to_string(), "online", "busy"),
4635 ("user_c".to_string(), "online", "free")
4636 ]
4637 );
4638 assert_eq!(
4639 contacts(&client_c, cx_c),
4640 [
4641 ("user_a".to_string(), "online", "busy"),
4642 ("user_b".to_string(), "online", "busy")
4643 ]
4644 );
4645
4646 server.forbid_connections();
4647 server.disconnect_client(client_a.current_user_id(cx_a));
4648 deterministic.advance_clock(rpc::RECEIVE_TIMEOUT);
4649 assert_eq!(contacts(&client_a, cx_a), []);
4650 assert_eq!(
4651 contacts(&client_b, cx_b),
4652 [
4653 ("user_a".to_string(), "offline", "free"),
4654 ("user_c".to_string(), "online", "free")
4655 ]
4656 );
4657 assert_eq!(
4658 contacts(&client_c, cx_c),
4659 [
4660 ("user_a".to_string(), "offline", "free"),
4661 ("user_b".to_string(), "online", "free")
4662 ]
4663 );
4664
4665 #[allow(clippy::type_complexity)]
4666 fn contacts(
4667 client: &TestClient,
4668 cx: &TestAppContext,
4669 ) -> Vec<(String, &'static str, &'static str)> {
4670 client.user_store.read_with(cx, |store, _| {
4671 store
4672 .contacts()
4673 .iter()
4674 .map(|contact| {
4675 (
4676 contact.user.github_login.clone(),
4677 if contact.online { "online" } else { "offline" },
4678 if contact.busy { "busy" } else { "free" },
4679 )
4680 })
4681 .collect()
4682 })
4683 }
4684}
4685
4686#[gpui::test(iterations = 10)]
4687async fn test_contact_requests(
4688 executor: Arc<Deterministic>,
4689 cx_a: &mut TestAppContext,
4690 cx_a2: &mut TestAppContext,
4691 cx_b: &mut TestAppContext,
4692 cx_b2: &mut TestAppContext,
4693 cx_c: &mut TestAppContext,
4694 cx_c2: &mut TestAppContext,
4695) {
4696 cx_a.foreground().forbid_parking();
4697
4698 // Connect to a server as 3 clients.
4699 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
4700 let client_a = server.create_client(cx_a, "user_a").await;
4701 let client_a2 = server.create_client(cx_a2, "user_a").await;
4702 let client_b = server.create_client(cx_b, "user_b").await;
4703 let client_b2 = server.create_client(cx_b2, "user_b").await;
4704 let client_c = server.create_client(cx_c, "user_c").await;
4705 let client_c2 = server.create_client(cx_c2, "user_c").await;
4706
4707 assert_eq!(client_a.user_id().unwrap(), client_a2.user_id().unwrap());
4708 assert_eq!(client_b.user_id().unwrap(), client_b2.user_id().unwrap());
4709 assert_eq!(client_c.user_id().unwrap(), client_c2.user_id().unwrap());
4710
4711 // User A and User C request that user B become their contact.
4712 client_a
4713 .user_store
4714 .update(cx_a, |store, cx| {
4715 store.request_contact(client_b.user_id().unwrap(), cx)
4716 })
4717 .await
4718 .unwrap();
4719 client_c
4720 .user_store
4721 .update(cx_c, |store, cx| {
4722 store.request_contact(client_b.user_id().unwrap(), cx)
4723 })
4724 .await
4725 .unwrap();
4726 executor.run_until_parked();
4727
4728 // All users see the pending request appear in all their clients.
4729 assert_eq!(
4730 client_a.summarize_contacts(cx_a).outgoing_requests,
4731 &["user_b"]
4732 );
4733 assert_eq!(
4734 client_a2.summarize_contacts(cx_a2).outgoing_requests,
4735 &["user_b"]
4736 );
4737 assert_eq!(
4738 client_b.summarize_contacts(cx_b).incoming_requests,
4739 &["user_a", "user_c"]
4740 );
4741 assert_eq!(
4742 client_b2.summarize_contacts(cx_b2).incoming_requests,
4743 &["user_a", "user_c"]
4744 );
4745 assert_eq!(
4746 client_c.summarize_contacts(cx_c).outgoing_requests,
4747 &["user_b"]
4748 );
4749 assert_eq!(
4750 client_c2.summarize_contacts(cx_c2).outgoing_requests,
4751 &["user_b"]
4752 );
4753
4754 // Contact requests are present upon connecting (tested here via disconnect/reconnect)
4755 disconnect_and_reconnect(&client_a, cx_a).await;
4756 disconnect_and_reconnect(&client_b, cx_b).await;
4757 disconnect_and_reconnect(&client_c, cx_c).await;
4758 executor.run_until_parked();
4759 assert_eq!(
4760 client_a.summarize_contacts(cx_a).outgoing_requests,
4761 &["user_b"]
4762 );
4763 assert_eq!(
4764 client_b.summarize_contacts(cx_b).incoming_requests,
4765 &["user_a", "user_c"]
4766 );
4767 assert_eq!(
4768 client_c.summarize_contacts(cx_c).outgoing_requests,
4769 &["user_b"]
4770 );
4771
4772 // User B accepts the request from user A.
4773 client_b
4774 .user_store
4775 .update(cx_b, |store, cx| {
4776 store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx)
4777 })
4778 .await
4779 .unwrap();
4780
4781 executor.run_until_parked();
4782
4783 // User B sees user A as their contact now in all client, and the incoming request from them is removed.
4784 let contacts_b = client_b.summarize_contacts(cx_b);
4785 assert_eq!(contacts_b.current, &["user_a"]);
4786 assert_eq!(contacts_b.incoming_requests, &["user_c"]);
4787 let contacts_b2 = client_b2.summarize_contacts(cx_b2);
4788 assert_eq!(contacts_b2.current, &["user_a"]);
4789 assert_eq!(contacts_b2.incoming_requests, &["user_c"]);
4790
4791 // User A sees user B as their contact now in all clients, and the outgoing request to them is removed.
4792 let contacts_a = client_a.summarize_contacts(cx_a);
4793 assert_eq!(contacts_a.current, &["user_b"]);
4794 assert!(contacts_a.outgoing_requests.is_empty());
4795 let contacts_a2 = client_a2.summarize_contacts(cx_a2);
4796 assert_eq!(contacts_a2.current, &["user_b"]);
4797 assert!(contacts_a2.outgoing_requests.is_empty());
4798
4799 // Contacts are present upon connecting (tested here via disconnect/reconnect)
4800 disconnect_and_reconnect(&client_a, cx_a).await;
4801 disconnect_and_reconnect(&client_b, cx_b).await;
4802 disconnect_and_reconnect(&client_c, cx_c).await;
4803 executor.run_until_parked();
4804 assert_eq!(client_a.summarize_contacts(cx_a).current, &["user_b"]);
4805 assert_eq!(client_b.summarize_contacts(cx_b).current, &["user_a"]);
4806 assert_eq!(
4807 client_b.summarize_contacts(cx_b).incoming_requests,
4808 &["user_c"]
4809 );
4810 assert!(client_c.summarize_contacts(cx_c).current.is_empty());
4811 assert_eq!(
4812 client_c.summarize_contacts(cx_c).outgoing_requests,
4813 &["user_b"]
4814 );
4815
4816 // User B rejects the request from user C.
4817 client_b
4818 .user_store
4819 .update(cx_b, |store, cx| {
4820 store.respond_to_contact_request(client_c.user_id().unwrap(), false, cx)
4821 })
4822 .await
4823 .unwrap();
4824
4825 executor.run_until_parked();
4826
4827 // User B doesn't see user C as their contact, and the incoming request from them is removed.
4828 let contacts_b = client_b.summarize_contacts(cx_b);
4829 assert_eq!(contacts_b.current, &["user_a"]);
4830 assert!(contacts_b.incoming_requests.is_empty());
4831 let contacts_b2 = client_b2.summarize_contacts(cx_b2);
4832 assert_eq!(contacts_b2.current, &["user_a"]);
4833 assert!(contacts_b2.incoming_requests.is_empty());
4834
4835 // User C doesn't see user B as their contact, and the outgoing request to them is removed.
4836 let contacts_c = client_c.summarize_contacts(cx_c);
4837 assert!(contacts_c.current.is_empty());
4838 assert!(contacts_c.outgoing_requests.is_empty());
4839 let contacts_c2 = client_c2.summarize_contacts(cx_c2);
4840 assert!(contacts_c2.current.is_empty());
4841 assert!(contacts_c2.outgoing_requests.is_empty());
4842
4843 // Incoming/outgoing requests are not present upon connecting (tested here via disconnect/reconnect)
4844 disconnect_and_reconnect(&client_a, cx_a).await;
4845 disconnect_and_reconnect(&client_b, cx_b).await;
4846 disconnect_and_reconnect(&client_c, cx_c).await;
4847 executor.run_until_parked();
4848 assert_eq!(client_a.summarize_contacts(cx_a).current, &["user_b"]);
4849 assert_eq!(client_b.summarize_contacts(cx_b).current, &["user_a"]);
4850 assert!(client_b
4851 .summarize_contacts(cx_b)
4852 .incoming_requests
4853 .is_empty());
4854 assert!(client_c.summarize_contacts(cx_c).current.is_empty());
4855 assert!(client_c
4856 .summarize_contacts(cx_c)
4857 .outgoing_requests
4858 .is_empty());
4859
4860 async fn disconnect_and_reconnect(client: &TestClient, cx: &mut TestAppContext) {
4861 client.disconnect(&cx.to_async()).unwrap();
4862 client.clear_contacts(cx).await;
4863 client
4864 .authenticate_and_connect(false, &cx.to_async())
4865 .await
4866 .unwrap();
4867 }
4868}
4869
4870#[gpui::test(iterations = 10)]
4871async fn test_following(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
4872 cx_a.foreground().forbid_parking();
4873 cx_a.update(editor::init);
4874 cx_b.update(editor::init);
4875
4876 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
4877 let client_a = server.create_client(cx_a, "user_a").await;
4878 let client_b = server.create_client(cx_b, "user_b").await;
4879 server
4880 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4881 .await;
4882 let active_call_a = cx_a.read(ActiveCall::global);
4883
4884 client_a
4885 .fs
4886 .insert_tree(
4887 "/a",
4888 json!({
4889 "1.txt": "one",
4890 "2.txt": "two",
4891 "3.txt": "three",
4892 }),
4893 )
4894 .await;
4895 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
4896 let project_id = active_call_a
4897 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4898 .await
4899 .unwrap();
4900 let project_b = client_b.build_remote_project(project_id, cx_b).await;
4901
4902 // Client A opens some editors.
4903 let workspace_a = client_a.build_workspace(&project_a, cx_a);
4904 let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
4905 let editor_a1 = workspace_a
4906 .update(cx_a, |workspace, cx| {
4907 workspace.open_path((worktree_id, "1.txt"), true, cx)
4908 })
4909 .await
4910 .unwrap()
4911 .downcast::<Editor>()
4912 .unwrap();
4913 let editor_a2 = workspace_a
4914 .update(cx_a, |workspace, cx| {
4915 workspace.open_path((worktree_id, "2.txt"), true, cx)
4916 })
4917 .await
4918 .unwrap()
4919 .downcast::<Editor>()
4920 .unwrap();
4921
4922 // Client B opens an editor.
4923 let workspace_b = client_b.build_workspace(&project_b, cx_b);
4924 let editor_b1 = workspace_b
4925 .update(cx_b, |workspace, cx| {
4926 workspace.open_path((worktree_id, "1.txt"), true, cx)
4927 })
4928 .await
4929 .unwrap()
4930 .downcast::<Editor>()
4931 .unwrap();
4932
4933 let client_a_id = project_b.read_with(cx_b, |project, _| {
4934 project.collaborators().values().next().unwrap().peer_id
4935 });
4936 let client_b_id = project_a.read_with(cx_a, |project, _| {
4937 project.collaborators().values().next().unwrap().peer_id
4938 });
4939
4940 // When client B starts following client A, all visible view states are replicated to client B.
4941 editor_a1.update(cx_a, |editor, cx| {
4942 editor.change_selections(None, cx, |s| s.select_ranges([0..1]))
4943 });
4944 editor_a2.update(cx_a, |editor, cx| {
4945 editor.change_selections(None, cx, |s| s.select_ranges([2..3]))
4946 });
4947 workspace_b
4948 .update(cx_b, |workspace, cx| {
4949 workspace
4950 .toggle_follow(&ToggleFollow(client_a_id), cx)
4951 .unwrap()
4952 })
4953 .await
4954 .unwrap();
4955
4956 let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
4957 workspace
4958 .active_item(cx)
4959 .unwrap()
4960 .downcast::<Editor>()
4961 .unwrap()
4962 });
4963 assert!(cx_b.read(|cx| editor_b2.is_focused(cx)));
4964 assert_eq!(
4965 editor_b2.read_with(cx_b, |editor, cx| editor.project_path(cx)),
4966 Some((worktree_id, "2.txt").into())
4967 );
4968 assert_eq!(
4969 editor_b2.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
4970 vec![2..3]
4971 );
4972 assert_eq!(
4973 editor_b1.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
4974 vec![0..1]
4975 );
4976
4977 // When client A activates a different editor, client B does so as well.
4978 workspace_a.update(cx_a, |workspace, cx| {
4979 workspace.activate_item(&editor_a1, cx)
4980 });
4981 workspace_b
4982 .condition(cx_b, |workspace, cx| {
4983 workspace.active_item(cx).unwrap().id() == editor_b1.id()
4984 })
4985 .await;
4986
4987 // When client A navigates back and forth, client B does so as well.
4988 workspace_a
4989 .update(cx_a, |workspace, cx| {
4990 workspace::Pane::go_back(workspace, None, cx)
4991 })
4992 .await;
4993 workspace_b
4994 .condition(cx_b, |workspace, cx| {
4995 workspace.active_item(cx).unwrap().id() == editor_b2.id()
4996 })
4997 .await;
4998
4999 workspace_a
5000 .update(cx_a, |workspace, cx| {
5001 workspace::Pane::go_forward(workspace, None, cx)
5002 })
5003 .await;
5004 workspace_b
5005 .condition(cx_b, |workspace, cx| {
5006 workspace.active_item(cx).unwrap().id() == editor_b1.id()
5007 })
5008 .await;
5009
5010 // Changes to client A's editor are reflected on client B.
5011 editor_a1.update(cx_a, |editor, cx| {
5012 editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2]));
5013 });
5014 editor_b1
5015 .condition(cx_b, |editor, cx| {
5016 editor.selections.ranges(cx) == vec![1..1, 2..2]
5017 })
5018 .await;
5019
5020 editor_a1.update(cx_a, |editor, cx| editor.set_text("TWO", cx));
5021 editor_b1
5022 .condition(cx_b, |editor, cx| editor.text(cx) == "TWO")
5023 .await;
5024
5025 editor_a1.update(cx_a, |editor, cx| {
5026 editor.change_selections(None, cx, |s| s.select_ranges([3..3]));
5027 editor.set_scroll_position(vec2f(0., 100.), cx);
5028 });
5029 editor_b1
5030 .condition(cx_b, |editor, cx| {
5031 editor.selections.ranges(cx) == vec![3..3]
5032 })
5033 .await;
5034
5035 // After unfollowing, client B stops receiving updates from client A.
5036 workspace_b.update(cx_b, |workspace, cx| {
5037 workspace.unfollow(&workspace.active_pane().clone(), cx)
5038 });
5039 workspace_a.update(cx_a, |workspace, cx| {
5040 workspace.activate_item(&editor_a2, cx)
5041 });
5042 cx_a.foreground().run_until_parked();
5043 assert_eq!(
5044 workspace_b.read_with(cx_b, |workspace, cx| workspace
5045 .active_item(cx)
5046 .unwrap()
5047 .id()),
5048 editor_b1.id()
5049 );
5050
5051 // Client A starts following client B.
5052 workspace_a
5053 .update(cx_a, |workspace, cx| {
5054 workspace
5055 .toggle_follow(&ToggleFollow(client_b_id), cx)
5056 .unwrap()
5057 })
5058 .await
5059 .unwrap();
5060 assert_eq!(
5061 workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
5062 Some(client_b_id)
5063 );
5064 assert_eq!(
5065 workspace_a.read_with(cx_a, |workspace, cx| workspace
5066 .active_item(cx)
5067 .unwrap()
5068 .id()),
5069 editor_a1.id()
5070 );
5071
5072 // Following interrupts when client B disconnects.
5073 client_b.disconnect(&cx_b.to_async()).unwrap();
5074 cx_a.foreground().run_until_parked();
5075 assert_eq!(
5076 workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
5077 None
5078 );
5079}
5080
5081#[gpui::test(iterations = 10)]
5082async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
5083 cx_a.foreground().forbid_parking();
5084 cx_a.update(editor::init);
5085 cx_b.update(editor::init);
5086
5087 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
5088 let client_a = server.create_client(cx_a, "user_a").await;
5089 let client_b = server.create_client(cx_b, "user_b").await;
5090 server
5091 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5092 .await;
5093 let active_call_a = cx_a.read(ActiveCall::global);
5094
5095 // Client A shares a project.
5096 client_a
5097 .fs
5098 .insert_tree(
5099 "/a",
5100 json!({
5101 "1.txt": "one",
5102 "2.txt": "two",
5103 "3.txt": "three",
5104 "4.txt": "four",
5105 }),
5106 )
5107 .await;
5108 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
5109 let project_id = active_call_a
5110 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5111 .await
5112 .unwrap();
5113
5114 // Client B joins the project.
5115 let project_b = client_b.build_remote_project(project_id, cx_b).await;
5116
5117 // Client A opens some editors.
5118 let workspace_a = client_a.build_workspace(&project_a, cx_a);
5119 let pane_a1 = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
5120 let _editor_a1 = workspace_a
5121 .update(cx_a, |workspace, cx| {
5122 workspace.open_path((worktree_id, "1.txt"), true, cx)
5123 })
5124 .await
5125 .unwrap()
5126 .downcast::<Editor>()
5127 .unwrap();
5128
5129 // Client B opens an editor.
5130 let workspace_b = client_b.build_workspace(&project_b, cx_b);
5131 let pane_b1 = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
5132 let _editor_b1 = workspace_b
5133 .update(cx_b, |workspace, cx| {
5134 workspace.open_path((worktree_id, "2.txt"), true, cx)
5135 })
5136 .await
5137 .unwrap()
5138 .downcast::<Editor>()
5139 .unwrap();
5140
5141 // Clients A and B follow each other in split panes
5142 workspace_a.update(cx_a, |workspace, cx| {
5143 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
5144 let pane_a1 = pane_a1.clone();
5145 cx.defer(move |workspace, _| {
5146 assert_ne!(*workspace.active_pane(), pane_a1);
5147 });
5148 });
5149 workspace_a
5150 .update(cx_a, |workspace, cx| {
5151 let leader_id = *project_a.read(cx).collaborators().keys().next().unwrap();
5152 workspace
5153 .toggle_follow(&workspace::ToggleFollow(leader_id), cx)
5154 .unwrap()
5155 })
5156 .await
5157 .unwrap();
5158 workspace_b.update(cx_b, |workspace, cx| {
5159 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
5160 let pane_b1 = pane_b1.clone();
5161 cx.defer(move |workspace, _| {
5162 assert_ne!(*workspace.active_pane(), pane_b1);
5163 });
5164 });
5165 workspace_b
5166 .update(cx_b, |workspace, cx| {
5167 let leader_id = *project_b.read(cx).collaborators().keys().next().unwrap();
5168 workspace
5169 .toggle_follow(&workspace::ToggleFollow(leader_id), cx)
5170 .unwrap()
5171 })
5172 .await
5173 .unwrap();
5174
5175 workspace_a.update(cx_a, |workspace, cx| {
5176 workspace.activate_next_pane(cx);
5177 });
5178 // Wait for focus effects to be fully flushed
5179 workspace_a.update(cx_a, |workspace, _| {
5180 assert_eq!(*workspace.active_pane(), pane_a1);
5181 });
5182
5183 workspace_a
5184 .update(cx_a, |workspace, cx| {
5185 workspace.open_path((worktree_id, "3.txt"), true, cx)
5186 })
5187 .await
5188 .unwrap();
5189 workspace_b.update(cx_b, |workspace, cx| {
5190 workspace.activate_next_pane(cx);
5191 });
5192
5193 workspace_b
5194 .update(cx_b, |workspace, cx| {
5195 assert_eq!(*workspace.active_pane(), pane_b1);
5196 workspace.open_path((worktree_id, "4.txt"), true, cx)
5197 })
5198 .await
5199 .unwrap();
5200 cx_a.foreground().run_until_parked();
5201
5202 // Ensure leader updates don't change the active pane of followers
5203 workspace_a.read_with(cx_a, |workspace, _| {
5204 assert_eq!(*workspace.active_pane(), pane_a1);
5205 });
5206 workspace_b.read_with(cx_b, |workspace, _| {
5207 assert_eq!(*workspace.active_pane(), pane_b1);
5208 });
5209
5210 // Ensure peers following each other doesn't cause an infinite loop.
5211 assert_eq!(
5212 workspace_a.read_with(cx_a, |workspace, cx| workspace
5213 .active_item(cx)
5214 .unwrap()
5215 .project_path(cx)),
5216 Some((worktree_id, "3.txt").into())
5217 );
5218 workspace_a.update(cx_a, |workspace, cx| {
5219 assert_eq!(
5220 workspace.active_item(cx).unwrap().project_path(cx),
5221 Some((worktree_id, "3.txt").into())
5222 );
5223 workspace.activate_next_pane(cx);
5224 });
5225
5226 workspace_a.update(cx_a, |workspace, cx| {
5227 assert_eq!(
5228 workspace.active_item(cx).unwrap().project_path(cx),
5229 Some((worktree_id, "4.txt").into())
5230 );
5231 });
5232
5233 workspace_b.update(cx_b, |workspace, cx| {
5234 assert_eq!(
5235 workspace.active_item(cx).unwrap().project_path(cx),
5236 Some((worktree_id, "4.txt").into())
5237 );
5238 workspace.activate_next_pane(cx);
5239 });
5240
5241 workspace_b.update(cx_b, |workspace, cx| {
5242 assert_eq!(
5243 workspace.active_item(cx).unwrap().project_path(cx),
5244 Some((worktree_id, "3.txt").into())
5245 );
5246 });
5247}
5248
5249#[gpui::test(iterations = 10)]
5250async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
5251 cx_a.foreground().forbid_parking();
5252 cx_a.update(editor::init);
5253 cx_b.update(editor::init);
5254
5255 // 2 clients connect to a server.
5256 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
5257 let client_a = server.create_client(cx_a, "user_a").await;
5258 let client_b = server.create_client(cx_b, "user_b").await;
5259 server
5260 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5261 .await;
5262 let active_call_a = cx_a.read(ActiveCall::global);
5263
5264 // Client A shares a project.
5265 client_a
5266 .fs
5267 .insert_tree(
5268 "/a",
5269 json!({
5270 "1.txt": "one",
5271 "2.txt": "two",
5272 "3.txt": "three",
5273 }),
5274 )
5275 .await;
5276 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
5277 let project_id = active_call_a
5278 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5279 .await
5280 .unwrap();
5281 let project_b = client_b.build_remote_project(project_id, cx_b).await;
5282
5283 // Client A opens some editors.
5284 let workspace_a = client_a.build_workspace(&project_a, cx_a);
5285 let _editor_a1 = workspace_a
5286 .update(cx_a, |workspace, cx| {
5287 workspace.open_path((worktree_id, "1.txt"), true, cx)
5288 })
5289 .await
5290 .unwrap()
5291 .downcast::<Editor>()
5292 .unwrap();
5293
5294 // Client B starts following client A.
5295 let workspace_b = client_b.build_workspace(&project_b, cx_b);
5296 let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
5297 let leader_id = project_b.read_with(cx_b, |project, _| {
5298 project.collaborators().values().next().unwrap().peer_id
5299 });
5300 workspace_b
5301 .update(cx_b, |workspace, cx| {
5302 workspace
5303 .toggle_follow(&ToggleFollow(leader_id), cx)
5304 .unwrap()
5305 })
5306 .await
5307 .unwrap();
5308 assert_eq!(
5309 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
5310 Some(leader_id)
5311 );
5312 let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
5313 workspace
5314 .active_item(cx)
5315 .unwrap()
5316 .downcast::<Editor>()
5317 .unwrap()
5318 });
5319
5320 // When client B moves, it automatically stops following client A.
5321 editor_b2.update(cx_b, |editor, cx| editor.move_right(&editor::MoveRight, cx));
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 edits, it automatically stops following client A.
5341 editor_b2.update(cx_b, |editor, cx| editor.insert("X", cx));
5342 assert_eq!(
5343 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
5344 None
5345 );
5346
5347 workspace_b
5348 .update(cx_b, |workspace, cx| {
5349 workspace
5350 .toggle_follow(&ToggleFollow(leader_id), cx)
5351 .unwrap()
5352 })
5353 .await
5354 .unwrap();
5355 assert_eq!(
5356 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
5357 Some(leader_id)
5358 );
5359
5360 // When client B scrolls, it automatically stops following client A.
5361 editor_b2.update(cx_b, |editor, cx| {
5362 editor.set_scroll_position(vec2f(0., 3.), cx)
5363 });
5364 assert_eq!(
5365 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
5366 None
5367 );
5368
5369 workspace_b
5370 .update(cx_b, |workspace, cx| {
5371 workspace
5372 .toggle_follow(&ToggleFollow(leader_id), cx)
5373 .unwrap()
5374 })
5375 .await
5376 .unwrap();
5377 assert_eq!(
5378 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
5379 Some(leader_id)
5380 );
5381
5382 // When client B activates a different pane, it continues following client A in the original pane.
5383 workspace_b.update(cx_b, |workspace, cx| {
5384 workspace.split_pane(pane_b.clone(), SplitDirection::Right, cx)
5385 });
5386 assert_eq!(
5387 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
5388 Some(leader_id)
5389 );
5390
5391 workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
5392 assert_eq!(
5393 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
5394 Some(leader_id)
5395 );
5396
5397 // When client B activates a different item in the original pane, it automatically stops following client A.
5398 workspace_b
5399 .update(cx_b, |workspace, cx| {
5400 workspace.open_path((worktree_id, "2.txt"), true, cx)
5401 })
5402 .await
5403 .unwrap();
5404 assert_eq!(
5405 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
5406 None
5407 );
5408}
5409
5410#[gpui::test(iterations = 10)]
5411async fn test_peers_simultaneously_following_each_other(
5412 deterministic: Arc<Deterministic>,
5413 cx_a: &mut TestAppContext,
5414 cx_b: &mut TestAppContext,
5415) {
5416 deterministic.forbid_parking();
5417 cx_a.update(editor::init);
5418 cx_b.update(editor::init);
5419
5420 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
5421 let client_a = server.create_client(cx_a, "user_a").await;
5422 let client_b = server.create_client(cx_b, "user_b").await;
5423 server
5424 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5425 .await;
5426 let active_call_a = cx_a.read(ActiveCall::global);
5427
5428 client_a.fs.insert_tree("/a", json!({})).await;
5429 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
5430 let workspace_a = client_a.build_workspace(&project_a, cx_a);
5431 let project_id = active_call_a
5432 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5433 .await
5434 .unwrap();
5435
5436 let project_b = client_b.build_remote_project(project_id, cx_b).await;
5437 let workspace_b = client_b.build_workspace(&project_b, cx_b);
5438
5439 deterministic.run_until_parked();
5440 let client_a_id = project_b.read_with(cx_b, |project, _| {
5441 project.collaborators().values().next().unwrap().peer_id
5442 });
5443 let client_b_id = project_a.read_with(cx_a, |project, _| {
5444 project.collaborators().values().next().unwrap().peer_id
5445 });
5446
5447 let a_follow_b = workspace_a.update(cx_a, |workspace, cx| {
5448 workspace
5449 .toggle_follow(&ToggleFollow(client_b_id), cx)
5450 .unwrap()
5451 });
5452 let b_follow_a = workspace_b.update(cx_b, |workspace, cx| {
5453 workspace
5454 .toggle_follow(&ToggleFollow(client_a_id), cx)
5455 .unwrap()
5456 });
5457
5458 futures::try_join!(a_follow_b, b_follow_a).unwrap();
5459 workspace_a.read_with(cx_a, |workspace, _| {
5460 assert_eq!(
5461 workspace.leader_for_pane(workspace.active_pane()),
5462 Some(client_b_id)
5463 );
5464 });
5465 workspace_b.read_with(cx_b, |workspace, _| {
5466 assert_eq!(
5467 workspace.leader_for_pane(workspace.active_pane()),
5468 Some(client_a_id)
5469 );
5470 });
5471}
5472
5473#[gpui::test(iterations = 100)]
5474async fn test_random_collaboration(
5475 cx: &mut TestAppContext,
5476 deterministic: Arc<Deterministic>,
5477 rng: StdRng,
5478) {
5479 deterministic.forbid_parking();
5480 let max_peers = env::var("MAX_PEERS")
5481 .map(|i| i.parse().expect("invalid `MAX_PEERS` variable"))
5482 .unwrap_or(5);
5483 assert!(max_peers <= 5);
5484
5485 let max_operations = env::var("OPERATIONS")
5486 .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
5487 .unwrap_or(10);
5488
5489 let rng = Arc::new(Mutex::new(rng));
5490
5491 let guest_lang_registry = Arc::new(LanguageRegistry::test());
5492 let host_language_registry = Arc::new(LanguageRegistry::test());
5493
5494 let fs = FakeFs::new(cx.background());
5495 fs.insert_tree("/_collab", json!({"init": ""})).await;
5496
5497 let mut server = TestServer::start(cx.foreground(), cx.background()).await;
5498 let db = server.app_state.db.clone();
5499
5500 let room_creator_user_id = db
5501 .create_user(
5502 "room-creator@example.com",
5503 false,
5504 NewUserParams {
5505 github_login: "room-creator".into(),
5506 github_user_id: 0,
5507 invite_count: 0,
5508 },
5509 )
5510 .await
5511 .unwrap()
5512 .user_id;
5513 let mut available_guests = vec![
5514 "guest-1".to_string(),
5515 "guest-2".to_string(),
5516 "guest-3".to_string(),
5517 "guest-4".to_string(),
5518 ];
5519
5520 for (ix, username) in Some(&"host".to_string())
5521 .into_iter()
5522 .chain(&available_guests)
5523 .enumerate()
5524 {
5525 let user_id = db
5526 .create_user(
5527 &format!("{username}@example.com"),
5528 false,
5529 NewUserParams {
5530 github_login: username.into(),
5531 github_user_id: (ix + 1) as i32,
5532 invite_count: 0,
5533 },
5534 )
5535 .await
5536 .unwrap()
5537 .user_id;
5538 server
5539 .app_state
5540 .db
5541 .send_contact_request(user_id, room_creator_user_id)
5542 .await
5543 .unwrap();
5544 server
5545 .app_state
5546 .db
5547 .respond_to_contact_request(room_creator_user_id, user_id, true)
5548 .await
5549 .unwrap();
5550 }
5551
5552 let _room_creator = server.create_client(cx, "room-creator").await;
5553 let active_call = cx.read(ActiveCall::global);
5554
5555 let mut clients = Vec::new();
5556 let mut user_ids = Vec::new();
5557 let mut op_start_signals = Vec::new();
5558
5559 let mut next_entity_id = 100000;
5560 let mut host_cx = TestAppContext::new(
5561 cx.foreground_platform(),
5562 cx.platform(),
5563 deterministic.build_foreground(next_entity_id),
5564 deterministic.build_background(),
5565 cx.font_cache(),
5566 cx.leak_detector(),
5567 next_entity_id,
5568 cx.function_name.clone(),
5569 );
5570 let host = server.create_client(&mut host_cx, "host").await;
5571 let host_project = host_cx.update(|cx| {
5572 Project::local(
5573 host.client.clone(),
5574 host.user_store.clone(),
5575 host.project_store.clone(),
5576 host_language_registry.clone(),
5577 fs.clone(),
5578 cx,
5579 )
5580 });
5581
5582 let (collab_worktree, _) = host_project
5583 .update(&mut host_cx, |project, cx| {
5584 project.find_or_create_local_worktree("/_collab", true, cx)
5585 })
5586 .await
5587 .unwrap();
5588 collab_worktree
5589 .read_with(&host_cx, |tree, _| tree.as_local().unwrap().scan_complete())
5590 .await;
5591
5592 // Set up fake language servers.
5593 let mut language = Language::new(
5594 LanguageConfig {
5595 name: "Rust".into(),
5596 path_suffixes: vec!["rs".to_string()],
5597 ..Default::default()
5598 },
5599 None,
5600 );
5601 let _fake_servers = language
5602 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
5603 name: "the-fake-language-server",
5604 capabilities: lsp::LanguageServer::full_capabilities(),
5605 initializer: Some(Box::new({
5606 let rng = rng.clone();
5607 let fs = fs.clone();
5608 let project = host_project.downgrade();
5609 move |fake_server: &mut FakeLanguageServer| {
5610 fake_server.handle_request::<lsp::request::Completion, _, _>(
5611 |_, _| async move {
5612 Ok(Some(lsp::CompletionResponse::Array(vec![
5613 lsp::CompletionItem {
5614 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
5615 range: lsp::Range::new(
5616 lsp::Position::new(0, 0),
5617 lsp::Position::new(0, 0),
5618 ),
5619 new_text: "the-new-text".to_string(),
5620 })),
5621 ..Default::default()
5622 },
5623 ])))
5624 },
5625 );
5626
5627 fake_server.handle_request::<lsp::request::CodeActionRequest, _, _>(
5628 |_, _| async move {
5629 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
5630 lsp::CodeAction {
5631 title: "the-code-action".to_string(),
5632 ..Default::default()
5633 },
5634 )]))
5635 },
5636 );
5637
5638 fake_server.handle_request::<lsp::request::PrepareRenameRequest, _, _>(
5639 |params, _| async move {
5640 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
5641 params.position,
5642 params.position,
5643 ))))
5644 },
5645 );
5646
5647 fake_server.handle_request::<lsp::request::GotoDefinition, _, _>({
5648 let fs = fs.clone();
5649 let rng = rng.clone();
5650 move |_, _| {
5651 let fs = fs.clone();
5652 let rng = rng.clone();
5653 async move {
5654 let files = fs.files().await;
5655 let mut rng = rng.lock();
5656 let count = rng.gen_range::<usize, _>(1..3);
5657 let files = (0..count)
5658 .map(|_| files.choose(&mut *rng).unwrap())
5659 .collect::<Vec<_>>();
5660 log::info!("LSP: Returning definitions in files {:?}", &files);
5661 Ok(Some(lsp::GotoDefinitionResponse::Array(
5662 files
5663 .into_iter()
5664 .map(|file| lsp::Location {
5665 uri: lsp::Url::from_file_path(file).unwrap(),
5666 range: Default::default(),
5667 })
5668 .collect(),
5669 )))
5670 }
5671 }
5672 });
5673
5674 fake_server.handle_request::<lsp::request::DocumentHighlightRequest, _, _>({
5675 let rng = rng.clone();
5676 let project = project;
5677 move |params, mut cx| {
5678 let highlights = if let Some(project) = project.upgrade(&cx) {
5679 project.update(&mut cx, |project, cx| {
5680 let path = params
5681 .text_document_position_params
5682 .text_document
5683 .uri
5684 .to_file_path()
5685 .unwrap();
5686 let (worktree, relative_path) =
5687 project.find_local_worktree(&path, cx)?;
5688 let project_path =
5689 ProjectPath::from((worktree.read(cx).id(), relative_path));
5690 let buffer =
5691 project.get_open_buffer(&project_path, cx)?.read(cx);
5692
5693 let mut highlights = Vec::new();
5694 let highlight_count = rng.lock().gen_range(1..=5);
5695 let mut prev_end = 0;
5696 for _ in 0..highlight_count {
5697 let range =
5698 buffer.random_byte_range(prev_end, &mut *rng.lock());
5699
5700 highlights.push(lsp::DocumentHighlight {
5701 range: range_to_lsp(range.to_point_utf16(buffer)),
5702 kind: Some(lsp::DocumentHighlightKind::READ),
5703 });
5704 prev_end = range.end;
5705 }
5706 Some(highlights)
5707 })
5708 } else {
5709 None
5710 };
5711 async move { Ok(highlights) }
5712 }
5713 });
5714 }
5715 })),
5716 ..Default::default()
5717 }))
5718 .await;
5719 host_language_registry.add(Arc::new(language));
5720
5721 let host_user_id = host.current_user_id(&host_cx);
5722 active_call
5723 .update(cx, |call, cx| {
5724 call.invite(host_user_id.to_proto(), None, cx)
5725 })
5726 .await
5727 .unwrap();
5728 active_call.read_with(cx, |call, cx| call.room().unwrap().read(cx).id());
5729 deterministic.run_until_parked();
5730 let host_active_call = host_cx.read(ActiveCall::global);
5731 host_active_call
5732 .update(&mut host_cx, |call, cx| call.accept_incoming(cx))
5733 .await
5734 .unwrap();
5735
5736 let host_project_id = host_active_call
5737 .update(&mut host_cx, |call, cx| {
5738 call.share_project(host_project.clone(), cx)
5739 })
5740 .await
5741 .unwrap();
5742
5743 let op_start_signal = futures::channel::mpsc::unbounded();
5744 user_ids.push(host_user_id);
5745 op_start_signals.push(op_start_signal.0);
5746 clients.push(host_cx.foreground().spawn(host.simulate_host(
5747 host_project,
5748 op_start_signal.1,
5749 rng.clone(),
5750 host_cx,
5751 )));
5752
5753 let disconnect_host_at = if rng.lock().gen_bool(0.2) {
5754 rng.lock().gen_range(0..max_operations)
5755 } else {
5756 max_operations
5757 };
5758
5759 let mut operations = 0;
5760 while operations < max_operations {
5761 if operations == disconnect_host_at {
5762 server.disconnect_client(user_ids[0]);
5763 deterministic.advance_clock(RECEIVE_TIMEOUT);
5764 drop(op_start_signals);
5765
5766 deterministic.start_waiting();
5767 let mut clients = futures::future::join_all(clients).await;
5768 deterministic.finish_waiting();
5769 deterministic.run_until_parked();
5770
5771 let (host, host_project, mut host_cx, host_err) = clients.remove(0);
5772 if let Some(host_err) = host_err {
5773 log::error!("host error - {:?}", host_err);
5774 }
5775 host_project.read_with(&host_cx, |project, _| assert!(!project.is_shared()));
5776 for (guest, guest_project, mut guest_cx, guest_err) in clients {
5777 if let Some(guest_err) = guest_err {
5778 log::error!("{} error - {:?}", guest.username, guest_err);
5779 }
5780
5781 guest_project.read_with(&guest_cx, |project, _| assert!(project.is_read_only()));
5782 guest_cx.update(|cx| {
5783 cx.clear_globals();
5784 drop((guest, guest_project));
5785 });
5786 }
5787 host_cx.update(|cx| {
5788 cx.clear_globals();
5789 drop((host, host_project));
5790 });
5791
5792 return;
5793 }
5794
5795 let distribution = rng.lock().gen_range(0..100);
5796 match distribution {
5797 0..=19 if !available_guests.is_empty() => {
5798 let guest_ix = rng.lock().gen_range(0..available_guests.len());
5799 let guest_username = available_guests.remove(guest_ix);
5800 log::info!("Adding new connection for {}", guest_username);
5801 next_entity_id += 100000;
5802 let mut guest_cx = TestAppContext::new(
5803 cx.foreground_platform(),
5804 cx.platform(),
5805 deterministic.build_foreground(next_entity_id),
5806 deterministic.build_background(),
5807 cx.font_cache(),
5808 cx.leak_detector(),
5809 next_entity_id,
5810 cx.function_name.clone(),
5811 );
5812
5813 deterministic.start_waiting();
5814 let guest = server.create_client(&mut guest_cx, &guest_username).await;
5815 let guest_user_id = guest.current_user_id(&guest_cx);
5816
5817 active_call
5818 .update(cx, |call, cx| {
5819 call.invite(guest_user_id.to_proto(), None, cx)
5820 })
5821 .await
5822 .unwrap();
5823 deterministic.run_until_parked();
5824 guest_cx
5825 .read(ActiveCall::global)
5826 .update(&mut guest_cx, |call, cx| call.accept_incoming(cx))
5827 .await
5828 .unwrap();
5829
5830 let guest_project = Project::remote(
5831 host_project_id,
5832 guest.client.clone(),
5833 guest.user_store.clone(),
5834 guest.project_store.clone(),
5835 guest_lang_registry.clone(),
5836 FakeFs::new(cx.background()),
5837 guest_cx.to_async(),
5838 )
5839 .await
5840 .unwrap();
5841 deterministic.finish_waiting();
5842
5843 let op_start_signal = futures::channel::mpsc::unbounded();
5844 user_ids.push(guest_user_id);
5845 op_start_signals.push(op_start_signal.0);
5846 clients.push(guest_cx.foreground().spawn(guest.simulate_guest(
5847 guest_username.clone(),
5848 guest_project,
5849 op_start_signal.1,
5850 rng.clone(),
5851 guest_cx,
5852 )));
5853
5854 log::info!("Added connection for {}", guest_username);
5855 operations += 1;
5856 }
5857 20..=29 if clients.len() > 1 => {
5858 let guest_ix = rng.lock().gen_range(1..clients.len());
5859 log::info!("Removing guest {}", user_ids[guest_ix]);
5860 let removed_guest_id = user_ids.remove(guest_ix);
5861 let guest = clients.remove(guest_ix);
5862 op_start_signals.remove(guest_ix);
5863 server.forbid_connections();
5864 server.disconnect_client(removed_guest_id);
5865 deterministic.advance_clock(RECEIVE_TIMEOUT);
5866 deterministic.start_waiting();
5867 log::info!("Waiting for guest {} to exit...", removed_guest_id);
5868 let (guest, guest_project, mut guest_cx, guest_err) = guest.await;
5869 deterministic.finish_waiting();
5870 server.allow_connections();
5871
5872 if let Some(guest_err) = guest_err {
5873 log::error!("{} error - {:?}", guest.username, guest_err);
5874 }
5875 guest_project.read_with(&guest_cx, |project, _| assert!(project.is_read_only()));
5876 for user_id in &user_ids {
5877 let contacts = server.app_state.db.get_contacts(*user_id).await.unwrap();
5878 let contacts = server
5879 .store
5880 .lock()
5881 .await
5882 .build_initial_contacts_update(contacts)
5883 .contacts;
5884 for contact in contacts {
5885 if contact.online {
5886 assert_ne!(
5887 contact.user_id, removed_guest_id.0 as u64,
5888 "removed guest is still a contact of another peer"
5889 );
5890 }
5891 }
5892 }
5893
5894 log::info!("{} removed", guest.username);
5895 available_guests.push(guest.username.clone());
5896 guest_cx.update(|cx| {
5897 cx.clear_globals();
5898 drop((guest, guest_project));
5899 });
5900
5901 operations += 1;
5902 }
5903 _ => {
5904 while operations < max_operations && rng.lock().gen_bool(0.7) {
5905 op_start_signals
5906 .choose(&mut *rng.lock())
5907 .unwrap()
5908 .unbounded_send(())
5909 .unwrap();
5910 operations += 1;
5911 }
5912
5913 if rng.lock().gen_bool(0.8) {
5914 deterministic.run_until_parked();
5915 }
5916 }
5917 }
5918 }
5919
5920 drop(op_start_signals);
5921 deterministic.start_waiting();
5922 let mut clients = futures::future::join_all(clients).await;
5923 deterministic.finish_waiting();
5924 deterministic.run_until_parked();
5925
5926 let (host_client, host_project, mut host_cx, host_err) = clients.remove(0);
5927 if let Some(host_err) = host_err {
5928 panic!("host error - {:?}", host_err);
5929 }
5930 let host_worktree_snapshots = host_project.read_with(&host_cx, |project, cx| {
5931 project
5932 .worktrees(cx)
5933 .map(|worktree| {
5934 let snapshot = worktree.read(cx).snapshot();
5935 (snapshot.id(), snapshot)
5936 })
5937 .collect::<BTreeMap<_, _>>()
5938 });
5939
5940 host_project.read_with(&host_cx, |project, cx| project.check_invariants(cx));
5941
5942 for (guest_client, guest_project, mut guest_cx, guest_err) in clients.into_iter() {
5943 if let Some(guest_err) = guest_err {
5944 panic!("{} error - {:?}", guest_client.username, guest_err);
5945 }
5946 let worktree_snapshots = guest_project.read_with(&guest_cx, |project, cx| {
5947 project
5948 .worktrees(cx)
5949 .map(|worktree| {
5950 let worktree = worktree.read(cx);
5951 (worktree.id(), worktree.snapshot())
5952 })
5953 .collect::<BTreeMap<_, _>>()
5954 });
5955
5956 assert_eq!(
5957 worktree_snapshots.keys().collect::<Vec<_>>(),
5958 host_worktree_snapshots.keys().collect::<Vec<_>>(),
5959 "{} has different worktrees than the host",
5960 guest_client.username
5961 );
5962 for (id, host_snapshot) in &host_worktree_snapshots {
5963 let guest_snapshot = &worktree_snapshots[id];
5964 assert_eq!(
5965 guest_snapshot.root_name(),
5966 host_snapshot.root_name(),
5967 "{} has different root name than the host for worktree {}",
5968 guest_client.username,
5969 id
5970 );
5971 assert_eq!(
5972 guest_snapshot.entries(false).collect::<Vec<_>>(),
5973 host_snapshot.entries(false).collect::<Vec<_>>(),
5974 "{} has different snapshot than the host for worktree {}",
5975 guest_client.username,
5976 id
5977 );
5978 assert_eq!(guest_snapshot.scan_id(), host_snapshot.scan_id());
5979 }
5980
5981 guest_project.read_with(&guest_cx, |project, cx| project.check_invariants(cx));
5982
5983 for guest_buffer in &guest_client.buffers {
5984 let buffer_id = guest_buffer.read_with(&guest_cx, |buffer, _| buffer.remote_id());
5985 let host_buffer = host_project.read_with(&host_cx, |project, cx| {
5986 project.buffer_for_id(buffer_id, cx).unwrap_or_else(|| {
5987 panic!(
5988 "host does not have buffer for guest:{}, peer:{}, id:{}",
5989 guest_client.username, guest_client.peer_id, buffer_id
5990 )
5991 })
5992 });
5993 let path =
5994 host_buffer.read_with(&host_cx, |buffer, cx| buffer.file().unwrap().full_path(cx));
5995
5996 assert_eq!(
5997 guest_buffer.read_with(&guest_cx, |buffer, _| buffer.deferred_ops_len()),
5998 0,
5999 "{}, buffer {}, path {:?} has deferred operations",
6000 guest_client.username,
6001 buffer_id,
6002 path,
6003 );
6004 assert_eq!(
6005 guest_buffer.read_with(&guest_cx, |buffer, _| buffer.text()),
6006 host_buffer.read_with(&host_cx, |buffer, _| buffer.text()),
6007 "{}, buffer {}, path {:?}, differs from the host's buffer",
6008 guest_client.username,
6009 buffer_id,
6010 path
6011 );
6012 }
6013
6014 guest_cx.update(|cx| {
6015 cx.clear_globals();
6016 drop((guest_project, guest_client));
6017 });
6018 }
6019
6020 host_cx.update(|cx| {
6021 cx.clear_globals();
6022 drop((host_client, host_project))
6023 });
6024}
6025
6026struct TestServer {
6027 peer: Arc<Peer>,
6028 app_state: Arc<AppState>,
6029 server: Arc<Server>,
6030 foreground: Rc<executor::Foreground>,
6031 notifications: mpsc::UnboundedReceiver<()>,
6032 connection_killers: Arc<Mutex<HashMap<UserId, Arc<AtomicBool>>>>,
6033 forbid_connections: Arc<AtomicBool>,
6034 _test_db: TestDb,
6035}
6036
6037impl TestServer {
6038 async fn start(
6039 foreground: Rc<executor::Foreground>,
6040 background: Arc<executor::Background>,
6041 ) -> Self {
6042 let test_db = TestDb::fake(background.clone());
6043 let app_state = Self::build_app_state(&test_db).await;
6044 let peer = Peer::new();
6045 let notifications = mpsc::unbounded();
6046 let server = Server::new(app_state.clone(), Some(notifications.0));
6047 Self {
6048 peer,
6049 app_state,
6050 server,
6051 foreground,
6052 notifications: notifications.1,
6053 connection_killers: Default::default(),
6054 forbid_connections: Default::default(),
6055 _test_db: test_db,
6056 }
6057 }
6058
6059 async fn create_client(&mut self, cx: &mut TestAppContext, name: &str) -> TestClient {
6060 cx.update(|cx| {
6061 let mut settings = Settings::test(cx);
6062 settings.projects_online_by_default = false;
6063 cx.set_global(settings);
6064 });
6065
6066 let http = FakeHttpClient::with_404_response();
6067 let user_id = if let Ok(Some(user)) = self
6068 .app_state
6069 .db
6070 .get_user_by_github_account(name, None)
6071 .await
6072 {
6073 user.id
6074 } else {
6075 self.app_state
6076 .db
6077 .create_user(
6078 &format!("{name}@example.com"),
6079 false,
6080 NewUserParams {
6081 github_login: name.into(),
6082 github_user_id: 0,
6083 invite_count: 0,
6084 },
6085 )
6086 .await
6087 .unwrap()
6088 .user_id
6089 };
6090 let client_name = name.to_string();
6091 let mut client = cx.read(|cx| Client::new(http.clone(), cx));
6092 let server = self.server.clone();
6093 let db = self.app_state.db.clone();
6094 let connection_killers = self.connection_killers.clone();
6095 let forbid_connections = self.forbid_connections.clone();
6096 let (connection_id_tx, mut connection_id_rx) = mpsc::channel(16);
6097
6098 Arc::get_mut(&mut client)
6099 .unwrap()
6100 .set_id(user_id.0 as usize)
6101 .override_authenticate(move |cx| {
6102 cx.spawn(|_| async move {
6103 let access_token = "the-token".to_string();
6104 Ok(Credentials {
6105 user_id: user_id.0 as u64,
6106 access_token,
6107 })
6108 })
6109 })
6110 .override_establish_connection(move |credentials, cx| {
6111 assert_eq!(credentials.user_id, user_id.0 as u64);
6112 assert_eq!(credentials.access_token, "the-token");
6113
6114 let server = server.clone();
6115 let db = db.clone();
6116 let connection_killers = connection_killers.clone();
6117 let forbid_connections = forbid_connections.clone();
6118 let client_name = client_name.clone();
6119 let connection_id_tx = connection_id_tx.clone();
6120 cx.spawn(move |cx| async move {
6121 if forbid_connections.load(SeqCst) {
6122 Err(EstablishConnectionError::other(anyhow!(
6123 "server is forbidding connections"
6124 )))
6125 } else {
6126 let (client_conn, server_conn, killed) =
6127 Connection::in_memory(cx.background());
6128 connection_killers.lock().insert(user_id, killed);
6129 let user = db.get_user_by_id(user_id).await.unwrap().unwrap();
6130 cx.background()
6131 .spawn(server.handle_connection(
6132 server_conn,
6133 client_name,
6134 user,
6135 Some(connection_id_tx),
6136 cx.background(),
6137 ))
6138 .detach();
6139 Ok(client_conn)
6140 }
6141 })
6142 });
6143
6144 let fs = FakeFs::new(cx.background());
6145 let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
6146 let project_store = cx.add_model(|_| ProjectStore::new());
6147 let app_state = Arc::new(workspace::AppState {
6148 client: client.clone(),
6149 user_store: user_store.clone(),
6150 project_store: project_store.clone(),
6151 languages: Arc::new(LanguageRegistry::new(Task::ready(()))),
6152 themes: ThemeRegistry::new((), cx.font_cache()),
6153 fs: fs.clone(),
6154 build_window_options: Default::default,
6155 initialize_workspace: |_, _, _| unimplemented!(),
6156 default_item_factory: |_, _| unimplemented!(),
6157 });
6158
6159 Channel::init(&client);
6160 Project::init(&client);
6161 cx.update(|cx| {
6162 workspace::init(app_state.clone(), cx);
6163 call::init(client.clone(), user_store.clone(), cx);
6164 });
6165
6166 client
6167 .authenticate_and_connect(false, &cx.to_async())
6168 .await
6169 .unwrap();
6170 let peer_id = PeerId(connection_id_rx.next().await.unwrap().0);
6171
6172 let client = TestClient {
6173 client,
6174 peer_id,
6175 username: name.to_string(),
6176 user_store,
6177 project_store,
6178 fs,
6179 language_registry: Arc::new(LanguageRegistry::test()),
6180 buffers: Default::default(),
6181 };
6182 client.wait_for_current_user(cx).await;
6183 client
6184 }
6185
6186 fn disconnect_client(&self, user_id: UserId) {
6187 self.connection_killers
6188 .lock()
6189 .remove(&user_id)
6190 .unwrap()
6191 .store(true, SeqCst);
6192 }
6193
6194 fn forbid_connections(&self) {
6195 self.forbid_connections.store(true, SeqCst);
6196 }
6197
6198 fn allow_connections(&self) {
6199 self.forbid_connections.store(false, SeqCst);
6200 }
6201
6202 async fn make_contacts(&self, clients: &mut [(&TestClient, &mut TestAppContext)]) {
6203 for ix in 1..clients.len() {
6204 let (left, right) = clients.split_at_mut(ix);
6205 let (client_a, cx_a) = left.last_mut().unwrap();
6206 for (client_b, cx_b) in right {
6207 client_a
6208 .user_store
6209 .update(*cx_a, |store, cx| {
6210 store.request_contact(client_b.user_id().unwrap(), cx)
6211 })
6212 .await
6213 .unwrap();
6214 cx_a.foreground().run_until_parked();
6215 client_b
6216 .user_store
6217 .update(*cx_b, |store, cx| {
6218 store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx)
6219 })
6220 .await
6221 .unwrap();
6222 }
6223 }
6224 }
6225
6226 async fn create_room(&self, clients: &mut [(&TestClient, &mut TestAppContext)]) {
6227 self.make_contacts(clients).await;
6228
6229 let (left, right) = clients.split_at_mut(1);
6230 let (_client_a, cx_a) = &mut left[0];
6231 let active_call_a = cx_a.read(ActiveCall::global);
6232
6233 for (client_b, cx_b) in right {
6234 let user_id_b = client_b.current_user_id(*cx_b).to_proto();
6235 active_call_a
6236 .update(*cx_a, |call, cx| call.invite(user_id_b, None, cx))
6237 .await
6238 .unwrap();
6239
6240 cx_b.foreground().run_until_parked();
6241 let active_call_b = cx_b.read(ActiveCall::global);
6242 active_call_b
6243 .update(*cx_b, |call, cx| call.accept_incoming(cx))
6244 .await
6245 .unwrap();
6246 }
6247 }
6248
6249 async fn build_app_state(test_db: &TestDb) -> Arc<AppState> {
6250 Arc::new(AppState {
6251 db: test_db.db().clone(),
6252 api_token: Default::default(),
6253 invite_link_prefix: Default::default(),
6254 })
6255 }
6256
6257 async fn condition<F>(&mut self, mut predicate: F)
6258 where
6259 F: FnMut(&Store) -> bool,
6260 {
6261 assert!(
6262 self.foreground.parking_forbidden(),
6263 "you must call forbid_parking to use server conditions so we don't block indefinitely"
6264 );
6265 while !(predicate)(&*self.server.store.lock().await) {
6266 self.foreground.start_waiting();
6267 self.notifications.next().await;
6268 self.foreground.finish_waiting();
6269 }
6270 }
6271}
6272
6273impl Deref for TestServer {
6274 type Target = Server;
6275
6276 fn deref(&self) -> &Self::Target {
6277 &self.server
6278 }
6279}
6280
6281impl Drop for TestServer {
6282 fn drop(&mut self) {
6283 self.peer.reset();
6284 }
6285}
6286
6287struct TestClient {
6288 client: Arc<Client>,
6289 username: String,
6290 pub peer_id: PeerId,
6291 pub user_store: ModelHandle<UserStore>,
6292 pub project_store: ModelHandle<ProjectStore>,
6293 language_registry: Arc<LanguageRegistry>,
6294 fs: Arc<FakeFs>,
6295 buffers: HashSet<ModelHandle<language::Buffer>>,
6296}
6297
6298impl Deref for TestClient {
6299 type Target = Arc<Client>;
6300
6301 fn deref(&self) -> &Self::Target {
6302 &self.client
6303 }
6304}
6305
6306struct ContactsSummary {
6307 pub current: Vec<String>,
6308 pub outgoing_requests: Vec<String>,
6309 pub incoming_requests: Vec<String>,
6310}
6311
6312impl TestClient {
6313 pub fn current_user_id(&self, cx: &TestAppContext) -> UserId {
6314 UserId::from_proto(
6315 self.user_store
6316 .read_with(cx, |user_store, _| user_store.current_user().unwrap().id),
6317 )
6318 }
6319
6320 async fn wait_for_current_user(&self, cx: &TestAppContext) {
6321 let mut authed_user = self
6322 .user_store
6323 .read_with(cx, |user_store, _| user_store.watch_current_user());
6324 while authed_user.next().await.unwrap().is_none() {}
6325 }
6326
6327 async fn clear_contacts(&self, cx: &mut TestAppContext) {
6328 self.user_store
6329 .update(cx, |store, _| store.clear_contacts())
6330 .await;
6331 }
6332
6333 fn summarize_contacts(&self, cx: &TestAppContext) -> ContactsSummary {
6334 self.user_store.read_with(cx, |store, _| ContactsSummary {
6335 current: store
6336 .contacts()
6337 .iter()
6338 .map(|contact| contact.user.github_login.clone())
6339 .collect(),
6340 outgoing_requests: store
6341 .outgoing_contact_requests()
6342 .iter()
6343 .map(|user| user.github_login.clone())
6344 .collect(),
6345 incoming_requests: store
6346 .incoming_contact_requests()
6347 .iter()
6348 .map(|user| user.github_login.clone())
6349 .collect(),
6350 })
6351 }
6352
6353 async fn build_local_project(
6354 &self,
6355 root_path: impl AsRef<Path>,
6356 cx: &mut TestAppContext,
6357 ) -> (ModelHandle<Project>, WorktreeId) {
6358 let project = cx.update(|cx| {
6359 Project::local(
6360 self.client.clone(),
6361 self.user_store.clone(),
6362 self.project_store.clone(),
6363 self.language_registry.clone(),
6364 self.fs.clone(),
6365 cx,
6366 )
6367 });
6368 let (worktree, _) = project
6369 .update(cx, |p, cx| {
6370 p.find_or_create_local_worktree(root_path, true, cx)
6371 })
6372 .await
6373 .unwrap();
6374 worktree
6375 .read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete())
6376 .await;
6377 (project, worktree.read_with(cx, |tree, _| tree.id()))
6378 }
6379
6380 async fn build_remote_project(
6381 &self,
6382 host_project_id: u64,
6383 guest_cx: &mut TestAppContext,
6384 ) -> ModelHandle<Project> {
6385 let project_b = guest_cx.spawn(|cx| {
6386 Project::remote(
6387 host_project_id,
6388 self.client.clone(),
6389 self.user_store.clone(),
6390 self.project_store.clone(),
6391 self.language_registry.clone(),
6392 FakeFs::new(cx.background()),
6393 cx,
6394 )
6395 });
6396 project_b.await.unwrap()
6397 }
6398
6399 fn build_workspace(
6400 &self,
6401 project: &ModelHandle<Project>,
6402 cx: &mut TestAppContext,
6403 ) -> ViewHandle<Workspace> {
6404 let (_, root_view) = cx.add_window(|_| EmptyView);
6405 cx.add_view(&root_view, |cx| {
6406 Workspace::new(project.clone(), |_, _| unimplemented!(), cx)
6407 })
6408 }
6409
6410 async fn simulate_host(
6411 mut self,
6412 project: ModelHandle<Project>,
6413 op_start_signal: futures::channel::mpsc::UnboundedReceiver<()>,
6414 rng: Arc<Mutex<StdRng>>,
6415 mut cx: TestAppContext,
6416 ) -> (
6417 Self,
6418 ModelHandle<Project>,
6419 TestAppContext,
6420 Option<anyhow::Error>,
6421 ) {
6422 async fn simulate_host_internal(
6423 client: &mut TestClient,
6424 project: ModelHandle<Project>,
6425 mut op_start_signal: futures::channel::mpsc::UnboundedReceiver<()>,
6426 rng: Arc<Mutex<StdRng>>,
6427 cx: &mut TestAppContext,
6428 ) -> anyhow::Result<()> {
6429 let fs = project.read_with(cx, |project, _| project.fs().clone());
6430
6431 while op_start_signal.next().await.is_some() {
6432 let distribution = rng.lock().gen_range::<usize, _>(0..100);
6433 let files = fs.as_fake().files().await;
6434 match distribution {
6435 0..=19 if !files.is_empty() => {
6436 let path = files.choose(&mut *rng.lock()).unwrap();
6437 let mut path = path.as_path();
6438 while let Some(parent_path) = path.parent() {
6439 path = parent_path;
6440 if rng.lock().gen() {
6441 break;
6442 }
6443 }
6444
6445 log::info!("Host: find/create local worktree {:?}", path);
6446 let find_or_create_worktree = project.update(cx, |project, cx| {
6447 project.find_or_create_local_worktree(path, true, cx)
6448 });
6449 if rng.lock().gen() {
6450 cx.background().spawn(find_or_create_worktree).detach();
6451 } else {
6452 find_or_create_worktree.await?;
6453 }
6454 }
6455 20..=79 if !files.is_empty() => {
6456 let buffer = if client.buffers.is_empty() || rng.lock().gen() {
6457 let file = files.choose(&mut *rng.lock()).unwrap();
6458 let (worktree, path) = project
6459 .update(cx, |project, cx| {
6460 project.find_or_create_local_worktree(file.clone(), true, cx)
6461 })
6462 .await?;
6463 let project_path =
6464 worktree.read_with(cx, |worktree, _| (worktree.id(), path));
6465 log::info!(
6466 "Host: opening path {:?}, worktree {}, relative_path {:?}",
6467 file,
6468 project_path.0,
6469 project_path.1
6470 );
6471 let buffer = project
6472 .update(cx, |project, cx| project.open_buffer(project_path, cx))
6473 .await
6474 .unwrap();
6475 client.buffers.insert(buffer.clone());
6476 buffer
6477 } else {
6478 client
6479 .buffers
6480 .iter()
6481 .choose(&mut *rng.lock())
6482 .unwrap()
6483 .clone()
6484 };
6485
6486 if rng.lock().gen_bool(0.1) {
6487 cx.update(|cx| {
6488 log::info!(
6489 "Host: dropping buffer {:?}",
6490 buffer.read(cx).file().unwrap().full_path(cx)
6491 );
6492 client.buffers.remove(&buffer);
6493 drop(buffer);
6494 });
6495 } else {
6496 buffer.update(cx, |buffer, cx| {
6497 log::info!(
6498 "Host: updating buffer {:?} ({})",
6499 buffer.file().unwrap().full_path(cx),
6500 buffer.remote_id()
6501 );
6502
6503 if rng.lock().gen_bool(0.7) {
6504 buffer.randomly_edit(&mut *rng.lock(), 5, cx);
6505 } else {
6506 buffer.randomly_undo_redo(&mut *rng.lock(), cx);
6507 }
6508 });
6509 }
6510 }
6511 _ => loop {
6512 let path_component_count = rng.lock().gen_range::<usize, _>(1..=5);
6513 let mut path = PathBuf::new();
6514 path.push("/");
6515 for _ in 0..path_component_count {
6516 let letter = rng.lock().gen_range(b'a'..=b'z');
6517 path.push(std::str::from_utf8(&[letter]).unwrap());
6518 }
6519 path.set_extension("rs");
6520 let parent_path = path.parent().unwrap();
6521
6522 log::info!("Host: creating file {:?}", path,);
6523
6524 if fs.create_dir(parent_path).await.is_ok()
6525 && fs.create_file(&path, Default::default()).await.is_ok()
6526 {
6527 break;
6528 } else {
6529 log::info!("Host: cannot create file");
6530 }
6531 },
6532 }
6533
6534 cx.background().simulate_random_delay().await;
6535 }
6536
6537 Ok(())
6538 }
6539
6540 let result =
6541 simulate_host_internal(&mut self, project.clone(), op_start_signal, rng, &mut cx).await;
6542 log::info!("Host done");
6543 (self, project, cx, result.err())
6544 }
6545
6546 pub async fn simulate_guest(
6547 mut self,
6548 guest_username: String,
6549 project: ModelHandle<Project>,
6550 op_start_signal: futures::channel::mpsc::UnboundedReceiver<()>,
6551 rng: Arc<Mutex<StdRng>>,
6552 mut cx: TestAppContext,
6553 ) -> (
6554 Self,
6555 ModelHandle<Project>,
6556 TestAppContext,
6557 Option<anyhow::Error>,
6558 ) {
6559 async fn simulate_guest_internal(
6560 client: &mut TestClient,
6561 guest_username: &str,
6562 project: ModelHandle<Project>,
6563 mut op_start_signal: futures::channel::mpsc::UnboundedReceiver<()>,
6564 rng: Arc<Mutex<StdRng>>,
6565 cx: &mut TestAppContext,
6566 ) -> anyhow::Result<()> {
6567 while op_start_signal.next().await.is_some() {
6568 let buffer = if client.buffers.is_empty() || rng.lock().gen() {
6569 let worktree = if let Some(worktree) = project.read_with(cx, |project, cx| {
6570 project
6571 .worktrees(cx)
6572 .filter(|worktree| {
6573 let worktree = worktree.read(cx);
6574 worktree.is_visible()
6575 && worktree.entries(false).any(|e| e.is_file())
6576 })
6577 .choose(&mut *rng.lock())
6578 }) {
6579 worktree
6580 } else {
6581 cx.background().simulate_random_delay().await;
6582 continue;
6583 };
6584
6585 let (worktree_root_name, project_path) =
6586 worktree.read_with(cx, |worktree, _| {
6587 let entry = worktree
6588 .entries(false)
6589 .filter(|e| e.is_file())
6590 .choose(&mut *rng.lock())
6591 .unwrap();
6592 (
6593 worktree.root_name().to_string(),
6594 (worktree.id(), entry.path.clone()),
6595 )
6596 });
6597 log::info!(
6598 "{}: opening path {:?} in worktree {} ({})",
6599 guest_username,
6600 project_path.1,
6601 project_path.0,
6602 worktree_root_name,
6603 );
6604 let buffer = project
6605 .update(cx, |project, cx| {
6606 project.open_buffer(project_path.clone(), cx)
6607 })
6608 .await?;
6609 log::info!(
6610 "{}: opened path {:?} in worktree {} ({}) with buffer id {}",
6611 guest_username,
6612 project_path.1,
6613 project_path.0,
6614 worktree_root_name,
6615 buffer.read_with(cx, |buffer, _| buffer.remote_id())
6616 );
6617 client.buffers.insert(buffer.clone());
6618 buffer
6619 } else {
6620 client
6621 .buffers
6622 .iter()
6623 .choose(&mut *rng.lock())
6624 .unwrap()
6625 .clone()
6626 };
6627
6628 let choice = rng.lock().gen_range(0..100);
6629 match choice {
6630 0..=9 => {
6631 cx.update(|cx| {
6632 log::info!(
6633 "{}: dropping buffer {:?}",
6634 guest_username,
6635 buffer.read(cx).file().unwrap().full_path(cx)
6636 );
6637 client.buffers.remove(&buffer);
6638 drop(buffer);
6639 });
6640 }
6641 10..=19 => {
6642 let completions = project.update(cx, |project, cx| {
6643 log::info!(
6644 "{}: requesting completions for buffer {} ({:?})",
6645 guest_username,
6646 buffer.read(cx).remote_id(),
6647 buffer.read(cx).file().unwrap().full_path(cx)
6648 );
6649 let offset = rng.lock().gen_range(0..=buffer.read(cx).len());
6650 project.completions(&buffer, offset, cx)
6651 });
6652 let completions = cx.background().spawn(async move {
6653 completions
6654 .await
6655 .map_err(|err| anyhow!("completions request failed: {:?}", err))
6656 });
6657 if rng.lock().gen_bool(0.3) {
6658 log::info!("{}: detaching completions request", guest_username);
6659 cx.update(|cx| completions.detach_and_log_err(cx));
6660 } else {
6661 completions.await?;
6662 }
6663 }
6664 20..=29 => {
6665 let code_actions = project.update(cx, |project, cx| {
6666 log::info!(
6667 "{}: requesting code actions for buffer {} ({:?})",
6668 guest_username,
6669 buffer.read(cx).remote_id(),
6670 buffer.read(cx).file().unwrap().full_path(cx)
6671 );
6672 let range = buffer.read(cx).random_byte_range(0, &mut *rng.lock());
6673 project.code_actions(&buffer, range, cx)
6674 });
6675 let code_actions = cx.background().spawn(async move {
6676 code_actions
6677 .await
6678 .map_err(|err| anyhow!("code actions request failed: {:?}", err))
6679 });
6680 if rng.lock().gen_bool(0.3) {
6681 log::info!("{}: detaching code actions request", guest_username);
6682 cx.update(|cx| code_actions.detach_and_log_err(cx));
6683 } else {
6684 code_actions.await?;
6685 }
6686 }
6687 30..=39 if buffer.read_with(cx, |buffer, _| buffer.is_dirty()) => {
6688 let (requested_version, save) = buffer.update(cx, |buffer, cx| {
6689 log::info!(
6690 "{}: saving buffer {} ({:?})",
6691 guest_username,
6692 buffer.remote_id(),
6693 buffer.file().unwrap().full_path(cx)
6694 );
6695 (buffer.version(), buffer.save(cx))
6696 });
6697 let save = cx.background().spawn(async move {
6698 let (saved_version, _, _) = save
6699 .await
6700 .map_err(|err| anyhow!("save request failed: {:?}", err))?;
6701 assert!(saved_version.observed_all(&requested_version));
6702 Ok::<_, anyhow::Error>(())
6703 });
6704 if rng.lock().gen_bool(0.3) {
6705 log::info!("{}: detaching save request", guest_username);
6706 cx.update(|cx| save.detach_and_log_err(cx));
6707 } else {
6708 save.await?;
6709 }
6710 }
6711 40..=44 => {
6712 let prepare_rename = project.update(cx, |project, cx| {
6713 log::info!(
6714 "{}: preparing rename for buffer {} ({:?})",
6715 guest_username,
6716 buffer.read(cx).remote_id(),
6717 buffer.read(cx).file().unwrap().full_path(cx)
6718 );
6719 let offset = rng.lock().gen_range(0..=buffer.read(cx).len());
6720 project.prepare_rename(buffer, offset, cx)
6721 });
6722 let prepare_rename = cx.background().spawn(async move {
6723 prepare_rename
6724 .await
6725 .map_err(|err| anyhow!("prepare rename request failed: {:?}", err))
6726 });
6727 if rng.lock().gen_bool(0.3) {
6728 log::info!("{}: detaching prepare rename request", guest_username);
6729 cx.update(|cx| prepare_rename.detach_and_log_err(cx));
6730 } else {
6731 prepare_rename.await?;
6732 }
6733 }
6734 45..=49 => {
6735 let definitions = project.update(cx, |project, cx| {
6736 log::info!(
6737 "{}: requesting definitions for buffer {} ({:?})",
6738 guest_username,
6739 buffer.read(cx).remote_id(),
6740 buffer.read(cx).file().unwrap().full_path(cx)
6741 );
6742 let offset = rng.lock().gen_range(0..=buffer.read(cx).len());
6743 project.definition(&buffer, offset, cx)
6744 });
6745 let definitions = cx.background().spawn(async move {
6746 definitions
6747 .await
6748 .map_err(|err| anyhow!("definitions request failed: {:?}", err))
6749 });
6750 if rng.lock().gen_bool(0.3) {
6751 log::info!("{}: detaching definitions request", guest_username);
6752 cx.update(|cx| definitions.detach_and_log_err(cx));
6753 } else {
6754 client.buffers.extend(
6755 definitions.await?.into_iter().map(|loc| loc.target.buffer),
6756 );
6757 }
6758 }
6759 50..=54 => {
6760 let highlights = project.update(cx, |project, cx| {
6761 log::info!(
6762 "{}: requesting highlights for buffer {} ({:?})",
6763 guest_username,
6764 buffer.read(cx).remote_id(),
6765 buffer.read(cx).file().unwrap().full_path(cx)
6766 );
6767 let offset = rng.lock().gen_range(0..=buffer.read(cx).len());
6768 project.document_highlights(&buffer, offset, cx)
6769 });
6770 let highlights = cx.background().spawn(async move {
6771 highlights
6772 .await
6773 .map_err(|err| anyhow!("highlights request failed: {:?}", err))
6774 });
6775 if rng.lock().gen_bool(0.3) {
6776 log::info!("{}: detaching highlights request", guest_username);
6777 cx.update(|cx| highlights.detach_and_log_err(cx));
6778 } else {
6779 highlights.await?;
6780 }
6781 }
6782 55..=59 => {
6783 let search = project.update(cx, |project, cx| {
6784 let query = rng.lock().gen_range('a'..='z');
6785 log::info!("{}: project-wide search {:?}", guest_username, query);
6786 project.search(SearchQuery::text(query, false, false), cx)
6787 });
6788 let search = cx.background().spawn(async move {
6789 search
6790 .await
6791 .map_err(|err| anyhow!("search request failed: {:?}", err))
6792 });
6793 if rng.lock().gen_bool(0.3) {
6794 log::info!("{}: detaching search request", guest_username);
6795 cx.update(|cx| search.detach_and_log_err(cx));
6796 } else {
6797 client.buffers.extend(search.await?.into_keys());
6798 }
6799 }
6800 60..=69 => {
6801 let worktree = project
6802 .read_with(cx, |project, cx| {
6803 project
6804 .worktrees(cx)
6805 .filter(|worktree| {
6806 let worktree = worktree.read(cx);
6807 worktree.is_visible()
6808 && worktree.entries(false).any(|e| e.is_file())
6809 && worktree.root_entry().map_or(false, |e| e.is_dir())
6810 })
6811 .choose(&mut *rng.lock())
6812 })
6813 .unwrap();
6814 let (worktree_id, worktree_root_name) = worktree
6815 .read_with(cx, |worktree, _| {
6816 (worktree.id(), worktree.root_name().to_string())
6817 });
6818
6819 let mut new_name = String::new();
6820 for _ in 0..10 {
6821 let letter = rng.lock().gen_range('a'..='z');
6822 new_name.push(letter);
6823 }
6824 let mut new_path = PathBuf::new();
6825 new_path.push(new_name);
6826 new_path.set_extension("rs");
6827 log::info!(
6828 "{}: creating {:?} in worktree {} ({})",
6829 guest_username,
6830 new_path,
6831 worktree_id,
6832 worktree_root_name,
6833 );
6834 project
6835 .update(cx, |project, cx| {
6836 project.create_entry((worktree_id, new_path), false, cx)
6837 })
6838 .unwrap()
6839 .await?;
6840 }
6841 _ => {
6842 buffer.update(cx, |buffer, cx| {
6843 log::info!(
6844 "{}: updating buffer {} ({:?})",
6845 guest_username,
6846 buffer.remote_id(),
6847 buffer.file().unwrap().full_path(cx)
6848 );
6849 if rng.lock().gen_bool(0.7) {
6850 buffer.randomly_edit(&mut *rng.lock(), 5, cx);
6851 } else {
6852 buffer.randomly_undo_redo(&mut *rng.lock(), cx);
6853 }
6854 });
6855 }
6856 }
6857 cx.background().simulate_random_delay().await;
6858 }
6859 Ok(())
6860 }
6861
6862 let result = simulate_guest_internal(
6863 &mut self,
6864 &guest_username,
6865 project.clone(),
6866 op_start_signal,
6867 rng,
6868 &mut cx,
6869 )
6870 .await;
6871 log::info!("{}: done", guest_username);
6872
6873 (self, project, cx, result.err())
6874 }
6875}
6876
6877impl Drop for TestClient {
6878 fn drop(&mut self) {
6879 self.client.tear_down();
6880 }
6881}
6882
6883impl Executor for Arc<gpui::executor::Background> {
6884 type Sleep = gpui::executor::Timer;
6885
6886 fn spawn_detached<F: 'static + Send + Future<Output = ()>>(&self, future: F) {
6887 self.spawn(future).detach();
6888 }
6889
6890 fn sleep(&self, duration: Duration) -> Self::Sleep {
6891 self.as_ref().timer(duration)
6892 }
6893}
6894
6895fn channel_messages(channel: &Channel) -> Vec<(String, String, bool)> {
6896 channel
6897 .messages()
6898 .cursor::<()>()
6899 .map(|m| {
6900 (
6901 m.sender.github_login.clone(),
6902 m.body.clone(),
6903 m.is_pending(),
6904 )
6905 })
6906 .collect()
6907}
6908
6909#[derive(Debug, Eq, PartialEq)]
6910struct RoomParticipants {
6911 remote: Vec<String>,
6912 pending: Vec<String>,
6913}
6914
6915fn room_participants(room: &ModelHandle<Room>, cx: &mut TestAppContext) -> RoomParticipants {
6916 room.read_with(cx, |room, _| RoomParticipants {
6917 remote: room
6918 .remote_participants()
6919 .iter()
6920 .map(|(_, participant)| participant.user.github_login.clone())
6921 .collect(),
6922 pending: room
6923 .pending_participants()
6924 .iter()
6925 .map(|user| user.github_login.clone())
6926 .collect(),
6927 })
6928}