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