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