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