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