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