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