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