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