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