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