1use crate::{
2 db::{tests::TestDb, ProjectId, UserId},
3 rpc::{Executor, Server, Store},
4 AppState,
5};
6use ::rpc::Peer;
7use anyhow::anyhow;
8use client::{
9 self, proto, test::FakeHttpClient, Channel, ChannelDetails, ChannelList, Client, Connection,
10 Credentials, EstablishConnectionError, ProjectMetadata, UserStore, RECEIVE_TIMEOUT,
11};
12use collections::{BTreeMap, HashMap, HashSet};
13use editor::{
14 self, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Redo, Rename, ToOffset,
15 ToggleCodeActions, Undo,
16};
17use futures::{channel::mpsc, Future, StreamExt as _};
18use gpui::{
19 executor::{self, Deterministic},
20 geometry::vector::vec2f,
21 test::EmptyView,
22 ModelHandle, Task, TestAppContext, ViewHandle,
23};
24use language::{
25 range_to_lsp, tree_sitter_rust, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language,
26 LanguageConfig, LanguageRegistry, LineEnding, OffsetRangeExt, Point, Rope,
27};
28use lsp::{self, FakeLanguageServer};
29use parking_lot::Mutex;
30use project::{
31 fs::{FakeFs, Fs as _},
32 search::SearchQuery,
33 worktree::WorktreeHandle,
34 DiagnosticSummary, Project, ProjectPath, ProjectStore, WorktreeId,
35};
36use rand::prelude::*;
37use room::Room;
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(), 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(), 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(), 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(), 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 for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] {
3913 client.user_store.read_with(*cx, |store, _| {
3914 assert_eq!(
3915 contacts(store),
3916 [
3917 ("user_a", true, vec![]),
3918 ("user_b", true, vec![]),
3919 ("user_c", true, vec![])
3920 ],
3921 "{} has the wrong contacts",
3922 client.username
3923 )
3924 });
3925 }
3926
3927 // Share a project as client A.
3928 client_a.fs.create_dir(Path::new("/a")).await.unwrap();
3929 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
3930
3931 deterministic.run_until_parked();
3932 for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] {
3933 client.user_store.read_with(*cx, |store, _| {
3934 assert_eq!(
3935 contacts(store),
3936 [
3937 ("user_a", true, vec![("a", vec![])]),
3938 ("user_b", true, vec![]),
3939 ("user_c", true, vec![])
3940 ],
3941 "{} has the wrong contacts",
3942 client.username
3943 )
3944 });
3945 }
3946
3947 let _project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
3948
3949 deterministic.run_until_parked();
3950 for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] {
3951 client.user_store.read_with(*cx, |store, _| {
3952 assert_eq!(
3953 contacts(store),
3954 [
3955 ("user_a", true, vec![("a", vec!["user_b"])]),
3956 ("user_b", true, vec![]),
3957 ("user_c", true, vec![])
3958 ],
3959 "{} has the wrong contacts",
3960 client.username
3961 )
3962 });
3963 }
3964
3965 // Add a local project as client B
3966 client_a.fs.create_dir("/b".as_ref()).await.unwrap();
3967 let (_project_b, _) = client_b.build_local_project("/b", cx_b).await;
3968
3969 deterministic.run_until_parked();
3970 for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] {
3971 client.user_store.read_with(*cx, |store, _| {
3972 assert_eq!(
3973 contacts(store),
3974 [
3975 ("user_a", true, vec![("a", vec!["user_b"])]),
3976 ("user_b", true, vec![("b", vec![])]),
3977 ("user_c", true, vec![])
3978 ],
3979 "{} has the wrong contacts",
3980 client.username
3981 )
3982 });
3983 }
3984
3985 project_a
3986 .condition(cx_a, |project, _| {
3987 project.collaborators().contains_key(&client_b.peer_id)
3988 })
3989 .await;
3990
3991 cx_a.update(move |_| drop(project_a));
3992 deterministic.run_until_parked();
3993 for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] {
3994 client.user_store.read_with(*cx, |store, _| {
3995 assert_eq!(
3996 contacts(store),
3997 [
3998 ("user_a", true, vec![]),
3999 ("user_b", true, vec![("b", vec![])]),
4000 ("user_c", true, vec![])
4001 ],
4002 "{} has the wrong contacts",
4003 client.username
4004 )
4005 });
4006 }
4007
4008 server.disconnect_client(client_c.current_user_id(cx_c));
4009 server.forbid_connections();
4010 deterministic.advance_clock(rpc::RECEIVE_TIMEOUT);
4011 for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b)] {
4012 client.user_store.read_with(*cx, |store, _| {
4013 assert_eq!(
4014 contacts(store),
4015 [
4016 ("user_a", true, vec![]),
4017 ("user_b", true, vec![("b", vec![])]),
4018 ("user_c", false, vec![])
4019 ],
4020 "{} has the wrong contacts",
4021 client.username
4022 )
4023 });
4024 }
4025 client_c
4026 .user_store
4027 .read_with(cx_c, |store, _| assert_eq!(contacts(store), []));
4028
4029 server.allow_connections();
4030 client_c
4031 .authenticate_and_connect(false, &cx_c.to_async())
4032 .await
4033 .unwrap();
4034
4035 deterministic.run_until_parked();
4036 for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] {
4037 client.user_store.read_with(*cx, |store, _| {
4038 assert_eq!(
4039 contacts(store),
4040 [
4041 ("user_a", true, vec![]),
4042 ("user_b", true, vec![("b", vec![])]),
4043 ("user_c", true, vec![])
4044 ],
4045 "{} has the wrong contacts",
4046 client.username
4047 )
4048 });
4049 }
4050
4051 #[allow(clippy::type_complexity)]
4052 fn contacts(user_store: &UserStore) -> Vec<(&str, bool, Vec<(&str, Vec<&str>)>)> {
4053 user_store
4054 .contacts()
4055 .iter()
4056 .map(|contact| {
4057 let projects = contact
4058 .projects
4059 .iter()
4060 .map(|p| {
4061 (
4062 p.visible_worktree_root_names[0].as_str(),
4063 p.guests.iter().map(|p| p.github_login.as_str()).collect(),
4064 )
4065 })
4066 .collect();
4067 (contact.user.github_login.as_str(), contact.online, projects)
4068 })
4069 .collect()
4070 }
4071}
4072
4073#[gpui::test(iterations = 10)]
4074async fn test_contact_requests(
4075 executor: Arc<Deterministic>,
4076 cx_a: &mut TestAppContext,
4077 cx_a2: &mut TestAppContext,
4078 cx_b: &mut TestAppContext,
4079 cx_b2: &mut TestAppContext,
4080 cx_c: &mut TestAppContext,
4081 cx_c2: &mut TestAppContext,
4082) {
4083 cx_a.foreground().forbid_parking();
4084
4085 // Connect to a server as 3 clients.
4086 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
4087 let client_a = server.create_client(cx_a, "user_a").await;
4088 let client_a2 = server.create_client(cx_a2, "user_a").await;
4089 let client_b = server.create_client(cx_b, "user_b").await;
4090 let client_b2 = server.create_client(cx_b2, "user_b").await;
4091 let client_c = server.create_client(cx_c, "user_c").await;
4092 let client_c2 = server.create_client(cx_c2, "user_c").await;
4093
4094 assert_eq!(client_a.user_id().unwrap(), client_a2.user_id().unwrap());
4095 assert_eq!(client_b.user_id().unwrap(), client_b2.user_id().unwrap());
4096 assert_eq!(client_c.user_id().unwrap(), client_c2.user_id().unwrap());
4097
4098 // User A and User C request that user B become their contact.
4099 client_a
4100 .user_store
4101 .update(cx_a, |store, cx| {
4102 store.request_contact(client_b.user_id().unwrap(), cx)
4103 })
4104 .await
4105 .unwrap();
4106 client_c
4107 .user_store
4108 .update(cx_c, |store, cx| {
4109 store.request_contact(client_b.user_id().unwrap(), cx)
4110 })
4111 .await
4112 .unwrap();
4113 executor.run_until_parked();
4114
4115 // All users see the pending request appear in all their clients.
4116 assert_eq!(
4117 client_a.summarize_contacts(cx_a).outgoing_requests,
4118 &["user_b"]
4119 );
4120 assert_eq!(
4121 client_a2.summarize_contacts(cx_a2).outgoing_requests,
4122 &["user_b"]
4123 );
4124 assert_eq!(
4125 client_b.summarize_contacts(cx_b).incoming_requests,
4126 &["user_a", "user_c"]
4127 );
4128 assert_eq!(
4129 client_b2.summarize_contacts(cx_b2).incoming_requests,
4130 &["user_a", "user_c"]
4131 );
4132 assert_eq!(
4133 client_c.summarize_contacts(cx_c).outgoing_requests,
4134 &["user_b"]
4135 );
4136 assert_eq!(
4137 client_c2.summarize_contacts(cx_c2).outgoing_requests,
4138 &["user_b"]
4139 );
4140
4141 // Contact requests are present upon connecting (tested here via disconnect/reconnect)
4142 disconnect_and_reconnect(&client_a, cx_a).await;
4143 disconnect_and_reconnect(&client_b, cx_b).await;
4144 disconnect_and_reconnect(&client_c, cx_c).await;
4145 executor.run_until_parked();
4146 assert_eq!(
4147 client_a.summarize_contacts(cx_a).outgoing_requests,
4148 &["user_b"]
4149 );
4150 assert_eq!(
4151 client_b.summarize_contacts(cx_b).incoming_requests,
4152 &["user_a", "user_c"]
4153 );
4154 assert_eq!(
4155 client_c.summarize_contacts(cx_c).outgoing_requests,
4156 &["user_b"]
4157 );
4158
4159 // User B accepts the request from user A.
4160 client_b
4161 .user_store
4162 .update(cx_b, |store, cx| {
4163 store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx)
4164 })
4165 .await
4166 .unwrap();
4167
4168 executor.run_until_parked();
4169
4170 // User B sees user A as their contact now in all client, and the incoming request from them is removed.
4171 let contacts_b = client_b.summarize_contacts(cx_b);
4172 assert_eq!(contacts_b.current, &["user_a", "user_b"]);
4173 assert_eq!(contacts_b.incoming_requests, &["user_c"]);
4174 let contacts_b2 = client_b2.summarize_contacts(cx_b2);
4175 assert_eq!(contacts_b2.current, &["user_a", "user_b"]);
4176 assert_eq!(contacts_b2.incoming_requests, &["user_c"]);
4177
4178 // User A sees user B as their contact now in all clients, and the outgoing request to them is removed.
4179 let contacts_a = client_a.summarize_contacts(cx_a);
4180 assert_eq!(contacts_a.current, &["user_a", "user_b"]);
4181 assert!(contacts_a.outgoing_requests.is_empty());
4182 let contacts_a2 = client_a2.summarize_contacts(cx_a2);
4183 assert_eq!(contacts_a2.current, &["user_a", "user_b"]);
4184 assert!(contacts_a2.outgoing_requests.is_empty());
4185
4186 // Contacts are present upon connecting (tested here via disconnect/reconnect)
4187 disconnect_and_reconnect(&client_a, cx_a).await;
4188 disconnect_and_reconnect(&client_b, cx_b).await;
4189 disconnect_and_reconnect(&client_c, cx_c).await;
4190 executor.run_until_parked();
4191 assert_eq!(
4192 client_a.summarize_contacts(cx_a).current,
4193 &["user_a", "user_b"]
4194 );
4195 assert_eq!(
4196 client_b.summarize_contacts(cx_b).current,
4197 &["user_a", "user_b"]
4198 );
4199 assert_eq!(
4200 client_b.summarize_contacts(cx_b).incoming_requests,
4201 &["user_c"]
4202 );
4203 assert_eq!(client_c.summarize_contacts(cx_c).current, &["user_c"]);
4204 assert_eq!(
4205 client_c.summarize_contacts(cx_c).outgoing_requests,
4206 &["user_b"]
4207 );
4208
4209 // User B rejects the request from user C.
4210 client_b
4211 .user_store
4212 .update(cx_b, |store, cx| {
4213 store.respond_to_contact_request(client_c.user_id().unwrap(), false, cx)
4214 })
4215 .await
4216 .unwrap();
4217
4218 executor.run_until_parked();
4219
4220 // User B doesn't see user C as their contact, and the incoming request from them is removed.
4221 let contacts_b = client_b.summarize_contacts(cx_b);
4222 assert_eq!(contacts_b.current, &["user_a", "user_b"]);
4223 assert!(contacts_b.incoming_requests.is_empty());
4224 let contacts_b2 = client_b2.summarize_contacts(cx_b2);
4225 assert_eq!(contacts_b2.current, &["user_a", "user_b"]);
4226 assert!(contacts_b2.incoming_requests.is_empty());
4227
4228 // User C doesn't see user B as their contact, and the outgoing request to them is removed.
4229 let contacts_c = client_c.summarize_contacts(cx_c);
4230 assert_eq!(contacts_c.current, &["user_c"]);
4231 assert!(contacts_c.outgoing_requests.is_empty());
4232 let contacts_c2 = client_c2.summarize_contacts(cx_c2);
4233 assert_eq!(contacts_c2.current, &["user_c"]);
4234 assert!(contacts_c2.outgoing_requests.is_empty());
4235
4236 // Incoming/outgoing requests are not present upon connecting (tested here via disconnect/reconnect)
4237 disconnect_and_reconnect(&client_a, cx_a).await;
4238 disconnect_and_reconnect(&client_b, cx_b).await;
4239 disconnect_and_reconnect(&client_c, cx_c).await;
4240 executor.run_until_parked();
4241 assert_eq!(
4242 client_a.summarize_contacts(cx_a).current,
4243 &["user_a", "user_b"]
4244 );
4245 assert_eq!(
4246 client_b.summarize_contacts(cx_b).current,
4247 &["user_a", "user_b"]
4248 );
4249 assert!(client_b
4250 .summarize_contacts(cx_b)
4251 .incoming_requests
4252 .is_empty());
4253 assert_eq!(client_c.summarize_contacts(cx_c).current, &["user_c"]);
4254 assert!(client_c
4255 .summarize_contacts(cx_c)
4256 .outgoing_requests
4257 .is_empty());
4258
4259 async fn disconnect_and_reconnect(client: &TestClient, cx: &mut TestAppContext) {
4260 client.disconnect(&cx.to_async()).unwrap();
4261 client.clear_contacts(cx).await;
4262 client
4263 .authenticate_and_connect(false, &cx.to_async())
4264 .await
4265 .unwrap();
4266 }
4267}
4268
4269#[gpui::test(iterations = 10)]
4270async fn test_following(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
4271 cx_a.foreground().forbid_parking();
4272 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
4273 let client_a = server.create_client(cx_a, "user_a").await;
4274 let client_b = server.create_client(cx_b, "user_b").await;
4275 server
4276 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
4277 .await;
4278 cx_a.update(editor::init);
4279 cx_b.update(editor::init);
4280
4281 client_a
4282 .fs
4283 .insert_tree(
4284 "/a",
4285 json!({
4286 "1.txt": "one",
4287 "2.txt": "two",
4288 "3.txt": "three",
4289 }),
4290 )
4291 .await;
4292 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
4293
4294 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
4295
4296 // Client A opens some editors.
4297 let workspace_a = client_a.build_workspace(&project_a, cx_a);
4298 let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
4299 let editor_a1 = workspace_a
4300 .update(cx_a, |workspace, cx| {
4301 workspace.open_path((worktree_id, "1.txt"), true, cx)
4302 })
4303 .await
4304 .unwrap()
4305 .downcast::<Editor>()
4306 .unwrap();
4307 let editor_a2 = workspace_a
4308 .update(cx_a, |workspace, cx| {
4309 workspace.open_path((worktree_id, "2.txt"), true, cx)
4310 })
4311 .await
4312 .unwrap()
4313 .downcast::<Editor>()
4314 .unwrap();
4315
4316 // Client B opens an editor.
4317 let workspace_b = client_b.build_workspace(&project_b, cx_b);
4318 let editor_b1 = workspace_b
4319 .update(cx_b, |workspace, cx| {
4320 workspace.open_path((worktree_id, "1.txt"), true, cx)
4321 })
4322 .await
4323 .unwrap()
4324 .downcast::<Editor>()
4325 .unwrap();
4326
4327 let client_a_id = project_b.read_with(cx_b, |project, _| {
4328 project.collaborators().values().next().unwrap().peer_id
4329 });
4330 let client_b_id = project_a.read_with(cx_a, |project, _| {
4331 project.collaborators().values().next().unwrap().peer_id
4332 });
4333
4334 // When client B starts following client A, all visible view states are replicated to client B.
4335 editor_a1.update(cx_a, |editor, cx| {
4336 editor.change_selections(None, cx, |s| s.select_ranges([0..1]))
4337 });
4338 editor_a2.update(cx_a, |editor, cx| {
4339 editor.change_selections(None, cx, |s| s.select_ranges([2..3]))
4340 });
4341 workspace_b
4342 .update(cx_b, |workspace, cx| {
4343 workspace
4344 .toggle_follow(&ToggleFollow(client_a_id), cx)
4345 .unwrap()
4346 })
4347 .await
4348 .unwrap();
4349
4350 let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
4351 workspace
4352 .active_item(cx)
4353 .unwrap()
4354 .downcast::<Editor>()
4355 .unwrap()
4356 });
4357 assert!(cx_b.read(|cx| editor_b2.is_focused(cx)));
4358 assert_eq!(
4359 editor_b2.read_with(cx_b, |editor, cx| editor.project_path(cx)),
4360 Some((worktree_id, "2.txt").into())
4361 );
4362 assert_eq!(
4363 editor_b2.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
4364 vec![2..3]
4365 );
4366 assert_eq!(
4367 editor_b1.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
4368 vec![0..1]
4369 );
4370
4371 // When client A activates a different editor, client B does so as well.
4372 workspace_a.update(cx_a, |workspace, cx| {
4373 workspace.activate_item(&editor_a1, cx)
4374 });
4375 workspace_b
4376 .condition(cx_b, |workspace, cx| {
4377 workspace.active_item(cx).unwrap().id() == editor_b1.id()
4378 })
4379 .await;
4380
4381 // When client A navigates back and forth, client B does so as well.
4382 workspace_a
4383 .update(cx_a, |workspace, cx| {
4384 workspace::Pane::go_back(workspace, None, cx)
4385 })
4386 .await;
4387 workspace_b
4388 .condition(cx_b, |workspace, cx| {
4389 workspace.active_item(cx).unwrap().id() == editor_b2.id()
4390 })
4391 .await;
4392
4393 workspace_a
4394 .update(cx_a, |workspace, cx| {
4395 workspace::Pane::go_forward(workspace, None, cx)
4396 })
4397 .await;
4398 workspace_b
4399 .condition(cx_b, |workspace, cx| {
4400 workspace.active_item(cx).unwrap().id() == editor_b1.id()
4401 })
4402 .await;
4403
4404 // Changes to client A's editor are reflected on client B.
4405 editor_a1.update(cx_a, |editor, cx| {
4406 editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2]));
4407 });
4408 editor_b1
4409 .condition(cx_b, |editor, cx| {
4410 editor.selections.ranges(cx) == vec![1..1, 2..2]
4411 })
4412 .await;
4413
4414 editor_a1.update(cx_a, |editor, cx| editor.set_text("TWO", cx));
4415 editor_b1
4416 .condition(cx_b, |editor, cx| editor.text(cx) == "TWO")
4417 .await;
4418
4419 editor_a1.update(cx_a, |editor, cx| {
4420 editor.change_selections(None, cx, |s| s.select_ranges([3..3]));
4421 editor.set_scroll_position(vec2f(0., 100.), cx);
4422 });
4423 editor_b1
4424 .condition(cx_b, |editor, cx| {
4425 editor.selections.ranges(cx) == vec![3..3]
4426 })
4427 .await;
4428
4429 // After unfollowing, client B stops receiving updates from client A.
4430 workspace_b.update(cx_b, |workspace, cx| {
4431 workspace.unfollow(&workspace.active_pane().clone(), cx)
4432 });
4433 workspace_a.update(cx_a, |workspace, cx| {
4434 workspace.activate_item(&editor_a2, cx)
4435 });
4436 cx_a.foreground().run_until_parked();
4437 assert_eq!(
4438 workspace_b.read_with(cx_b, |workspace, cx| workspace
4439 .active_item(cx)
4440 .unwrap()
4441 .id()),
4442 editor_b1.id()
4443 );
4444
4445 // Client A starts following client B.
4446 workspace_a
4447 .update(cx_a, |workspace, cx| {
4448 workspace
4449 .toggle_follow(&ToggleFollow(client_b_id), cx)
4450 .unwrap()
4451 })
4452 .await
4453 .unwrap();
4454 assert_eq!(
4455 workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
4456 Some(client_b_id)
4457 );
4458 assert_eq!(
4459 workspace_a.read_with(cx_a, |workspace, cx| workspace
4460 .active_item(cx)
4461 .unwrap()
4462 .id()),
4463 editor_a1.id()
4464 );
4465
4466 // Following interrupts when client B disconnects.
4467 client_b.disconnect(&cx_b.to_async()).unwrap();
4468 cx_a.foreground().run_until_parked();
4469 assert_eq!(
4470 workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
4471 None
4472 );
4473}
4474
4475#[gpui::test(iterations = 10)]
4476async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
4477 cx_a.foreground().forbid_parking();
4478 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
4479 let client_a = server.create_client(cx_a, "user_a").await;
4480 let client_b = server.create_client(cx_b, "user_b").await;
4481 server
4482 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
4483 .await;
4484 cx_a.update(editor::init);
4485 cx_b.update(editor::init);
4486
4487 // Client A shares a project.
4488 client_a
4489 .fs
4490 .insert_tree(
4491 "/a",
4492 json!({
4493 "1.txt": "one",
4494 "2.txt": "two",
4495 "3.txt": "three",
4496 "4.txt": "four",
4497 }),
4498 )
4499 .await;
4500 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
4501
4502 // Client B joins the project.
4503 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
4504
4505 // Client A opens some editors.
4506 let workspace_a = client_a.build_workspace(&project_a, cx_a);
4507 let pane_a1 = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
4508 let _editor_a1 = workspace_a
4509 .update(cx_a, |workspace, cx| {
4510 workspace.open_path((worktree_id, "1.txt"), true, cx)
4511 })
4512 .await
4513 .unwrap()
4514 .downcast::<Editor>()
4515 .unwrap();
4516
4517 // Client B opens an editor.
4518 let workspace_b = client_b.build_workspace(&project_b, cx_b);
4519 let pane_b1 = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
4520 let _editor_b1 = workspace_b
4521 .update(cx_b, |workspace, cx| {
4522 workspace.open_path((worktree_id, "2.txt"), true, cx)
4523 })
4524 .await
4525 .unwrap()
4526 .downcast::<Editor>()
4527 .unwrap();
4528
4529 // Clients A and B follow each other in split panes
4530 workspace_a.update(cx_a, |workspace, cx| {
4531 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
4532 let pane_a1 = pane_a1.clone();
4533 cx.defer(move |workspace, _| {
4534 assert_ne!(*workspace.active_pane(), pane_a1);
4535 });
4536 });
4537 workspace_a
4538 .update(cx_a, |workspace, cx| {
4539 let leader_id = *project_a.read(cx).collaborators().keys().next().unwrap();
4540 workspace
4541 .toggle_follow(&workspace::ToggleFollow(leader_id), cx)
4542 .unwrap()
4543 })
4544 .await
4545 .unwrap();
4546 workspace_b.update(cx_b, |workspace, cx| {
4547 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
4548 let pane_b1 = pane_b1.clone();
4549 cx.defer(move |workspace, _| {
4550 assert_ne!(*workspace.active_pane(), pane_b1);
4551 });
4552 });
4553 workspace_b
4554 .update(cx_b, |workspace, cx| {
4555 let leader_id = *project_b.read(cx).collaborators().keys().next().unwrap();
4556 workspace
4557 .toggle_follow(&workspace::ToggleFollow(leader_id), cx)
4558 .unwrap()
4559 })
4560 .await
4561 .unwrap();
4562
4563 workspace_a.update(cx_a, |workspace, cx| {
4564 workspace.activate_next_pane(cx);
4565 });
4566 // Wait for focus effects to be fully flushed
4567 workspace_a.update(cx_a, |workspace, _| {
4568 assert_eq!(*workspace.active_pane(), pane_a1);
4569 });
4570
4571 workspace_a
4572 .update(cx_a, |workspace, cx| {
4573 workspace.open_path((worktree_id, "3.txt"), true, cx)
4574 })
4575 .await
4576 .unwrap();
4577 workspace_b.update(cx_b, |workspace, cx| {
4578 workspace.activate_next_pane(cx);
4579 });
4580
4581 workspace_b
4582 .update(cx_b, |workspace, cx| {
4583 assert_eq!(*workspace.active_pane(), pane_b1);
4584 workspace.open_path((worktree_id, "4.txt"), true, cx)
4585 })
4586 .await
4587 .unwrap();
4588 cx_a.foreground().run_until_parked();
4589
4590 // Ensure leader updates don't change the active pane of followers
4591 workspace_a.read_with(cx_a, |workspace, _| {
4592 assert_eq!(*workspace.active_pane(), pane_a1);
4593 });
4594 workspace_b.read_with(cx_b, |workspace, _| {
4595 assert_eq!(*workspace.active_pane(), pane_b1);
4596 });
4597
4598 // Ensure peers following each other doesn't cause an infinite loop.
4599 assert_eq!(
4600 workspace_a.read_with(cx_a, |workspace, cx| workspace
4601 .active_item(cx)
4602 .unwrap()
4603 .project_path(cx)),
4604 Some((worktree_id, "3.txt").into())
4605 );
4606 workspace_a.update(cx_a, |workspace, cx| {
4607 assert_eq!(
4608 workspace.active_item(cx).unwrap().project_path(cx),
4609 Some((worktree_id, "3.txt").into())
4610 );
4611 workspace.activate_next_pane(cx);
4612 });
4613
4614 workspace_a.update(cx_a, |workspace, cx| {
4615 assert_eq!(
4616 workspace.active_item(cx).unwrap().project_path(cx),
4617 Some((worktree_id, "4.txt").into())
4618 );
4619 });
4620
4621 workspace_b.update(cx_b, |workspace, cx| {
4622 assert_eq!(
4623 workspace.active_item(cx).unwrap().project_path(cx),
4624 Some((worktree_id, "4.txt").into())
4625 );
4626 workspace.activate_next_pane(cx);
4627 });
4628
4629 workspace_b.update(cx_b, |workspace, cx| {
4630 assert_eq!(
4631 workspace.active_item(cx).unwrap().project_path(cx),
4632 Some((worktree_id, "3.txt").into())
4633 );
4634 });
4635}
4636
4637#[gpui::test(iterations = 10)]
4638async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
4639 cx_a.foreground().forbid_parking();
4640
4641 // 2 clients connect to a server.
4642 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
4643 let client_a = server.create_client(cx_a, "user_a").await;
4644 let client_b = server.create_client(cx_b, "user_b").await;
4645 server
4646 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
4647 .await;
4648 cx_a.update(editor::init);
4649 cx_b.update(editor::init);
4650
4651 // Client A shares a project.
4652 client_a
4653 .fs
4654 .insert_tree(
4655 "/a",
4656 json!({
4657 "1.txt": "one",
4658 "2.txt": "two",
4659 "3.txt": "three",
4660 }),
4661 )
4662 .await;
4663 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
4664 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
4665
4666 // Client A opens some editors.
4667 let workspace_a = client_a.build_workspace(&project_a, cx_a);
4668 let _editor_a1 = workspace_a
4669 .update(cx_a, |workspace, cx| {
4670 workspace.open_path((worktree_id, "1.txt"), true, cx)
4671 })
4672 .await
4673 .unwrap()
4674 .downcast::<Editor>()
4675 .unwrap();
4676
4677 // Client B starts following client A.
4678 let workspace_b = client_b.build_workspace(&project_b, cx_b);
4679 let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
4680 let leader_id = project_b.read_with(cx_b, |project, _| {
4681 project.collaborators().values().next().unwrap().peer_id
4682 });
4683 workspace_b
4684 .update(cx_b, |workspace, cx| {
4685 workspace
4686 .toggle_follow(&ToggleFollow(leader_id), cx)
4687 .unwrap()
4688 })
4689 .await
4690 .unwrap();
4691 assert_eq!(
4692 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
4693 Some(leader_id)
4694 );
4695 let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
4696 workspace
4697 .active_item(cx)
4698 .unwrap()
4699 .downcast::<Editor>()
4700 .unwrap()
4701 });
4702
4703 // When client B moves, it automatically stops following client A.
4704 editor_b2.update(cx_b, |editor, cx| editor.move_right(&editor::MoveRight, cx));
4705 assert_eq!(
4706 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
4707 None
4708 );
4709
4710 workspace_b
4711 .update(cx_b, |workspace, cx| {
4712 workspace
4713 .toggle_follow(&ToggleFollow(leader_id), cx)
4714 .unwrap()
4715 })
4716 .await
4717 .unwrap();
4718 assert_eq!(
4719 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
4720 Some(leader_id)
4721 );
4722
4723 // When client B edits, it automatically stops following client A.
4724 editor_b2.update(cx_b, |editor, cx| editor.insert("X", cx));
4725 assert_eq!(
4726 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
4727 None
4728 );
4729
4730 workspace_b
4731 .update(cx_b, |workspace, cx| {
4732 workspace
4733 .toggle_follow(&ToggleFollow(leader_id), cx)
4734 .unwrap()
4735 })
4736 .await
4737 .unwrap();
4738 assert_eq!(
4739 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
4740 Some(leader_id)
4741 );
4742
4743 // When client B scrolls, it automatically stops following client A.
4744 editor_b2.update(cx_b, |editor, cx| {
4745 editor.set_scroll_position(vec2f(0., 3.), cx)
4746 });
4747 assert_eq!(
4748 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
4749 None
4750 );
4751
4752 workspace_b
4753 .update(cx_b, |workspace, cx| {
4754 workspace
4755 .toggle_follow(&ToggleFollow(leader_id), cx)
4756 .unwrap()
4757 })
4758 .await
4759 .unwrap();
4760 assert_eq!(
4761 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
4762 Some(leader_id)
4763 );
4764
4765 // When client B activates a different pane, it continues following client A in the original pane.
4766 workspace_b.update(cx_b, |workspace, cx| {
4767 workspace.split_pane(pane_b.clone(), SplitDirection::Right, cx)
4768 });
4769 assert_eq!(
4770 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
4771 Some(leader_id)
4772 );
4773
4774 workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
4775 assert_eq!(
4776 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
4777 Some(leader_id)
4778 );
4779
4780 // When client B activates a different item in the original pane, it automatically stops following client A.
4781 workspace_b
4782 .update(cx_b, |workspace, cx| {
4783 workspace.open_path((worktree_id, "2.txt"), true, cx)
4784 })
4785 .await
4786 .unwrap();
4787 assert_eq!(
4788 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
4789 None
4790 );
4791}
4792
4793#[gpui::test(iterations = 10)]
4794async fn test_peers_simultaneously_following_each_other(
4795 deterministic: Arc<Deterministic>,
4796 cx_a: &mut TestAppContext,
4797 cx_b: &mut TestAppContext,
4798) {
4799 deterministic.forbid_parking();
4800
4801 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
4802 let client_a = server.create_client(cx_a, "user_a").await;
4803 let client_b = server.create_client(cx_b, "user_b").await;
4804 server
4805 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
4806 .await;
4807 cx_a.update(editor::init);
4808 cx_b.update(editor::init);
4809
4810 client_a.fs.insert_tree("/a", json!({})).await;
4811 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
4812 let workspace_a = client_a.build_workspace(&project_a, cx_a);
4813
4814 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
4815 let workspace_b = client_b.build_workspace(&project_b, cx_b);
4816
4817 deterministic.run_until_parked();
4818 let client_a_id = project_b.read_with(cx_b, |project, _| {
4819 project.collaborators().values().next().unwrap().peer_id
4820 });
4821 let client_b_id = project_a.read_with(cx_a, |project, _| {
4822 project.collaborators().values().next().unwrap().peer_id
4823 });
4824
4825 let a_follow_b = workspace_a.update(cx_a, |workspace, cx| {
4826 workspace
4827 .toggle_follow(&ToggleFollow(client_b_id), cx)
4828 .unwrap()
4829 });
4830 let b_follow_a = workspace_b.update(cx_b, |workspace, cx| {
4831 workspace
4832 .toggle_follow(&ToggleFollow(client_a_id), cx)
4833 .unwrap()
4834 });
4835
4836 futures::try_join!(a_follow_b, b_follow_a).unwrap();
4837 workspace_a.read_with(cx_a, |workspace, _| {
4838 assert_eq!(
4839 workspace.leader_for_pane(workspace.active_pane()),
4840 Some(client_b_id)
4841 );
4842 });
4843 workspace_b.read_with(cx_b, |workspace, _| {
4844 assert_eq!(
4845 workspace.leader_for_pane(workspace.active_pane()),
4846 Some(client_a_id)
4847 );
4848 });
4849}
4850
4851#[gpui::test(iterations = 100)]
4852async fn test_random_collaboration(
4853 cx: &mut TestAppContext,
4854 deterministic: Arc<Deterministic>,
4855 rng: StdRng,
4856) {
4857 deterministic.forbid_parking();
4858 let max_peers = env::var("MAX_PEERS")
4859 .map(|i| i.parse().expect("invalid `MAX_PEERS` variable"))
4860 .unwrap_or(5);
4861 assert!(max_peers <= 5);
4862
4863 let max_operations = env::var("OPERATIONS")
4864 .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
4865 .unwrap_or(10);
4866
4867 let rng = Arc::new(Mutex::new(rng));
4868
4869 let guest_lang_registry = Arc::new(LanguageRegistry::test());
4870 let host_language_registry = Arc::new(LanguageRegistry::test());
4871
4872 let fs = FakeFs::new(cx.background());
4873 fs.insert_tree("/_collab", json!({"init": ""})).await;
4874
4875 let mut server = TestServer::start(cx.foreground(), cx.background()).await;
4876 let db = server.app_state.db.clone();
4877 let host_user_id = db.create_user("host", None, false).await.unwrap();
4878 let mut available_guests = vec![
4879 "guest-1".to_string(),
4880 "guest-2".to_string(),
4881 "guest-3".to_string(),
4882 "guest-4".to_string(),
4883 ];
4884
4885 for username in &available_guests {
4886 let guest_user_id = db.create_user(username, None, false).await.unwrap();
4887 assert_eq!(*username, format!("guest-{}", guest_user_id));
4888 server
4889 .app_state
4890 .db
4891 .send_contact_request(guest_user_id, host_user_id)
4892 .await
4893 .unwrap();
4894 server
4895 .app_state
4896 .db
4897 .respond_to_contact_request(host_user_id, guest_user_id, true)
4898 .await
4899 .unwrap();
4900 }
4901
4902 let mut clients = Vec::new();
4903 let mut user_ids = Vec::new();
4904 let mut op_start_signals = Vec::new();
4905
4906 let mut next_entity_id = 100000;
4907 let mut host_cx = TestAppContext::new(
4908 cx.foreground_platform(),
4909 cx.platform(),
4910 deterministic.build_foreground(next_entity_id),
4911 deterministic.build_background(),
4912 cx.font_cache(),
4913 cx.leak_detector(),
4914 next_entity_id,
4915 );
4916 let host = server.create_client(&mut host_cx, "host").await;
4917 let host_project = host_cx.update(|cx| {
4918 Project::local(
4919 true,
4920 host.client.clone(),
4921 host.user_store.clone(),
4922 host.project_store.clone(),
4923 host_language_registry.clone(),
4924 fs.clone(),
4925 cx,
4926 )
4927 });
4928 let host_project_id = host_project
4929 .update(&mut host_cx, |p, _| p.next_remote_id())
4930 .await;
4931
4932 let (collab_worktree, _) = host_project
4933 .update(&mut host_cx, |project, cx| {
4934 project.find_or_create_local_worktree("/_collab", true, cx)
4935 })
4936 .await
4937 .unwrap();
4938 collab_worktree
4939 .read_with(&host_cx, |tree, _| tree.as_local().unwrap().scan_complete())
4940 .await;
4941
4942 // Set up fake language servers.
4943 let mut language = Language::new(
4944 LanguageConfig {
4945 name: "Rust".into(),
4946 path_suffixes: vec!["rs".to_string()],
4947 ..Default::default()
4948 },
4949 None,
4950 );
4951 let _fake_servers = language
4952 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
4953 name: "the-fake-language-server",
4954 capabilities: lsp::LanguageServer::full_capabilities(),
4955 initializer: Some(Box::new({
4956 let rng = rng.clone();
4957 let fs = fs.clone();
4958 let project = host_project.downgrade();
4959 move |fake_server: &mut FakeLanguageServer| {
4960 fake_server.handle_request::<lsp::request::Completion, _, _>(
4961 |_, _| async move {
4962 Ok(Some(lsp::CompletionResponse::Array(vec![
4963 lsp::CompletionItem {
4964 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
4965 range: lsp::Range::new(
4966 lsp::Position::new(0, 0),
4967 lsp::Position::new(0, 0),
4968 ),
4969 new_text: "the-new-text".to_string(),
4970 })),
4971 ..Default::default()
4972 },
4973 ])))
4974 },
4975 );
4976
4977 fake_server.handle_request::<lsp::request::CodeActionRequest, _, _>(
4978 |_, _| async move {
4979 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
4980 lsp::CodeAction {
4981 title: "the-code-action".to_string(),
4982 ..Default::default()
4983 },
4984 )]))
4985 },
4986 );
4987
4988 fake_server.handle_request::<lsp::request::PrepareRenameRequest, _, _>(
4989 |params, _| async move {
4990 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
4991 params.position,
4992 params.position,
4993 ))))
4994 },
4995 );
4996
4997 fake_server.handle_request::<lsp::request::GotoDefinition, _, _>({
4998 let fs = fs.clone();
4999 let rng = rng.clone();
5000 move |_, _| {
5001 let fs = fs.clone();
5002 let rng = rng.clone();
5003 async move {
5004 let files = fs.files().await;
5005 let mut rng = rng.lock();
5006 let count = rng.gen_range::<usize, _>(1..3);
5007 let files = (0..count)
5008 .map(|_| files.choose(&mut *rng).unwrap())
5009 .collect::<Vec<_>>();
5010 log::info!("LSP: Returning definitions in files {:?}", &files);
5011 Ok(Some(lsp::GotoDefinitionResponse::Array(
5012 files
5013 .into_iter()
5014 .map(|file| lsp::Location {
5015 uri: lsp::Url::from_file_path(file).unwrap(),
5016 range: Default::default(),
5017 })
5018 .collect(),
5019 )))
5020 }
5021 }
5022 });
5023
5024 fake_server.handle_request::<lsp::request::DocumentHighlightRequest, _, _>({
5025 let rng = rng.clone();
5026 let project = project;
5027 move |params, mut cx| {
5028 let highlights = if let Some(project) = project.upgrade(&cx) {
5029 project.update(&mut cx, |project, cx| {
5030 let path = params
5031 .text_document_position_params
5032 .text_document
5033 .uri
5034 .to_file_path()
5035 .unwrap();
5036 let (worktree, relative_path) =
5037 project.find_local_worktree(&path, cx)?;
5038 let project_path =
5039 ProjectPath::from((worktree.read(cx).id(), relative_path));
5040 let buffer =
5041 project.get_open_buffer(&project_path, cx)?.read(cx);
5042
5043 let mut highlights = Vec::new();
5044 let highlight_count = rng.lock().gen_range(1..=5);
5045 let mut prev_end = 0;
5046 for _ in 0..highlight_count {
5047 let range =
5048 buffer.random_byte_range(prev_end, &mut *rng.lock());
5049
5050 highlights.push(lsp::DocumentHighlight {
5051 range: range_to_lsp(range.to_point_utf16(buffer)),
5052 kind: Some(lsp::DocumentHighlightKind::READ),
5053 });
5054 prev_end = range.end;
5055 }
5056 Some(highlights)
5057 })
5058 } else {
5059 None
5060 };
5061 async move { Ok(highlights) }
5062 }
5063 });
5064 }
5065 })),
5066 ..Default::default()
5067 }))
5068 .await;
5069 host_language_registry.add(Arc::new(language));
5070
5071 let op_start_signal = futures::channel::mpsc::unbounded();
5072 user_ids.push(host.current_user_id(&host_cx));
5073 op_start_signals.push(op_start_signal.0);
5074 clients.push(host_cx.foreground().spawn(host.simulate_host(
5075 host_project,
5076 op_start_signal.1,
5077 rng.clone(),
5078 host_cx,
5079 )));
5080
5081 let disconnect_host_at = if rng.lock().gen_bool(0.2) {
5082 rng.lock().gen_range(0..max_operations)
5083 } else {
5084 max_operations
5085 };
5086
5087 let mut operations = 0;
5088 while operations < max_operations {
5089 if operations == disconnect_host_at {
5090 server.disconnect_client(user_ids[0]);
5091 deterministic.advance_clock(RECEIVE_TIMEOUT);
5092 drop(op_start_signals);
5093
5094 deterministic.start_waiting();
5095 let mut clients = futures::future::join_all(clients).await;
5096 deterministic.finish_waiting();
5097 deterministic.run_until_parked();
5098
5099 let (host, host_project, mut host_cx, host_err) = clients.remove(0);
5100 if let Some(host_err) = host_err {
5101 log::error!("host error - {:?}", host_err);
5102 }
5103 host_project.read_with(&host_cx, |project, _| assert!(!project.is_shared()));
5104 for (guest, guest_project, mut guest_cx, guest_err) in clients {
5105 if let Some(guest_err) = guest_err {
5106 log::error!("{} error - {:?}", guest.username, guest_err);
5107 }
5108
5109 let contacts = server
5110 .app_state
5111 .db
5112 .get_contacts(guest.current_user_id(&guest_cx))
5113 .await
5114 .unwrap();
5115 let contacts = server
5116 .store
5117 .lock()
5118 .await
5119 .build_initial_contacts_update(contacts)
5120 .contacts;
5121 assert!(!contacts
5122 .iter()
5123 .flat_map(|contact| &contact.projects)
5124 .any(|project| project.id == host_project_id));
5125 guest_project.read_with(&guest_cx, |project, _| assert!(project.is_read_only()));
5126 guest_cx.update(|_| drop((guest, guest_project)));
5127 }
5128 host_cx.update(|_| drop((host, host_project)));
5129
5130 return;
5131 }
5132
5133 let distribution = rng.lock().gen_range(0..100);
5134 match distribution {
5135 0..=19 if !available_guests.is_empty() => {
5136 let guest_ix = rng.lock().gen_range(0..available_guests.len());
5137 let guest_username = available_guests.remove(guest_ix);
5138 log::info!("Adding new connection for {}", guest_username);
5139 next_entity_id += 100000;
5140 let mut guest_cx = TestAppContext::new(
5141 cx.foreground_platform(),
5142 cx.platform(),
5143 deterministic.build_foreground(next_entity_id),
5144 deterministic.build_background(),
5145 cx.font_cache(),
5146 cx.leak_detector(),
5147 next_entity_id,
5148 );
5149
5150 deterministic.start_waiting();
5151 let guest = server.create_client(&mut guest_cx, &guest_username).await;
5152 let guest_project = Project::remote(
5153 host_project_id,
5154 guest.client.clone(),
5155 guest.user_store.clone(),
5156 guest.project_store.clone(),
5157 guest_lang_registry.clone(),
5158 FakeFs::new(cx.background()),
5159 guest_cx.to_async(),
5160 )
5161 .await
5162 .unwrap();
5163 deterministic.finish_waiting();
5164
5165 let op_start_signal = futures::channel::mpsc::unbounded();
5166 user_ids.push(guest.current_user_id(&guest_cx));
5167 op_start_signals.push(op_start_signal.0);
5168 clients.push(guest_cx.foreground().spawn(guest.simulate_guest(
5169 guest_username.clone(),
5170 guest_project,
5171 op_start_signal.1,
5172 rng.clone(),
5173 guest_cx,
5174 )));
5175
5176 log::info!("Added connection for {}", guest_username);
5177 operations += 1;
5178 }
5179 20..=29 if clients.len() > 1 => {
5180 let guest_ix = rng.lock().gen_range(1..clients.len());
5181 log::info!("Removing guest {}", user_ids[guest_ix]);
5182 let removed_guest_id = user_ids.remove(guest_ix);
5183 let guest = clients.remove(guest_ix);
5184 op_start_signals.remove(guest_ix);
5185 server.forbid_connections();
5186 server.disconnect_client(removed_guest_id);
5187 deterministic.advance_clock(RECEIVE_TIMEOUT);
5188 deterministic.start_waiting();
5189 log::info!("Waiting for guest {} to exit...", removed_guest_id);
5190 let (guest, guest_project, mut guest_cx, guest_err) = guest.await;
5191 deterministic.finish_waiting();
5192 server.allow_connections();
5193
5194 if let Some(guest_err) = guest_err {
5195 log::error!("{} error - {:?}", guest.username, guest_err);
5196 }
5197 guest_project.read_with(&guest_cx, |project, _| assert!(project.is_read_only()));
5198 for user_id in &user_ids {
5199 let contacts = server.app_state.db.get_contacts(*user_id).await.unwrap();
5200 let contacts = server
5201 .store
5202 .lock()
5203 .await
5204 .build_initial_contacts_update(contacts)
5205 .contacts;
5206 for contact in contacts {
5207 if contact.online {
5208 assert_ne!(
5209 contact.user_id, removed_guest_id.0 as u64,
5210 "removed guest is still a contact of another peer"
5211 );
5212 }
5213 for project in contact.projects {
5214 for project_guest_id in project.guests {
5215 assert_ne!(
5216 project_guest_id, removed_guest_id.0 as u64,
5217 "removed guest appears as still participating on a project"
5218 );
5219 }
5220 }
5221 }
5222 }
5223
5224 log::info!("{} removed", guest.username);
5225 available_guests.push(guest.username.clone());
5226 guest_cx.update(|_| drop((guest, guest_project)));
5227
5228 operations += 1;
5229 }
5230 _ => {
5231 while operations < max_operations && rng.lock().gen_bool(0.7) {
5232 op_start_signals
5233 .choose(&mut *rng.lock())
5234 .unwrap()
5235 .unbounded_send(())
5236 .unwrap();
5237 operations += 1;
5238 }
5239
5240 if rng.lock().gen_bool(0.8) {
5241 deterministic.run_until_parked();
5242 }
5243 }
5244 }
5245 }
5246
5247 drop(op_start_signals);
5248 deterministic.start_waiting();
5249 let mut clients = futures::future::join_all(clients).await;
5250 deterministic.finish_waiting();
5251 deterministic.run_until_parked();
5252
5253 let (host_client, host_project, mut host_cx, host_err) = clients.remove(0);
5254 if let Some(host_err) = host_err {
5255 panic!("host error - {:?}", host_err);
5256 }
5257 let host_worktree_snapshots = host_project.read_with(&host_cx, |project, cx| {
5258 project
5259 .worktrees(cx)
5260 .map(|worktree| {
5261 let snapshot = worktree.read(cx).snapshot();
5262 (snapshot.id(), snapshot)
5263 })
5264 .collect::<BTreeMap<_, _>>()
5265 });
5266
5267 host_project.read_with(&host_cx, |project, cx| project.check_invariants(cx));
5268
5269 for (guest_client, guest_project, mut guest_cx, guest_err) in clients.into_iter() {
5270 if let Some(guest_err) = guest_err {
5271 panic!("{} error - {:?}", guest_client.username, guest_err);
5272 }
5273 let worktree_snapshots = guest_project.read_with(&guest_cx, |project, cx| {
5274 project
5275 .worktrees(cx)
5276 .map(|worktree| {
5277 let worktree = worktree.read(cx);
5278 (worktree.id(), worktree.snapshot())
5279 })
5280 .collect::<BTreeMap<_, _>>()
5281 });
5282
5283 assert_eq!(
5284 worktree_snapshots.keys().collect::<Vec<_>>(),
5285 host_worktree_snapshots.keys().collect::<Vec<_>>(),
5286 "{} has different worktrees than the host",
5287 guest_client.username
5288 );
5289 for (id, host_snapshot) in &host_worktree_snapshots {
5290 let guest_snapshot = &worktree_snapshots[id];
5291 assert_eq!(
5292 guest_snapshot.root_name(),
5293 host_snapshot.root_name(),
5294 "{} has different root name than the host for worktree {}",
5295 guest_client.username,
5296 id
5297 );
5298 assert_eq!(
5299 guest_snapshot.entries(false).collect::<Vec<_>>(),
5300 host_snapshot.entries(false).collect::<Vec<_>>(),
5301 "{} has different snapshot than the host for worktree {}",
5302 guest_client.username,
5303 id
5304 );
5305 assert_eq!(guest_snapshot.scan_id(), host_snapshot.scan_id());
5306 }
5307
5308 guest_project.read_with(&guest_cx, |project, cx| project.check_invariants(cx));
5309
5310 for guest_buffer in &guest_client.buffers {
5311 let buffer_id = guest_buffer.read_with(&guest_cx, |buffer, _| buffer.remote_id());
5312 let host_buffer = host_project.read_with(&host_cx, |project, cx| {
5313 project.buffer_for_id(buffer_id, cx).unwrap_or_else(|| {
5314 panic!(
5315 "host does not have buffer for guest:{}, peer:{}, id:{}",
5316 guest_client.username, guest_client.peer_id, buffer_id
5317 )
5318 })
5319 });
5320 let path =
5321 host_buffer.read_with(&host_cx, |buffer, cx| buffer.file().unwrap().full_path(cx));
5322
5323 assert_eq!(
5324 guest_buffer.read_with(&guest_cx, |buffer, _| buffer.deferred_ops_len()),
5325 0,
5326 "{}, buffer {}, path {:?} has deferred operations",
5327 guest_client.username,
5328 buffer_id,
5329 path,
5330 );
5331 assert_eq!(
5332 guest_buffer.read_with(&guest_cx, |buffer, _| buffer.text()),
5333 host_buffer.read_with(&host_cx, |buffer, _| buffer.text()),
5334 "{}, buffer {}, path {:?}, differs from the host's buffer",
5335 guest_client.username,
5336 buffer_id,
5337 path
5338 );
5339 }
5340
5341 guest_cx.update(|_| drop((guest_project, guest_client)));
5342 }
5343
5344 host_cx.update(|_| drop((host_client, host_project)));
5345}
5346
5347struct TestServer {
5348 peer: Arc<Peer>,
5349 app_state: Arc<AppState>,
5350 server: Arc<Server>,
5351 foreground: Rc<executor::Foreground>,
5352 notifications: mpsc::UnboundedReceiver<()>,
5353 connection_killers: Arc<Mutex<HashMap<UserId, Arc<AtomicBool>>>>,
5354 forbid_connections: Arc<AtomicBool>,
5355 _test_db: TestDb,
5356}
5357
5358impl TestServer {
5359 async fn start(
5360 foreground: Rc<executor::Foreground>,
5361 background: Arc<executor::Background>,
5362 ) -> Self {
5363 let test_db = TestDb::fake(background.clone());
5364 let app_state = Self::build_app_state(&test_db).await;
5365 let peer = Peer::new();
5366 let notifications = mpsc::unbounded();
5367 let server = Server::new(app_state.clone(), Some(notifications.0));
5368 Self {
5369 peer,
5370 app_state,
5371 server,
5372 foreground,
5373 notifications: notifications.1,
5374 connection_killers: Default::default(),
5375 forbid_connections: Default::default(),
5376 _test_db: test_db,
5377 }
5378 }
5379
5380 async fn create_client(&mut self, cx: &mut TestAppContext, name: &str) -> TestClient {
5381 cx.update(|cx| {
5382 let mut settings = Settings::test(cx);
5383 settings.projects_online_by_default = false;
5384 cx.set_global(settings);
5385 });
5386
5387 let http = FakeHttpClient::with_404_response();
5388 let user_id = if let Ok(Some(user)) = self.app_state.db.get_user_by_github_login(name).await
5389 {
5390 user.id
5391 } else {
5392 self.app_state
5393 .db
5394 .create_user(name, None, false)
5395 .await
5396 .unwrap()
5397 };
5398 let client_name = name.to_string();
5399 let mut client = Client::new(http.clone());
5400 let server = self.server.clone();
5401 let db = self.app_state.db.clone();
5402 let connection_killers = self.connection_killers.clone();
5403 let forbid_connections = self.forbid_connections.clone();
5404 let (connection_id_tx, mut connection_id_rx) = mpsc::channel(16);
5405
5406 Arc::get_mut(&mut client)
5407 .unwrap()
5408 .set_id(user_id.0 as usize)
5409 .override_authenticate(move |cx| {
5410 cx.spawn(|_| async move {
5411 let access_token = "the-token".to_string();
5412 Ok(Credentials {
5413 user_id: user_id.0 as u64,
5414 access_token,
5415 })
5416 })
5417 })
5418 .override_establish_connection(move |credentials, cx| {
5419 assert_eq!(credentials.user_id, user_id.0 as u64);
5420 assert_eq!(credentials.access_token, "the-token");
5421
5422 let server = server.clone();
5423 let db = db.clone();
5424 let connection_killers = connection_killers.clone();
5425 let forbid_connections = forbid_connections.clone();
5426 let client_name = client_name.clone();
5427 let connection_id_tx = connection_id_tx.clone();
5428 cx.spawn(move |cx| async move {
5429 if forbid_connections.load(SeqCst) {
5430 Err(EstablishConnectionError::other(anyhow!(
5431 "server is forbidding connections"
5432 )))
5433 } else {
5434 let (client_conn, server_conn, killed) =
5435 Connection::in_memory(cx.background());
5436 connection_killers.lock().insert(user_id, killed);
5437 let user = db.get_user_by_id(user_id).await.unwrap().unwrap();
5438 cx.background()
5439 .spawn(server.handle_connection(
5440 server_conn,
5441 client_name,
5442 user,
5443 Some(connection_id_tx),
5444 cx.background(),
5445 ))
5446 .detach();
5447 Ok(client_conn)
5448 }
5449 })
5450 });
5451
5452 let fs = FakeFs::new(cx.background());
5453 let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
5454 let project_store = cx.add_model(|_| ProjectStore::new(project::Db::open_fake()));
5455 let app_state = Arc::new(workspace::AppState {
5456 client: client.clone(),
5457 user_store: user_store.clone(),
5458 project_store: project_store.clone(),
5459 languages: Arc::new(LanguageRegistry::new(Task::ready(()))),
5460 themes: ThemeRegistry::new((), cx.font_cache()),
5461 fs: fs.clone(),
5462 build_window_options: Default::default,
5463 initialize_workspace: |_, _, _| unimplemented!(),
5464 default_item_factory: |_, _| unimplemented!(),
5465 });
5466
5467 Channel::init(&client);
5468 Project::init(&client);
5469 cx.update(|cx| workspace::init(app_state.clone(), cx));
5470
5471 client
5472 .authenticate_and_connect(false, &cx.to_async())
5473 .await
5474 .unwrap();
5475 let peer_id = PeerId(connection_id_rx.next().await.unwrap().0);
5476
5477 let client = TestClient {
5478 client,
5479 peer_id,
5480 username: name.to_string(),
5481 user_store,
5482 project_store,
5483 fs,
5484 language_registry: Arc::new(LanguageRegistry::test()),
5485 buffers: Default::default(),
5486 };
5487 client.wait_for_current_user(cx).await;
5488 client
5489 }
5490
5491 fn disconnect_client(&self, user_id: UserId) {
5492 self.connection_killers
5493 .lock()
5494 .remove(&user_id)
5495 .unwrap()
5496 .store(true, SeqCst);
5497 }
5498
5499 fn forbid_connections(&self) {
5500 self.forbid_connections.store(true, SeqCst);
5501 }
5502
5503 fn allow_connections(&self) {
5504 self.forbid_connections.store(false, SeqCst);
5505 }
5506
5507 async fn make_contacts(&self, mut clients: Vec<(&TestClient, &mut TestAppContext)>) {
5508 while let Some((client_a, cx_a)) = clients.pop() {
5509 for (client_b, cx_b) in &mut clients {
5510 client_a
5511 .user_store
5512 .update(cx_a, |store, cx| {
5513 store.request_contact(client_b.user_id().unwrap(), cx)
5514 })
5515 .await
5516 .unwrap();
5517 cx_a.foreground().run_until_parked();
5518 client_b
5519 .user_store
5520 .update(*cx_b, |store, cx| {
5521 store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx)
5522 })
5523 .await
5524 .unwrap();
5525 }
5526 }
5527 }
5528
5529 async fn build_app_state(test_db: &TestDb) -> Arc<AppState> {
5530 Arc::new(AppState {
5531 db: test_db.db().clone(),
5532 api_token: Default::default(),
5533 invite_link_prefix: Default::default(),
5534 })
5535 }
5536
5537 async fn condition<F>(&mut self, mut predicate: F)
5538 where
5539 F: FnMut(&Store) -> bool,
5540 {
5541 assert!(
5542 self.foreground.parking_forbidden(),
5543 "you must call forbid_parking to use server conditions so we don't block indefinitely"
5544 );
5545 while !(predicate)(&*self.server.store.lock().await) {
5546 self.foreground.start_waiting();
5547 self.notifications.next().await;
5548 self.foreground.finish_waiting();
5549 }
5550 }
5551}
5552
5553impl Deref for TestServer {
5554 type Target = Server;
5555
5556 fn deref(&self) -> &Self::Target {
5557 &self.server
5558 }
5559}
5560
5561impl Drop for TestServer {
5562 fn drop(&mut self) {
5563 self.peer.reset();
5564 }
5565}
5566
5567struct TestClient {
5568 client: Arc<Client>,
5569 username: String,
5570 pub peer_id: PeerId,
5571 pub user_store: ModelHandle<UserStore>,
5572 pub project_store: ModelHandle<ProjectStore>,
5573 language_registry: Arc<LanguageRegistry>,
5574 fs: Arc<FakeFs>,
5575 buffers: HashSet<ModelHandle<language::Buffer>>,
5576}
5577
5578impl Deref for TestClient {
5579 type Target = Arc<Client>;
5580
5581 fn deref(&self) -> &Self::Target {
5582 &self.client
5583 }
5584}
5585
5586struct ContactsSummary {
5587 pub current: Vec<String>,
5588 pub outgoing_requests: Vec<String>,
5589 pub incoming_requests: Vec<String>,
5590}
5591
5592impl TestClient {
5593 pub fn current_user_id(&self, cx: &TestAppContext) -> UserId {
5594 UserId::from_proto(
5595 self.user_store
5596 .read_with(cx, |user_store, _| user_store.current_user().unwrap().id),
5597 )
5598 }
5599
5600 async fn wait_for_current_user(&self, cx: &TestAppContext) {
5601 let mut authed_user = self
5602 .user_store
5603 .read_with(cx, |user_store, _| user_store.watch_current_user());
5604 while authed_user.next().await.unwrap().is_none() {}
5605 }
5606
5607 async fn clear_contacts(&self, cx: &mut TestAppContext) {
5608 self.user_store
5609 .update(cx, |store, _| store.clear_contacts())
5610 .await;
5611 }
5612
5613 fn summarize_contacts(&self, cx: &TestAppContext) -> ContactsSummary {
5614 self.user_store.read_with(cx, |store, _| ContactsSummary {
5615 current: store
5616 .contacts()
5617 .iter()
5618 .map(|contact| contact.user.github_login.clone())
5619 .collect(),
5620 outgoing_requests: store
5621 .outgoing_contact_requests()
5622 .iter()
5623 .map(|user| user.github_login.clone())
5624 .collect(),
5625 incoming_requests: store
5626 .incoming_contact_requests()
5627 .iter()
5628 .map(|user| user.github_login.clone())
5629 .collect(),
5630 })
5631 }
5632
5633 async fn build_local_project(
5634 &self,
5635 root_path: impl AsRef<Path>,
5636 cx: &mut TestAppContext,
5637 ) -> (ModelHandle<Project>, WorktreeId) {
5638 let project = cx.update(|cx| {
5639 Project::local(
5640 true,
5641 self.client.clone(),
5642 self.user_store.clone(),
5643 self.project_store.clone(),
5644 self.language_registry.clone(),
5645 self.fs.clone(),
5646 cx,
5647 )
5648 });
5649 let (worktree, _) = project
5650 .update(cx, |p, cx| {
5651 p.find_or_create_local_worktree(root_path, true, cx)
5652 })
5653 .await
5654 .unwrap();
5655 worktree
5656 .read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete())
5657 .await;
5658 (project, worktree.read_with(cx, |tree, _| tree.id()))
5659 }
5660
5661 async fn build_remote_project(
5662 &self,
5663 host_project: &ModelHandle<Project>,
5664 host_cx: &mut TestAppContext,
5665 guest_cx: &mut TestAppContext,
5666 ) -> ModelHandle<Project> {
5667 let host_project_id = host_project
5668 .read_with(host_cx, |project, _| project.next_remote_id())
5669 .await;
5670 let guest_user_id = self.user_id().unwrap();
5671 let languages = host_project.read_with(host_cx, |project, _| project.languages().clone());
5672 let project_b = guest_cx.spawn(|cx| {
5673 Project::remote(
5674 host_project_id,
5675 self.client.clone(),
5676 self.user_store.clone(),
5677 self.project_store.clone(),
5678 languages,
5679 FakeFs::new(cx.background()),
5680 cx,
5681 )
5682 });
5683 host_cx.foreground().run_until_parked();
5684 host_project.update(host_cx, |project, cx| {
5685 project.respond_to_join_request(guest_user_id, true, cx)
5686 });
5687 let project = project_b.await.unwrap();
5688 project
5689 }
5690
5691 fn build_workspace(
5692 &self,
5693 project: &ModelHandle<Project>,
5694 cx: &mut TestAppContext,
5695 ) -> ViewHandle<Workspace> {
5696 let (_, root_view) = cx.add_window(|_| EmptyView);
5697 cx.add_view(&root_view, |cx| {
5698 Workspace::new(project.clone(), |_, _| unimplemented!(), cx)
5699 })
5700 }
5701
5702 async fn simulate_host(
5703 mut self,
5704 project: ModelHandle<Project>,
5705 op_start_signal: futures::channel::mpsc::UnboundedReceiver<()>,
5706 rng: Arc<Mutex<StdRng>>,
5707 mut cx: TestAppContext,
5708 ) -> (
5709 Self,
5710 ModelHandle<Project>,
5711 TestAppContext,
5712 Option<anyhow::Error>,
5713 ) {
5714 async fn simulate_host_internal(
5715 client: &mut TestClient,
5716 project: ModelHandle<Project>,
5717 mut op_start_signal: futures::channel::mpsc::UnboundedReceiver<()>,
5718 rng: Arc<Mutex<StdRng>>,
5719 cx: &mut TestAppContext,
5720 ) -> anyhow::Result<()> {
5721 let fs = project.read_with(cx, |project, _| project.fs().clone());
5722
5723 cx.update(|cx| {
5724 cx.subscribe(&project, move |project, event, cx| {
5725 if let project::Event::ContactRequestedJoin(user) = event {
5726 log::info!("Host: accepting join request from {}", user.github_login);
5727 project.update(cx, |project, cx| {
5728 project.respond_to_join_request(user.id, true, cx)
5729 });
5730 }
5731 })
5732 .detach();
5733 });
5734
5735 while op_start_signal.next().await.is_some() {
5736 let distribution = rng.lock().gen_range::<usize, _>(0..100);
5737 let files = fs.as_fake().files().await;
5738 match distribution {
5739 0..=19 if !files.is_empty() => {
5740 let path = files.choose(&mut *rng.lock()).unwrap();
5741 let mut path = path.as_path();
5742 while let Some(parent_path) = path.parent() {
5743 path = parent_path;
5744 if rng.lock().gen() {
5745 break;
5746 }
5747 }
5748
5749 log::info!("Host: find/create local worktree {:?}", path);
5750 let find_or_create_worktree = project.update(cx, |project, cx| {
5751 project.find_or_create_local_worktree(path, true, cx)
5752 });
5753 if rng.lock().gen() {
5754 cx.background().spawn(find_or_create_worktree).detach();
5755 } else {
5756 find_or_create_worktree.await?;
5757 }
5758 }
5759 20..=79 if !files.is_empty() => {
5760 let buffer = if client.buffers.is_empty() || rng.lock().gen() {
5761 let file = files.choose(&mut *rng.lock()).unwrap();
5762 let (worktree, path) = project
5763 .update(cx, |project, cx| {
5764 project.find_or_create_local_worktree(file.clone(), true, cx)
5765 })
5766 .await?;
5767 let project_path =
5768 worktree.read_with(cx, |worktree, _| (worktree.id(), path));
5769 log::info!(
5770 "Host: opening path {:?}, worktree {}, relative_path {:?}",
5771 file,
5772 project_path.0,
5773 project_path.1
5774 );
5775 let buffer = project
5776 .update(cx, |project, cx| project.open_buffer(project_path, cx))
5777 .await
5778 .unwrap();
5779 client.buffers.insert(buffer.clone());
5780 buffer
5781 } else {
5782 client
5783 .buffers
5784 .iter()
5785 .choose(&mut *rng.lock())
5786 .unwrap()
5787 .clone()
5788 };
5789
5790 if rng.lock().gen_bool(0.1) {
5791 cx.update(|cx| {
5792 log::info!(
5793 "Host: dropping buffer {:?}",
5794 buffer.read(cx).file().unwrap().full_path(cx)
5795 );
5796 client.buffers.remove(&buffer);
5797 drop(buffer);
5798 });
5799 } else {
5800 buffer.update(cx, |buffer, cx| {
5801 log::info!(
5802 "Host: updating buffer {:?} ({})",
5803 buffer.file().unwrap().full_path(cx),
5804 buffer.remote_id()
5805 );
5806
5807 if rng.lock().gen_bool(0.7) {
5808 buffer.randomly_edit(&mut *rng.lock(), 5, cx);
5809 } else {
5810 buffer.randomly_undo_redo(&mut *rng.lock(), cx);
5811 }
5812 });
5813 }
5814 }
5815 _ => loop {
5816 let path_component_count = rng.lock().gen_range::<usize, _>(1..=5);
5817 let mut path = PathBuf::new();
5818 path.push("/");
5819 for _ in 0..path_component_count {
5820 let letter = rng.lock().gen_range(b'a'..=b'z');
5821 path.push(std::str::from_utf8(&[letter]).unwrap());
5822 }
5823 path.set_extension("rs");
5824 let parent_path = path.parent().unwrap();
5825
5826 log::info!("Host: creating file {:?}", path,);
5827
5828 if fs.create_dir(parent_path).await.is_ok()
5829 && fs.create_file(&path, Default::default()).await.is_ok()
5830 {
5831 break;
5832 } else {
5833 log::info!("Host: cannot create file");
5834 }
5835 },
5836 }
5837
5838 cx.background().simulate_random_delay().await;
5839 }
5840
5841 Ok(())
5842 }
5843
5844 let result =
5845 simulate_host_internal(&mut self, project.clone(), op_start_signal, rng, &mut cx).await;
5846 log::info!("Host done");
5847 (self, project, cx, result.err())
5848 }
5849
5850 pub async fn simulate_guest(
5851 mut self,
5852 guest_username: String,
5853 project: ModelHandle<Project>,
5854 op_start_signal: futures::channel::mpsc::UnboundedReceiver<()>,
5855 rng: Arc<Mutex<StdRng>>,
5856 mut cx: TestAppContext,
5857 ) -> (
5858 Self,
5859 ModelHandle<Project>,
5860 TestAppContext,
5861 Option<anyhow::Error>,
5862 ) {
5863 async fn simulate_guest_internal(
5864 client: &mut TestClient,
5865 guest_username: &str,
5866 project: ModelHandle<Project>,
5867 mut op_start_signal: futures::channel::mpsc::UnboundedReceiver<()>,
5868 rng: Arc<Mutex<StdRng>>,
5869 cx: &mut TestAppContext,
5870 ) -> anyhow::Result<()> {
5871 while op_start_signal.next().await.is_some() {
5872 let buffer = if client.buffers.is_empty() || rng.lock().gen() {
5873 let worktree = if let Some(worktree) = project.read_with(cx, |project, cx| {
5874 project
5875 .worktrees(cx)
5876 .filter(|worktree| {
5877 let worktree = worktree.read(cx);
5878 worktree.is_visible()
5879 && worktree.entries(false).any(|e| e.is_file())
5880 })
5881 .choose(&mut *rng.lock())
5882 }) {
5883 worktree
5884 } else {
5885 cx.background().simulate_random_delay().await;
5886 continue;
5887 };
5888
5889 let (worktree_root_name, project_path) =
5890 worktree.read_with(cx, |worktree, _| {
5891 let entry = worktree
5892 .entries(false)
5893 .filter(|e| e.is_file())
5894 .choose(&mut *rng.lock())
5895 .unwrap();
5896 (
5897 worktree.root_name().to_string(),
5898 (worktree.id(), entry.path.clone()),
5899 )
5900 });
5901 log::info!(
5902 "{}: opening path {:?} in worktree {} ({})",
5903 guest_username,
5904 project_path.1,
5905 project_path.0,
5906 worktree_root_name,
5907 );
5908 let buffer = project
5909 .update(cx, |project, cx| {
5910 project.open_buffer(project_path.clone(), cx)
5911 })
5912 .await?;
5913 log::info!(
5914 "{}: opened path {:?} in worktree {} ({}) with buffer id {}",
5915 guest_username,
5916 project_path.1,
5917 project_path.0,
5918 worktree_root_name,
5919 buffer.read_with(cx, |buffer, _| buffer.remote_id())
5920 );
5921 client.buffers.insert(buffer.clone());
5922 buffer
5923 } else {
5924 client
5925 .buffers
5926 .iter()
5927 .choose(&mut *rng.lock())
5928 .unwrap()
5929 .clone()
5930 };
5931
5932 let choice = rng.lock().gen_range(0..100);
5933 match choice {
5934 0..=9 => {
5935 cx.update(|cx| {
5936 log::info!(
5937 "{}: dropping buffer {:?}",
5938 guest_username,
5939 buffer.read(cx).file().unwrap().full_path(cx)
5940 );
5941 client.buffers.remove(&buffer);
5942 drop(buffer);
5943 });
5944 }
5945 10..=19 => {
5946 let completions = project.update(cx, |project, cx| {
5947 log::info!(
5948 "{}: requesting completions for buffer {} ({:?})",
5949 guest_username,
5950 buffer.read(cx).remote_id(),
5951 buffer.read(cx).file().unwrap().full_path(cx)
5952 );
5953 let offset = rng.lock().gen_range(0..=buffer.read(cx).len());
5954 project.completions(&buffer, offset, cx)
5955 });
5956 let completions = cx.background().spawn(async move {
5957 completions
5958 .await
5959 .map_err(|err| anyhow!("completions request failed: {:?}", err))
5960 });
5961 if rng.lock().gen_bool(0.3) {
5962 log::info!("{}: detaching completions request", guest_username);
5963 cx.update(|cx| completions.detach_and_log_err(cx));
5964 } else {
5965 completions.await?;
5966 }
5967 }
5968 20..=29 => {
5969 let code_actions = project.update(cx, |project, cx| {
5970 log::info!(
5971 "{}: requesting code actions for buffer {} ({:?})",
5972 guest_username,
5973 buffer.read(cx).remote_id(),
5974 buffer.read(cx).file().unwrap().full_path(cx)
5975 );
5976 let range = buffer.read(cx).random_byte_range(0, &mut *rng.lock());
5977 project.code_actions(&buffer, range, cx)
5978 });
5979 let code_actions = cx.background().spawn(async move {
5980 code_actions
5981 .await
5982 .map_err(|err| anyhow!("code actions request failed: {:?}", err))
5983 });
5984 if rng.lock().gen_bool(0.3) {
5985 log::info!("{}: detaching code actions request", guest_username);
5986 cx.update(|cx| code_actions.detach_and_log_err(cx));
5987 } else {
5988 code_actions.await?;
5989 }
5990 }
5991 30..=39 if buffer.read_with(cx, |buffer, _| buffer.is_dirty()) => {
5992 let (requested_version, save) = buffer.update(cx, |buffer, cx| {
5993 log::info!(
5994 "{}: saving buffer {} ({:?})",
5995 guest_username,
5996 buffer.remote_id(),
5997 buffer.file().unwrap().full_path(cx)
5998 );
5999 (buffer.version(), buffer.save(cx))
6000 });
6001 let save = cx.background().spawn(async move {
6002 let (saved_version, _, _) = save
6003 .await
6004 .map_err(|err| anyhow!("save request failed: {:?}", err))?;
6005 assert!(saved_version.observed_all(&requested_version));
6006 Ok::<_, anyhow::Error>(())
6007 });
6008 if rng.lock().gen_bool(0.3) {
6009 log::info!("{}: detaching save request", guest_username);
6010 cx.update(|cx| save.detach_and_log_err(cx));
6011 } else {
6012 save.await?;
6013 }
6014 }
6015 40..=44 => {
6016 let prepare_rename = project.update(cx, |project, cx| {
6017 log::info!(
6018 "{}: preparing rename for buffer {} ({:?})",
6019 guest_username,
6020 buffer.read(cx).remote_id(),
6021 buffer.read(cx).file().unwrap().full_path(cx)
6022 );
6023 let offset = rng.lock().gen_range(0..=buffer.read(cx).len());
6024 project.prepare_rename(buffer, offset, cx)
6025 });
6026 let prepare_rename = cx.background().spawn(async move {
6027 prepare_rename
6028 .await
6029 .map_err(|err| anyhow!("prepare rename request failed: {:?}", err))
6030 });
6031 if rng.lock().gen_bool(0.3) {
6032 log::info!("{}: detaching prepare rename request", guest_username);
6033 cx.update(|cx| prepare_rename.detach_and_log_err(cx));
6034 } else {
6035 prepare_rename.await?;
6036 }
6037 }
6038 45..=49 => {
6039 let definitions = project.update(cx, |project, cx| {
6040 log::info!(
6041 "{}: requesting definitions for buffer {} ({:?})",
6042 guest_username,
6043 buffer.read(cx).remote_id(),
6044 buffer.read(cx).file().unwrap().full_path(cx)
6045 );
6046 let offset = rng.lock().gen_range(0..=buffer.read(cx).len());
6047 project.definition(&buffer, offset, cx)
6048 });
6049 let definitions = cx.background().spawn(async move {
6050 definitions
6051 .await
6052 .map_err(|err| anyhow!("definitions request failed: {:?}", err))
6053 });
6054 if rng.lock().gen_bool(0.3) {
6055 log::info!("{}: detaching definitions request", guest_username);
6056 cx.update(|cx| definitions.detach_and_log_err(cx));
6057 } else {
6058 client.buffers.extend(
6059 definitions.await?.into_iter().map(|loc| loc.target.buffer),
6060 );
6061 }
6062 }
6063 50..=54 => {
6064 let highlights = project.update(cx, |project, cx| {
6065 log::info!(
6066 "{}: requesting highlights for buffer {} ({:?})",
6067 guest_username,
6068 buffer.read(cx).remote_id(),
6069 buffer.read(cx).file().unwrap().full_path(cx)
6070 );
6071 let offset = rng.lock().gen_range(0..=buffer.read(cx).len());
6072 project.document_highlights(&buffer, offset, cx)
6073 });
6074 let highlights = cx.background().spawn(async move {
6075 highlights
6076 .await
6077 .map_err(|err| anyhow!("highlights request failed: {:?}", err))
6078 });
6079 if rng.lock().gen_bool(0.3) {
6080 log::info!("{}: detaching highlights request", guest_username);
6081 cx.update(|cx| highlights.detach_and_log_err(cx));
6082 } else {
6083 highlights.await?;
6084 }
6085 }
6086 55..=59 => {
6087 let search = project.update(cx, |project, cx| {
6088 let query = rng.lock().gen_range('a'..='z');
6089 log::info!("{}: project-wide search {:?}", guest_username, query);
6090 project.search(SearchQuery::text(query, false, false), cx)
6091 });
6092 let search = cx.background().spawn(async move {
6093 search
6094 .await
6095 .map_err(|err| anyhow!("search request failed: {:?}", err))
6096 });
6097 if rng.lock().gen_bool(0.3) {
6098 log::info!("{}: detaching search request", guest_username);
6099 cx.update(|cx| search.detach_and_log_err(cx));
6100 } else {
6101 client.buffers.extend(search.await?.into_keys());
6102 }
6103 }
6104 60..=69 => {
6105 let worktree = project
6106 .read_with(cx, |project, cx| {
6107 project
6108 .worktrees(cx)
6109 .filter(|worktree| {
6110 let worktree = worktree.read(cx);
6111 worktree.is_visible()
6112 && worktree.entries(false).any(|e| e.is_file())
6113 && worktree.root_entry().map_or(false, |e| e.is_dir())
6114 })
6115 .choose(&mut *rng.lock())
6116 })
6117 .unwrap();
6118 let (worktree_id, worktree_root_name) = worktree
6119 .read_with(cx, |worktree, _| {
6120 (worktree.id(), worktree.root_name().to_string())
6121 });
6122
6123 let mut new_name = String::new();
6124 for _ in 0..10 {
6125 let letter = rng.lock().gen_range('a'..='z');
6126 new_name.push(letter);
6127 }
6128 let mut new_path = PathBuf::new();
6129 new_path.push(new_name);
6130 new_path.set_extension("rs");
6131 log::info!(
6132 "{}: creating {:?} in worktree {} ({})",
6133 guest_username,
6134 new_path,
6135 worktree_id,
6136 worktree_root_name,
6137 );
6138 project
6139 .update(cx, |project, cx| {
6140 project.create_entry((worktree_id, new_path), false, cx)
6141 })
6142 .unwrap()
6143 .await?;
6144 }
6145 _ => {
6146 buffer.update(cx, |buffer, cx| {
6147 log::info!(
6148 "{}: updating buffer {} ({:?})",
6149 guest_username,
6150 buffer.remote_id(),
6151 buffer.file().unwrap().full_path(cx)
6152 );
6153 if rng.lock().gen_bool(0.7) {
6154 buffer.randomly_edit(&mut *rng.lock(), 5, cx);
6155 } else {
6156 buffer.randomly_undo_redo(&mut *rng.lock(), cx);
6157 }
6158 });
6159 }
6160 }
6161 cx.background().simulate_random_delay().await;
6162 }
6163 Ok(())
6164 }
6165
6166 let result = simulate_guest_internal(
6167 &mut self,
6168 &guest_username,
6169 project.clone(),
6170 op_start_signal,
6171 rng,
6172 &mut cx,
6173 )
6174 .await;
6175 log::info!("{}: done", guest_username);
6176
6177 (self, project, cx, result.err())
6178 }
6179}
6180
6181impl Drop for TestClient {
6182 fn drop(&mut self) {
6183 self.client.tear_down();
6184 }
6185}
6186
6187impl Executor for Arc<gpui::executor::Background> {
6188 type Sleep = gpui::executor::Timer;
6189
6190 fn spawn_detached<F: 'static + Send + Future<Output = ()>>(&self, future: F) {
6191 self.spawn(future).detach();
6192 }
6193
6194 fn sleep(&self, duration: Duration) -> Self::Sleep {
6195 self.as_ref().timer(duration)
6196 }
6197}
6198
6199fn channel_messages(channel: &Channel) -> Vec<(String, String, bool)> {
6200 channel
6201 .messages()
6202 .cursor::<()>()
6203 .map(|m| {
6204 (
6205 m.sender.github_login.clone(),
6206 m.body.clone(),
6207 m.is_pending(),
6208 )
6209 })
6210 .collect()
6211}
6212
6213#[derive(Debug, Eq, PartialEq)]
6214struct RoomParticipants {
6215 remote: Vec<String>,
6216 pending: Vec<String>,
6217}
6218
6219async fn room_participants(
6220 room: &ModelHandle<Room>,
6221 client: &TestClient,
6222 cx: &mut TestAppContext,
6223) -> RoomParticipants {
6224 let remote_users = room.update(cx, |room, cx| {
6225 room.remote_participants()
6226 .values()
6227 .map(|participant| {
6228 client
6229 .user_store
6230 .update(cx, |users, cx| users.get_user(participant.user_id, cx))
6231 })
6232 .collect::<Vec<_>>()
6233 });
6234 let remote_users = futures::future::try_join_all(remote_users).await.unwrap();
6235 let pending_users = room.update(cx, |room, cx| {
6236 room.pending_user_ids()
6237 .iter()
6238 .map(|user_id| {
6239 client
6240 .user_store
6241 .update(cx, |users, cx| users.get_user(*user_id, cx))
6242 })
6243 .collect::<Vec<_>>()
6244 });
6245 let pending_users = futures::future::try_join_all(pending_users).await.unwrap();
6246
6247 RoomParticipants {
6248 remote: remote_users
6249 .into_iter()
6250 .map(|user| user.github_login.clone())
6251 .collect(),
6252 pending: pending_users
6253 .into_iter()
6254 .map(|user| user.github_login.clone())
6255 .collect(),
6256 }
6257}