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