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