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