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