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