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 project_c.read_with(cx_c, |project, cx| {
1476 assert_eq!(
1477 project.diagnostic_summaries(cx).collect::<Vec<_>>(),
1478 &[(
1479 ProjectPath {
1480 worktree_id,
1481 path: Arc::from(Path::new("a.rs")),
1482 },
1483 DiagnosticSummary {
1484 error_count: 1,
1485 warning_count: 0,
1486 ..Default::default()
1487 },
1488 )]
1489 )
1490 });
1491
1492 // Simulate a language server reporting more errors for a file.
1493 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
1494 lsp::PublishDiagnosticsParams {
1495 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
1496 version: None,
1497 diagnostics: vec![
1498 lsp::Diagnostic {
1499 severity: Some(lsp::DiagnosticSeverity::ERROR),
1500 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 7)),
1501 message: "message 1".to_string(),
1502 ..Default::default()
1503 },
1504 lsp::Diagnostic {
1505 severity: Some(lsp::DiagnosticSeverity::WARNING),
1506 range: lsp::Range::new(lsp::Position::new(0, 10), lsp::Position::new(0, 13)),
1507 message: "message 2".to_string(),
1508 ..Default::default()
1509 },
1510 ],
1511 },
1512 );
1513
1514 // Clients B and C get the updated summaries
1515 deterministic.run_until_parked();
1516 project_b.read_with(cx_b, |project, cx| {
1517 assert_eq!(
1518 project.diagnostic_summaries(cx).collect::<Vec<_>>(),
1519 [(
1520 ProjectPath {
1521 worktree_id,
1522 path: Arc::from(Path::new("a.rs")),
1523 },
1524 DiagnosticSummary {
1525 error_count: 1,
1526 warning_count: 1,
1527 ..Default::default()
1528 },
1529 )]
1530 );
1531 });
1532 project_c.read_with(cx_c, |project, cx| {
1533 assert_eq!(
1534 project.diagnostic_summaries(cx).collect::<Vec<_>>(),
1535 [(
1536 ProjectPath {
1537 worktree_id,
1538 path: Arc::from(Path::new("a.rs")),
1539 },
1540 DiagnosticSummary {
1541 error_count: 1,
1542 warning_count: 1,
1543 ..Default::default()
1544 },
1545 )]
1546 );
1547 });
1548
1549 // Open the file with the errors on client B. They should be present.
1550 let buffer_b = cx_b
1551 .background()
1552 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
1553 .await
1554 .unwrap();
1555
1556 buffer_b.read_with(cx_b, |buffer, _| {
1557 assert_eq!(
1558 buffer
1559 .snapshot()
1560 .diagnostics_in_range::<_, Point>(0..buffer.len(), false)
1561 .map(|entry| entry)
1562 .collect::<Vec<_>>(),
1563 &[
1564 DiagnosticEntry {
1565 range: Point::new(0, 4)..Point::new(0, 7),
1566 diagnostic: Diagnostic {
1567 group_id: 1,
1568 message: "message 1".to_string(),
1569 severity: lsp::DiagnosticSeverity::ERROR,
1570 is_primary: true,
1571 ..Default::default()
1572 }
1573 },
1574 DiagnosticEntry {
1575 range: Point::new(0, 10)..Point::new(0, 13),
1576 diagnostic: Diagnostic {
1577 group_id: 2,
1578 severity: lsp::DiagnosticSeverity::WARNING,
1579 message: "message 2".to_string(),
1580 is_primary: true,
1581 ..Default::default()
1582 }
1583 }
1584 ]
1585 );
1586 });
1587
1588 // Simulate a language server reporting no errors for a file.
1589 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
1590 lsp::PublishDiagnosticsParams {
1591 uri: lsp::Url::from_file_path("/a/a.rs").unwrap(),
1592 version: None,
1593 diagnostics: vec![],
1594 },
1595 );
1596 deterministic.run_until_parked();
1597 project_a.read_with(cx_a, |project, cx| {
1598 assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
1599 });
1600 project_b.read_with(cx_b, |project, cx| {
1601 assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
1602 });
1603 project_c.read_with(cx_c, |project, cx| {
1604 assert_eq!(project.diagnostic_summaries(cx).collect::<Vec<_>>(), [])
1605 });
1606}
1607
1608#[gpui::test(iterations = 10)]
1609async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
1610 cx_a.foreground().forbid_parking();
1611 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
1612 let client_a = server.create_client(cx_a, "user_a").await;
1613 let client_b = server.create_client(cx_b, "user_b").await;
1614 server
1615 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
1616 .await;
1617
1618 // Set up a fake language server.
1619 let mut language = Language::new(
1620 LanguageConfig {
1621 name: "Rust".into(),
1622 path_suffixes: vec!["rs".to_string()],
1623 ..Default::default()
1624 },
1625 Some(tree_sitter_rust::language()),
1626 );
1627 let mut fake_language_servers = language.set_fake_lsp_adapter(FakeLspAdapter {
1628 capabilities: lsp::ServerCapabilities {
1629 completion_provider: Some(lsp::CompletionOptions {
1630 trigger_characters: Some(vec![".".to_string()]),
1631 ..Default::default()
1632 }),
1633 ..Default::default()
1634 },
1635 ..Default::default()
1636 });
1637 client_a.language_registry.add(Arc::new(language));
1638
1639 client_a
1640 .fs
1641 .insert_tree(
1642 "/a",
1643 json!({
1644 "main.rs": "fn main() { a }",
1645 "other.rs": "",
1646 }),
1647 )
1648 .await;
1649 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1650 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
1651
1652 // Open a file in an editor as the guest.
1653 let buffer_b = project_b
1654 .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1655 .await
1656 .unwrap();
1657 let (window_b, _) = cx_b.add_window(|_| EmptyView);
1658 let editor_b = cx_b.add_view(window_b, |cx| {
1659 Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), cx)
1660 });
1661
1662 let fake_language_server = fake_language_servers.next().await.unwrap();
1663 buffer_b
1664 .condition(&cx_b, |buffer, _| !buffer.completion_triggers().is_empty())
1665 .await;
1666
1667 // Type a completion trigger character as the guest.
1668 editor_b.update(cx_b, |editor, cx| {
1669 editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
1670 editor.handle_input(&Input(".".into()), cx);
1671 cx.focus(&editor_b);
1672 });
1673
1674 // Receive a completion request as the host's language server.
1675 // Return some completions from the host's language server.
1676 cx_a.foreground().start_waiting();
1677 fake_language_server
1678 .handle_request::<lsp::request::Completion, _, _>(|params, _| async move {
1679 assert_eq!(
1680 params.text_document_position.text_document.uri,
1681 lsp::Url::from_file_path("/a/main.rs").unwrap(),
1682 );
1683 assert_eq!(
1684 params.text_document_position.position,
1685 lsp::Position::new(0, 14),
1686 );
1687
1688 Ok(Some(lsp::CompletionResponse::Array(vec![
1689 lsp::CompletionItem {
1690 label: "first_method(…)".into(),
1691 detail: Some("fn(&mut self, B) -> C".into()),
1692 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
1693 new_text: "first_method($1)".to_string(),
1694 range: lsp::Range::new(
1695 lsp::Position::new(0, 14),
1696 lsp::Position::new(0, 14),
1697 ),
1698 })),
1699 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
1700 ..Default::default()
1701 },
1702 lsp::CompletionItem {
1703 label: "second_method(…)".into(),
1704 detail: Some("fn(&mut self, C) -> D<E>".into()),
1705 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
1706 new_text: "second_method()".to_string(),
1707 range: lsp::Range::new(
1708 lsp::Position::new(0, 14),
1709 lsp::Position::new(0, 14),
1710 ),
1711 })),
1712 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
1713 ..Default::default()
1714 },
1715 ])))
1716 })
1717 .next()
1718 .await
1719 .unwrap();
1720 cx_a.foreground().finish_waiting();
1721
1722 // Open the buffer on the host.
1723 let buffer_a = project_a
1724 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
1725 .await
1726 .unwrap();
1727 buffer_a
1728 .condition(&cx_a, |buffer, _| buffer.text() == "fn main() { a. }")
1729 .await;
1730
1731 // Confirm a completion on the guest.
1732 editor_b
1733 .condition(&cx_b, |editor, _| editor.context_menu_visible())
1734 .await;
1735 editor_b.update(cx_b, |editor, cx| {
1736 editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, cx);
1737 assert_eq!(editor.text(cx), "fn main() { a.first_method() }");
1738 });
1739
1740 // Return a resolved completion from the host's language server.
1741 // The resolved completion has an additional text edit.
1742 fake_language_server.handle_request::<lsp::request::ResolveCompletionItem, _, _>(
1743 |params, _| async move {
1744 assert_eq!(params.label, "first_method(…)");
1745 Ok(lsp::CompletionItem {
1746 label: "first_method(…)".into(),
1747 detail: Some("fn(&mut self, B) -> C".into()),
1748 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
1749 new_text: "first_method($1)".to_string(),
1750 range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
1751 })),
1752 additional_text_edits: Some(vec![lsp::TextEdit {
1753 new_text: "use d::SomeTrait;\n".to_string(),
1754 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
1755 }]),
1756 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
1757 ..Default::default()
1758 })
1759 },
1760 );
1761
1762 // The additional edit is applied.
1763 buffer_a
1764 .condition(&cx_a, |buffer, _| {
1765 buffer.text() == "use d::SomeTrait;\nfn main() { a.first_method() }"
1766 })
1767 .await;
1768 buffer_b
1769 .condition(&cx_b, |buffer, _| {
1770 buffer.text() == "use d::SomeTrait;\nfn main() { a.first_method() }"
1771 })
1772 .await;
1773}
1774
1775#[gpui::test(iterations = 10)]
1776async fn test_reloading_buffer_manually(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
1777 cx_a.foreground().forbid_parking();
1778 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
1779 let client_a = server.create_client(cx_a, "user_a").await;
1780 let client_b = server.create_client(cx_b, "user_b").await;
1781 server
1782 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
1783 .await;
1784
1785 client_a
1786 .fs
1787 .insert_tree("/a", json!({ "a.rs": "let one = 1;" }))
1788 .await;
1789 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1790 let buffer_a = project_a
1791 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))
1792 .await
1793 .unwrap();
1794
1795 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
1796
1797 let buffer_b = cx_b
1798 .background()
1799 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
1800 .await
1801 .unwrap();
1802 buffer_b.update(cx_b, |buffer, cx| {
1803 buffer.edit([(4..7, "six")], cx);
1804 buffer.edit([(10..11, "6")], cx);
1805 assert_eq!(buffer.text(), "let six = 6;");
1806 assert!(buffer.is_dirty());
1807 assert!(!buffer.has_conflict());
1808 });
1809 buffer_a
1810 .condition(cx_a, |buffer, _| buffer.text() == "let six = 6;")
1811 .await;
1812
1813 client_a
1814 .fs
1815 .save("/a/a.rs".as_ref(), &Rope::from("let seven = 7;"))
1816 .await
1817 .unwrap();
1818 buffer_a
1819 .condition(cx_a, |buffer, _| buffer.has_conflict())
1820 .await;
1821 buffer_b
1822 .condition(cx_b, |buffer, _| buffer.has_conflict())
1823 .await;
1824
1825 project_b
1826 .update(cx_b, |project, cx| {
1827 project.reload_buffers(HashSet::from_iter([buffer_b.clone()]), true, cx)
1828 })
1829 .await
1830 .unwrap();
1831 buffer_a.read_with(cx_a, |buffer, _| {
1832 assert_eq!(buffer.text(), "let seven = 7;");
1833 assert!(!buffer.is_dirty());
1834 assert!(!buffer.has_conflict());
1835 });
1836 buffer_b.read_with(cx_b, |buffer, _| {
1837 assert_eq!(buffer.text(), "let seven = 7;");
1838 assert!(!buffer.is_dirty());
1839 assert!(!buffer.has_conflict());
1840 });
1841
1842 buffer_a.update(cx_a, |buffer, cx| {
1843 // Undoing on the host is a no-op when the reload was initiated by the guest.
1844 buffer.undo(cx);
1845 assert_eq!(buffer.text(), "let seven = 7;");
1846 assert!(!buffer.is_dirty());
1847 assert!(!buffer.has_conflict());
1848 });
1849 buffer_b.update(cx_b, |buffer, cx| {
1850 // Undoing on the guest rolls back the buffer to before it was reloaded but the conflict gets cleared.
1851 buffer.undo(cx);
1852 assert_eq!(buffer.text(), "let six = 6;");
1853 assert!(buffer.is_dirty());
1854 assert!(!buffer.has_conflict());
1855 });
1856}
1857
1858#[gpui::test(iterations = 10)]
1859async fn test_formatting_buffer(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
1860 cx_a.foreground().forbid_parking();
1861 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
1862 let client_a = server.create_client(cx_a, "user_a").await;
1863 let client_b = server.create_client(cx_b, "user_b").await;
1864 server
1865 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
1866 .await;
1867
1868 // Set up a fake language server.
1869 let mut language = Language::new(
1870 LanguageConfig {
1871 name: "Rust".into(),
1872 path_suffixes: vec!["rs".to_string()],
1873 ..Default::default()
1874 },
1875 Some(tree_sitter_rust::language()),
1876 );
1877 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default());
1878 client_a.language_registry.add(Arc::new(language));
1879
1880 client_a
1881 .fs
1882 .insert_tree("/a", json!({ "a.rs": "let one = two" }))
1883 .await;
1884 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
1885 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
1886
1887 let buffer_b = cx_b
1888 .background()
1889 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
1890 .await
1891 .unwrap();
1892
1893 let fake_language_server = fake_language_servers.next().await.unwrap();
1894 fake_language_server.handle_request::<lsp::request::Formatting, _, _>(|_, _| async move {
1895 Ok(Some(vec![
1896 lsp::TextEdit {
1897 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 4)),
1898 new_text: "h".to_string(),
1899 },
1900 lsp::TextEdit {
1901 range: lsp::Range::new(lsp::Position::new(0, 7), lsp::Position::new(0, 7)),
1902 new_text: "y".to_string(),
1903 },
1904 ]))
1905 });
1906
1907 project_b
1908 .update(cx_b, |project, cx| {
1909 project.format(HashSet::from_iter([buffer_b.clone()]), true, cx)
1910 })
1911 .await
1912 .unwrap();
1913 assert_eq!(
1914 buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
1915 "let honey = two"
1916 );
1917}
1918
1919#[gpui::test(iterations = 10)]
1920async fn test_definition(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
1921 cx_a.foreground().forbid_parking();
1922 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
1923 let client_a = server.create_client(cx_a, "user_a").await;
1924 let client_b = server.create_client(cx_b, "user_b").await;
1925 server
1926 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
1927 .await;
1928
1929 // Set up a fake language server.
1930 let mut language = Language::new(
1931 LanguageConfig {
1932 name: "Rust".into(),
1933 path_suffixes: vec!["rs".to_string()],
1934 ..Default::default()
1935 },
1936 Some(tree_sitter_rust::language()),
1937 );
1938 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default());
1939 client_a.language_registry.add(Arc::new(language));
1940
1941 client_a
1942 .fs
1943 .insert_tree(
1944 "/root",
1945 json!({
1946 "dir-1": {
1947 "a.rs": "const ONE: usize = b::TWO + b::THREE;",
1948 },
1949 "dir-2": {
1950 "b.rs": "const TWO: usize = 2;\nconst THREE: usize = 3;",
1951 }
1952 }),
1953 )
1954 .await;
1955 let (project_a, worktree_id) = client_a.build_local_project("/root/dir-1", cx_a).await;
1956 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
1957
1958 // Open the file on client B.
1959 let buffer_b = cx_b
1960 .background()
1961 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
1962 .await
1963 .unwrap();
1964
1965 // Request the definition of a symbol as the guest.
1966 let fake_language_server = fake_language_servers.next().await.unwrap();
1967 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
1968 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
1969 lsp::Location::new(
1970 lsp::Url::from_file_path("/root/dir-2/b.rs").unwrap(),
1971 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
1972 ),
1973 )))
1974 });
1975
1976 let definitions_1 = project_b
1977 .update(cx_b, |p, cx| p.definition(&buffer_b, 23, cx))
1978 .await
1979 .unwrap();
1980 cx_b.read(|cx| {
1981 assert_eq!(definitions_1.len(), 1);
1982 assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
1983 let target_buffer = definitions_1[0].target.buffer.read(cx);
1984 assert_eq!(
1985 target_buffer.text(),
1986 "const TWO: usize = 2;\nconst THREE: usize = 3;"
1987 );
1988 assert_eq!(
1989 definitions_1[0].target.range.to_point(target_buffer),
1990 Point::new(0, 6)..Point::new(0, 9)
1991 );
1992 });
1993
1994 // Try getting more definitions for the same buffer, ensuring the buffer gets reused from
1995 // the previous call to `definition`.
1996 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
1997 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
1998 lsp::Location::new(
1999 lsp::Url::from_file_path("/root/dir-2/b.rs").unwrap(),
2000 lsp::Range::new(lsp::Position::new(1, 6), lsp::Position::new(1, 11)),
2001 ),
2002 )))
2003 });
2004
2005 let definitions_2 = project_b
2006 .update(cx_b, |p, cx| p.definition(&buffer_b, 33, cx))
2007 .await
2008 .unwrap();
2009 cx_b.read(|cx| {
2010 assert_eq!(definitions_2.len(), 1);
2011 assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
2012 let target_buffer = definitions_2[0].target.buffer.read(cx);
2013 assert_eq!(
2014 target_buffer.text(),
2015 "const TWO: usize = 2;\nconst THREE: usize = 3;"
2016 );
2017 assert_eq!(
2018 definitions_2[0].target.range.to_point(target_buffer),
2019 Point::new(1, 6)..Point::new(1, 11)
2020 );
2021 });
2022 assert_eq!(
2023 definitions_1[0].target.buffer,
2024 definitions_2[0].target.buffer
2025 );
2026}
2027
2028#[gpui::test(iterations = 10)]
2029async fn test_references(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2030 cx_a.foreground().forbid_parking();
2031 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
2032 let client_a = server.create_client(cx_a, "user_a").await;
2033 let client_b = server.create_client(cx_b, "user_b").await;
2034 server
2035 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
2036 .await;
2037
2038 // Set up a fake language server.
2039 let mut language = Language::new(
2040 LanguageConfig {
2041 name: "Rust".into(),
2042 path_suffixes: vec!["rs".to_string()],
2043 ..Default::default()
2044 },
2045 Some(tree_sitter_rust::language()),
2046 );
2047 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default());
2048 client_a.language_registry.add(Arc::new(language));
2049
2050 client_a
2051 .fs
2052 .insert_tree(
2053 "/root",
2054 json!({
2055 "dir-1": {
2056 "one.rs": "const ONE: usize = 1;",
2057 "two.rs": "const TWO: usize = one::ONE + one::ONE;",
2058 },
2059 "dir-2": {
2060 "three.rs": "const THREE: usize = two::TWO + one::ONE;",
2061 }
2062 }),
2063 )
2064 .await;
2065 let (project_a, worktree_id) = client_a.build_local_project("/root/dir-1", cx_a).await;
2066 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
2067
2068 // Open the file on client B.
2069 let buffer_b = cx_b
2070 .background()
2071 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx)))
2072 .await
2073 .unwrap();
2074
2075 // Request references to a symbol as the guest.
2076 let fake_language_server = fake_language_servers.next().await.unwrap();
2077 fake_language_server.handle_request::<lsp::request::References, _, _>(|params, _| async move {
2078 assert_eq!(
2079 params.text_document_position.text_document.uri.as_str(),
2080 "file:///root/dir-1/one.rs"
2081 );
2082 Ok(Some(vec![
2083 lsp::Location {
2084 uri: lsp::Url::from_file_path("/root/dir-1/two.rs").unwrap(),
2085 range: lsp::Range::new(lsp::Position::new(0, 24), lsp::Position::new(0, 27)),
2086 },
2087 lsp::Location {
2088 uri: lsp::Url::from_file_path("/root/dir-1/two.rs").unwrap(),
2089 range: lsp::Range::new(lsp::Position::new(0, 35), lsp::Position::new(0, 38)),
2090 },
2091 lsp::Location {
2092 uri: lsp::Url::from_file_path("/root/dir-2/three.rs").unwrap(),
2093 range: lsp::Range::new(lsp::Position::new(0, 37), lsp::Position::new(0, 40)),
2094 },
2095 ]))
2096 });
2097
2098 let references = project_b
2099 .update(cx_b, |p, cx| p.references(&buffer_b, 7, cx))
2100 .await
2101 .unwrap();
2102 cx_b.read(|cx| {
2103 assert_eq!(references.len(), 3);
2104 assert_eq!(project_b.read(cx).worktrees(cx).count(), 2);
2105
2106 let two_buffer = references[0].buffer.read(cx);
2107 let three_buffer = references[2].buffer.read(cx);
2108 assert_eq!(
2109 two_buffer.file().unwrap().path().as_ref(),
2110 Path::new("two.rs")
2111 );
2112 assert_eq!(references[1].buffer, references[0].buffer);
2113 assert_eq!(
2114 three_buffer.file().unwrap().full_path(cx),
2115 Path::new("three.rs")
2116 );
2117
2118 assert_eq!(references[0].range.to_offset(&two_buffer), 24..27);
2119 assert_eq!(references[1].range.to_offset(&two_buffer), 35..38);
2120 assert_eq!(references[2].range.to_offset(&three_buffer), 37..40);
2121 });
2122}
2123
2124#[gpui::test(iterations = 10)]
2125async fn test_project_search(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2126 cx_a.foreground().forbid_parking();
2127 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
2128 let client_a = server.create_client(cx_a, "user_a").await;
2129 let client_b = server.create_client(cx_b, "user_b").await;
2130 server
2131 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
2132 .await;
2133
2134 client_a
2135 .fs
2136 .insert_tree(
2137 "/root",
2138 json!({
2139 "dir-1": {
2140 "a": "hello world",
2141 "b": "goodnight moon",
2142 "c": "a world of goo",
2143 "d": "world champion of clown world",
2144 },
2145 "dir-2": {
2146 "e": "disney world is fun",
2147 }
2148 }),
2149 )
2150 .await;
2151 let (project_a, _) = client_a.build_local_project("/root/dir-1", cx_a).await;
2152 let (worktree_2, _) = project_a
2153 .update(cx_a, |p, cx| {
2154 p.find_or_create_local_worktree("/root/dir-2", true, cx)
2155 })
2156 .await
2157 .unwrap();
2158 worktree_2
2159 .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
2160 .await;
2161
2162 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
2163
2164 // Perform a search as the guest.
2165 let results = project_b
2166 .update(cx_b, |project, cx| {
2167 project.search(SearchQuery::text("world", false, false), cx)
2168 })
2169 .await
2170 .unwrap();
2171
2172 let mut ranges_by_path = results
2173 .into_iter()
2174 .map(|(buffer, ranges)| {
2175 buffer.read_with(cx_b, |buffer, cx| {
2176 let path = buffer.file().unwrap().full_path(cx);
2177 let offset_ranges = ranges
2178 .into_iter()
2179 .map(|range| range.to_offset(buffer))
2180 .collect::<Vec<_>>();
2181 (path, offset_ranges)
2182 })
2183 })
2184 .collect::<Vec<_>>();
2185 ranges_by_path.sort_by_key(|(path, _)| path.clone());
2186
2187 assert_eq!(
2188 ranges_by_path,
2189 &[
2190 (PathBuf::from("dir-1/a"), vec![6..11]),
2191 (PathBuf::from("dir-1/c"), vec![2..7]),
2192 (PathBuf::from("dir-1/d"), vec![0..5, 24..29]),
2193 (PathBuf::from("dir-2/e"), vec![7..12]),
2194 ]
2195 );
2196}
2197
2198#[gpui::test(iterations = 10)]
2199async fn test_document_highlights(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2200 cx_a.foreground().forbid_parking();
2201 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
2202 let client_a = server.create_client(cx_a, "user_a").await;
2203 let client_b = server.create_client(cx_b, "user_b").await;
2204 server
2205 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
2206 .await;
2207
2208 client_a
2209 .fs
2210 .insert_tree(
2211 "/root-1",
2212 json!({
2213 "main.rs": "fn double(number: i32) -> i32 { number + number }",
2214 }),
2215 )
2216 .await;
2217
2218 // Set up a fake language server.
2219 let mut language = Language::new(
2220 LanguageConfig {
2221 name: "Rust".into(),
2222 path_suffixes: vec!["rs".to_string()],
2223 ..Default::default()
2224 },
2225 Some(tree_sitter_rust::language()),
2226 );
2227 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default());
2228 client_a.language_registry.add(Arc::new(language));
2229
2230 let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
2231 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
2232
2233 // Open the file on client B.
2234 let buffer_b = cx_b
2235 .background()
2236 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)))
2237 .await
2238 .unwrap();
2239
2240 // Request document highlights as the guest.
2241 let fake_language_server = fake_language_servers.next().await.unwrap();
2242 fake_language_server.handle_request::<lsp::request::DocumentHighlightRequest, _, _>(
2243 |params, _| async move {
2244 assert_eq!(
2245 params
2246 .text_document_position_params
2247 .text_document
2248 .uri
2249 .as_str(),
2250 "file:///root-1/main.rs"
2251 );
2252 assert_eq!(
2253 params.text_document_position_params.position,
2254 lsp::Position::new(0, 34)
2255 );
2256 Ok(Some(vec![
2257 lsp::DocumentHighlight {
2258 kind: Some(lsp::DocumentHighlightKind::WRITE),
2259 range: lsp::Range::new(lsp::Position::new(0, 10), lsp::Position::new(0, 16)),
2260 },
2261 lsp::DocumentHighlight {
2262 kind: Some(lsp::DocumentHighlightKind::READ),
2263 range: lsp::Range::new(lsp::Position::new(0, 32), lsp::Position::new(0, 38)),
2264 },
2265 lsp::DocumentHighlight {
2266 kind: Some(lsp::DocumentHighlightKind::READ),
2267 range: lsp::Range::new(lsp::Position::new(0, 41), lsp::Position::new(0, 47)),
2268 },
2269 ]))
2270 },
2271 );
2272
2273 let highlights = project_b
2274 .update(cx_b, |p, cx| p.document_highlights(&buffer_b, 34, cx))
2275 .await
2276 .unwrap();
2277 buffer_b.read_with(cx_b, |buffer, _| {
2278 let snapshot = buffer.snapshot();
2279
2280 let highlights = highlights
2281 .into_iter()
2282 .map(|highlight| (highlight.kind, highlight.range.to_offset(&snapshot)))
2283 .collect::<Vec<_>>();
2284 assert_eq!(
2285 highlights,
2286 &[
2287 (lsp::DocumentHighlightKind::WRITE, 10..16),
2288 (lsp::DocumentHighlightKind::READ, 32..38),
2289 (lsp::DocumentHighlightKind::READ, 41..47)
2290 ]
2291 )
2292 });
2293}
2294
2295#[gpui::test(iterations = 10)]
2296async fn test_lsp_hover(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2297 cx_a.foreground().forbid_parking();
2298 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
2299 let client_a = server.create_client(cx_a, "user_a").await;
2300 let client_b = server.create_client(cx_b, "user_b").await;
2301 server
2302 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
2303 .await;
2304
2305 client_a
2306 .fs
2307 .insert_tree(
2308 "/root-1",
2309 json!({
2310 "main.rs": "use std::collections::HashMap;",
2311 }),
2312 )
2313 .await;
2314
2315 // Set up a fake language server.
2316 let mut language = Language::new(
2317 LanguageConfig {
2318 name: "Rust".into(),
2319 path_suffixes: vec!["rs".to_string()],
2320 ..Default::default()
2321 },
2322 Some(tree_sitter_rust::language()),
2323 );
2324 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default());
2325 client_a.language_registry.add(Arc::new(language));
2326
2327 let (project_a, worktree_id) = client_a.build_local_project("/root-1", cx_a).await;
2328 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
2329
2330 // Open the file as the guest
2331 let buffer_b = cx_b
2332 .background()
2333 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)))
2334 .await
2335 .unwrap();
2336
2337 // Request hover information as the guest.
2338 let fake_language_server = fake_language_servers.next().await.unwrap();
2339 fake_language_server.handle_request::<lsp::request::HoverRequest, _, _>(
2340 |params, _| async move {
2341 assert_eq!(
2342 params
2343 .text_document_position_params
2344 .text_document
2345 .uri
2346 .as_str(),
2347 "file:///root-1/main.rs"
2348 );
2349 assert_eq!(
2350 params.text_document_position_params.position,
2351 lsp::Position::new(0, 22)
2352 );
2353 Ok(Some(lsp::Hover {
2354 contents: lsp::HoverContents::Array(vec![
2355 lsp::MarkedString::String("Test hover content.".to_string()),
2356 lsp::MarkedString::LanguageString(lsp::LanguageString {
2357 language: "Rust".to_string(),
2358 value: "let foo = 42;".to_string(),
2359 }),
2360 ]),
2361 range: Some(lsp::Range::new(
2362 lsp::Position::new(0, 22),
2363 lsp::Position::new(0, 29),
2364 )),
2365 }))
2366 },
2367 );
2368
2369 let hover_info = project_b
2370 .update(cx_b, |p, cx| p.hover(&buffer_b, 22, cx))
2371 .await
2372 .unwrap()
2373 .unwrap();
2374 buffer_b.read_with(cx_b, |buffer, _| {
2375 let snapshot = buffer.snapshot();
2376 assert_eq!(hover_info.range.unwrap().to_offset(&snapshot), 22..29);
2377 assert_eq!(
2378 hover_info.contents,
2379 vec![
2380 project::HoverBlock {
2381 text: "Test hover content.".to_string(),
2382 language: None,
2383 },
2384 project::HoverBlock {
2385 text: "let foo = 42;".to_string(),
2386 language: Some("Rust".to_string()),
2387 }
2388 ]
2389 );
2390 });
2391}
2392
2393#[gpui::test(iterations = 10)]
2394async fn test_project_symbols(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2395 cx_a.foreground().forbid_parking();
2396 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
2397 let client_a = server.create_client(cx_a, "user_a").await;
2398 let client_b = server.create_client(cx_b, "user_b").await;
2399 server
2400 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
2401 .await;
2402
2403 // Set up a fake language server.
2404 let mut language = Language::new(
2405 LanguageConfig {
2406 name: "Rust".into(),
2407 path_suffixes: vec!["rs".to_string()],
2408 ..Default::default()
2409 },
2410 Some(tree_sitter_rust::language()),
2411 );
2412 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default());
2413 client_a.language_registry.add(Arc::new(language));
2414
2415 client_a
2416 .fs
2417 .insert_tree(
2418 "/code",
2419 json!({
2420 "crate-1": {
2421 "one.rs": "const ONE: usize = 1;",
2422 },
2423 "crate-2": {
2424 "two.rs": "const TWO: usize = 2; const THREE: usize = 3;",
2425 },
2426 "private": {
2427 "passwords.txt": "the-password",
2428 }
2429 }),
2430 )
2431 .await;
2432 let (project_a, worktree_id) = client_a.build_local_project("/code/crate-1", cx_a).await;
2433 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
2434
2435 // Cause the language server to start.
2436 let _buffer = cx_b
2437 .background()
2438 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx)))
2439 .await
2440 .unwrap();
2441
2442 let fake_language_server = fake_language_servers.next().await.unwrap();
2443 fake_language_server.handle_request::<lsp::request::WorkspaceSymbol, _, _>(|_, _| async move {
2444 #[allow(deprecated)]
2445 Ok(Some(vec![lsp::SymbolInformation {
2446 name: "TWO".into(),
2447 location: lsp::Location {
2448 uri: lsp::Url::from_file_path("/code/crate-2/two.rs").unwrap(),
2449 range: lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
2450 },
2451 kind: lsp::SymbolKind::CONSTANT,
2452 tags: None,
2453 container_name: None,
2454 deprecated: None,
2455 }]))
2456 });
2457
2458 // Request the definition of a symbol as the guest.
2459 let symbols = project_b
2460 .update(cx_b, |p, cx| p.symbols("two", cx))
2461 .await
2462 .unwrap();
2463 assert_eq!(symbols.len(), 1);
2464 assert_eq!(symbols[0].name, "TWO");
2465
2466 // Open one of the returned symbols.
2467 let buffer_b_2 = project_b
2468 .update(cx_b, |project, cx| {
2469 project.open_buffer_for_symbol(&symbols[0], cx)
2470 })
2471 .await
2472 .unwrap();
2473 buffer_b_2.read_with(cx_b, |buffer, _| {
2474 assert_eq!(
2475 buffer.file().unwrap().path().as_ref(),
2476 Path::new("../crate-2/two.rs")
2477 );
2478 });
2479
2480 // Attempt to craft a symbol and violate host's privacy by opening an arbitrary file.
2481 let mut fake_symbol = symbols[0].clone();
2482 fake_symbol.path = Path::new("/code/secrets").into();
2483 let error = project_b
2484 .update(cx_b, |project, cx| {
2485 project.open_buffer_for_symbol(&fake_symbol, cx)
2486 })
2487 .await
2488 .unwrap_err();
2489 assert!(error.to_string().contains("invalid symbol signature"));
2490}
2491
2492#[gpui::test(iterations = 10)]
2493async fn test_open_buffer_while_getting_definition_pointing_to_it(
2494 cx_a: &mut TestAppContext,
2495 cx_b: &mut TestAppContext,
2496 mut rng: StdRng,
2497) {
2498 cx_a.foreground().forbid_parking();
2499 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
2500 let client_a = server.create_client(cx_a, "user_a").await;
2501 let client_b = server.create_client(cx_b, "user_b").await;
2502 server
2503 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
2504 .await;
2505
2506 // Set up a fake language server.
2507 let mut language = Language::new(
2508 LanguageConfig {
2509 name: "Rust".into(),
2510 path_suffixes: vec!["rs".to_string()],
2511 ..Default::default()
2512 },
2513 Some(tree_sitter_rust::language()),
2514 );
2515 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default());
2516 client_a.language_registry.add(Arc::new(language));
2517
2518 client_a
2519 .fs
2520 .insert_tree(
2521 "/root",
2522 json!({
2523 "a.rs": "const ONE: usize = b::TWO;",
2524 "b.rs": "const TWO: usize = 2",
2525 }),
2526 )
2527 .await;
2528 let (project_a, worktree_id) = client_a.build_local_project("/root", cx_a).await;
2529 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
2530
2531 let buffer_b1 = cx_b
2532 .background()
2533 .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
2534 .await
2535 .unwrap();
2536
2537 let fake_language_server = fake_language_servers.next().await.unwrap();
2538 fake_language_server.handle_request::<lsp::request::GotoDefinition, _, _>(|_, _| async move {
2539 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
2540 lsp::Location::new(
2541 lsp::Url::from_file_path("/root/b.rs").unwrap(),
2542 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
2543 ),
2544 )))
2545 });
2546
2547 let definitions;
2548 let buffer_b2;
2549 if rng.gen() {
2550 definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
2551 buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx));
2552 } else {
2553 buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx));
2554 definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx));
2555 }
2556
2557 let buffer_b2 = buffer_b2.await.unwrap();
2558 let definitions = definitions.await.unwrap();
2559 assert_eq!(definitions.len(), 1);
2560 assert_eq!(definitions[0].target.buffer, buffer_b2);
2561}
2562
2563#[gpui::test(iterations = 10)]
2564async fn test_collaborating_with_code_actions(
2565 cx_a: &mut TestAppContext,
2566 cx_b: &mut TestAppContext,
2567) {
2568 cx_a.foreground().forbid_parking();
2569 cx_b.update(|cx| editor::init(cx));
2570 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
2571 let client_a = server.create_client(cx_a, "user_a").await;
2572 let client_b = server.create_client(cx_b, "user_b").await;
2573 server
2574 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
2575 .await;
2576
2577 // Set up a fake language server.
2578 let mut language = Language::new(
2579 LanguageConfig {
2580 name: "Rust".into(),
2581 path_suffixes: vec!["rs".to_string()],
2582 ..Default::default()
2583 },
2584 Some(tree_sitter_rust::language()),
2585 );
2586 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default());
2587 client_a.language_registry.add(Arc::new(language));
2588
2589 client_a
2590 .fs
2591 .insert_tree(
2592 "/a",
2593 json!({
2594 "main.rs": "mod other;\nfn main() { let foo = other::foo(); }",
2595 "other.rs": "pub fn foo() -> usize { 4 }",
2596 }),
2597 )
2598 .await;
2599 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
2600
2601 // Join the project as client B.
2602 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
2603 let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(project_b.clone(), cx));
2604 let editor_b = workspace_b
2605 .update(cx_b, |workspace, cx| {
2606 workspace.open_path((worktree_id, "main.rs"), true, cx)
2607 })
2608 .await
2609 .unwrap()
2610 .downcast::<Editor>()
2611 .unwrap();
2612
2613 let mut fake_language_server = fake_language_servers.next().await.unwrap();
2614 fake_language_server
2615 .handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
2616 assert_eq!(
2617 params.text_document.uri,
2618 lsp::Url::from_file_path("/a/main.rs").unwrap(),
2619 );
2620 assert_eq!(params.range.start, lsp::Position::new(0, 0));
2621 assert_eq!(params.range.end, lsp::Position::new(0, 0));
2622 Ok(None)
2623 })
2624 .next()
2625 .await;
2626
2627 // Move cursor to a location that contains code actions.
2628 editor_b.update(cx_b, |editor, cx| {
2629 editor.change_selections(None, cx, |s| {
2630 s.select_ranges([Point::new(1, 31)..Point::new(1, 31)])
2631 });
2632 cx.focus(&editor_b);
2633 });
2634
2635 fake_language_server
2636 .handle_request::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
2637 assert_eq!(
2638 params.text_document.uri,
2639 lsp::Url::from_file_path("/a/main.rs").unwrap(),
2640 );
2641 assert_eq!(params.range.start, lsp::Position::new(1, 31));
2642 assert_eq!(params.range.end, lsp::Position::new(1, 31));
2643
2644 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
2645 lsp::CodeAction {
2646 title: "Inline into all callers".to_string(),
2647 edit: Some(lsp::WorkspaceEdit {
2648 changes: Some(
2649 [
2650 (
2651 lsp::Url::from_file_path("/a/main.rs").unwrap(),
2652 vec![lsp::TextEdit::new(
2653 lsp::Range::new(
2654 lsp::Position::new(1, 22),
2655 lsp::Position::new(1, 34),
2656 ),
2657 "4".to_string(),
2658 )],
2659 ),
2660 (
2661 lsp::Url::from_file_path("/a/other.rs").unwrap(),
2662 vec![lsp::TextEdit::new(
2663 lsp::Range::new(
2664 lsp::Position::new(0, 0),
2665 lsp::Position::new(0, 27),
2666 ),
2667 "".to_string(),
2668 )],
2669 ),
2670 ]
2671 .into_iter()
2672 .collect(),
2673 ),
2674 ..Default::default()
2675 }),
2676 data: Some(json!({
2677 "codeActionParams": {
2678 "range": {
2679 "start": {"line": 1, "column": 31},
2680 "end": {"line": 1, "column": 31},
2681 }
2682 }
2683 })),
2684 ..Default::default()
2685 },
2686 )]))
2687 })
2688 .next()
2689 .await;
2690
2691 // Toggle code actions and wait for them to display.
2692 editor_b.update(cx_b, |editor, cx| {
2693 editor.toggle_code_actions(
2694 &ToggleCodeActions {
2695 deployed_from_indicator: false,
2696 },
2697 cx,
2698 );
2699 });
2700 editor_b
2701 .condition(&cx_b, |editor, _| editor.context_menu_visible())
2702 .await;
2703
2704 fake_language_server.remove_request_handler::<lsp::request::CodeActionRequest>();
2705
2706 // Confirming the code action will trigger a resolve request.
2707 let confirm_action = workspace_b
2708 .update(cx_b, |workspace, cx| {
2709 Editor::confirm_code_action(workspace, &ConfirmCodeAction { item_ix: Some(0) }, cx)
2710 })
2711 .unwrap();
2712 fake_language_server.handle_request::<lsp::request::CodeActionResolveRequest, _, _>(
2713 |_, _| async move {
2714 Ok(lsp::CodeAction {
2715 title: "Inline into all callers".to_string(),
2716 edit: Some(lsp::WorkspaceEdit {
2717 changes: Some(
2718 [
2719 (
2720 lsp::Url::from_file_path("/a/main.rs").unwrap(),
2721 vec![lsp::TextEdit::new(
2722 lsp::Range::new(
2723 lsp::Position::new(1, 22),
2724 lsp::Position::new(1, 34),
2725 ),
2726 "4".to_string(),
2727 )],
2728 ),
2729 (
2730 lsp::Url::from_file_path("/a/other.rs").unwrap(),
2731 vec![lsp::TextEdit::new(
2732 lsp::Range::new(
2733 lsp::Position::new(0, 0),
2734 lsp::Position::new(0, 27),
2735 ),
2736 "".to_string(),
2737 )],
2738 ),
2739 ]
2740 .into_iter()
2741 .collect(),
2742 ),
2743 ..Default::default()
2744 }),
2745 ..Default::default()
2746 })
2747 },
2748 );
2749
2750 // After the action is confirmed, an editor containing both modified files is opened.
2751 confirm_action.await.unwrap();
2752 let code_action_editor = workspace_b.read_with(cx_b, |workspace, cx| {
2753 workspace
2754 .active_item(cx)
2755 .unwrap()
2756 .downcast::<Editor>()
2757 .unwrap()
2758 });
2759 code_action_editor.update(cx_b, |editor, cx| {
2760 assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
2761 editor.undo(&Undo, cx);
2762 assert_eq!(
2763 editor.text(cx),
2764 "mod other;\nfn main() { let foo = other::foo(); }\npub fn foo() -> usize { 4 }"
2765 );
2766 editor.redo(&Redo, cx);
2767 assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
2768 });
2769}
2770
2771#[gpui::test(iterations = 10)]
2772async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2773 cx_a.foreground().forbid_parking();
2774 cx_b.update(|cx| editor::init(cx));
2775 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
2776 let client_a = server.create_client(cx_a, "user_a").await;
2777 let client_b = server.create_client(cx_b, "user_b").await;
2778 server
2779 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
2780 .await;
2781
2782 // Set up a fake language server.
2783 let mut language = Language::new(
2784 LanguageConfig {
2785 name: "Rust".into(),
2786 path_suffixes: vec!["rs".to_string()],
2787 ..Default::default()
2788 },
2789 Some(tree_sitter_rust::language()),
2790 );
2791 let mut fake_language_servers = language.set_fake_lsp_adapter(FakeLspAdapter {
2792 capabilities: lsp::ServerCapabilities {
2793 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
2794 prepare_provider: Some(true),
2795 work_done_progress_options: Default::default(),
2796 })),
2797 ..Default::default()
2798 },
2799 ..Default::default()
2800 });
2801 client_a.language_registry.add(Arc::new(language));
2802
2803 client_a
2804 .fs
2805 .insert_tree(
2806 "/dir",
2807 json!({
2808 "one.rs": "const ONE: usize = 1;",
2809 "two.rs": "const TWO: usize = one::ONE + one::ONE;"
2810 }),
2811 )
2812 .await;
2813 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
2814 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
2815
2816 let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(project_b.clone(), cx));
2817 let editor_b = workspace_b
2818 .update(cx_b, |workspace, cx| {
2819 workspace.open_path((worktree_id, "one.rs"), true, cx)
2820 })
2821 .await
2822 .unwrap()
2823 .downcast::<Editor>()
2824 .unwrap();
2825 let fake_language_server = fake_language_servers.next().await.unwrap();
2826
2827 // Move cursor to a location that can be renamed.
2828 let prepare_rename = editor_b.update(cx_b, |editor, cx| {
2829 editor.change_selections(None, cx, |s| s.select_ranges([7..7]));
2830 editor.rename(&Rename, cx).unwrap()
2831 });
2832
2833 fake_language_server
2834 .handle_request::<lsp::request::PrepareRenameRequest, _, _>(|params, _| async move {
2835 assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs");
2836 assert_eq!(params.position, lsp::Position::new(0, 7));
2837 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
2838 lsp::Position::new(0, 6),
2839 lsp::Position::new(0, 9),
2840 ))))
2841 })
2842 .next()
2843 .await
2844 .unwrap();
2845 prepare_rename.await.unwrap();
2846 editor_b.update(cx_b, |editor, cx| {
2847 let rename = editor.pending_rename().unwrap();
2848 let buffer = editor.buffer().read(cx).snapshot(cx);
2849 assert_eq!(
2850 rename.range.start.to_offset(&buffer)..rename.range.end.to_offset(&buffer),
2851 6..9
2852 );
2853 rename.editor.update(cx, |rename_editor, cx| {
2854 rename_editor.buffer().update(cx, |rename_buffer, cx| {
2855 rename_buffer.edit([(0..3, "THREE")], cx);
2856 });
2857 });
2858 });
2859
2860 let confirm_rename = workspace_b.update(cx_b, |workspace, cx| {
2861 Editor::confirm_rename(workspace, &ConfirmRename, cx).unwrap()
2862 });
2863 fake_language_server
2864 .handle_request::<lsp::request::Rename, _, _>(|params, _| async move {
2865 assert_eq!(
2866 params.text_document_position.text_document.uri.as_str(),
2867 "file:///dir/one.rs"
2868 );
2869 assert_eq!(
2870 params.text_document_position.position,
2871 lsp::Position::new(0, 6)
2872 );
2873 assert_eq!(params.new_name, "THREE");
2874 Ok(Some(lsp::WorkspaceEdit {
2875 changes: Some(
2876 [
2877 (
2878 lsp::Url::from_file_path("/dir/one.rs").unwrap(),
2879 vec![lsp::TextEdit::new(
2880 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
2881 "THREE".to_string(),
2882 )],
2883 ),
2884 (
2885 lsp::Url::from_file_path("/dir/two.rs").unwrap(),
2886 vec![
2887 lsp::TextEdit::new(
2888 lsp::Range::new(
2889 lsp::Position::new(0, 24),
2890 lsp::Position::new(0, 27),
2891 ),
2892 "THREE".to_string(),
2893 ),
2894 lsp::TextEdit::new(
2895 lsp::Range::new(
2896 lsp::Position::new(0, 35),
2897 lsp::Position::new(0, 38),
2898 ),
2899 "THREE".to_string(),
2900 ),
2901 ],
2902 ),
2903 ]
2904 .into_iter()
2905 .collect(),
2906 ),
2907 ..Default::default()
2908 }))
2909 })
2910 .next()
2911 .await
2912 .unwrap();
2913 confirm_rename.await.unwrap();
2914
2915 let rename_editor = workspace_b.read_with(cx_b, |workspace, cx| {
2916 workspace
2917 .active_item(cx)
2918 .unwrap()
2919 .downcast::<Editor>()
2920 .unwrap()
2921 });
2922 rename_editor.update(cx_b, |editor, cx| {
2923 assert_eq!(
2924 editor.text(cx),
2925 "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
2926 );
2927 editor.undo(&Undo, cx);
2928 assert_eq!(
2929 editor.text(cx),
2930 "const ONE: usize = 1;\nconst TWO: usize = one::ONE + one::ONE;"
2931 );
2932 editor.redo(&Redo, cx);
2933 assert_eq!(
2934 editor.text(cx),
2935 "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
2936 );
2937 });
2938
2939 // Ensure temporary rename edits cannot be undone/redone.
2940 editor_b.update(cx_b, |editor, cx| {
2941 editor.undo(&Undo, cx);
2942 assert_eq!(editor.text(cx), "const ONE: usize = 1;");
2943 editor.undo(&Undo, cx);
2944 assert_eq!(editor.text(cx), "const ONE: usize = 1;");
2945 editor.redo(&Redo, cx);
2946 assert_eq!(editor.text(cx), "const THREE: usize = 1;");
2947 })
2948}
2949
2950#[gpui::test(iterations = 10)]
2951async fn test_language_server_statuses(
2952 deterministic: Arc<Deterministic>,
2953 cx_a: &mut TestAppContext,
2954 cx_b: &mut TestAppContext,
2955) {
2956 deterministic.forbid_parking();
2957
2958 cx_b.update(|cx| editor::init(cx));
2959 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
2960 let client_a = server.create_client(cx_a, "user_a").await;
2961 let client_b = server.create_client(cx_b, "user_b").await;
2962 server
2963 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
2964 .await;
2965
2966 // Set up a fake language server.
2967 let mut language = Language::new(
2968 LanguageConfig {
2969 name: "Rust".into(),
2970 path_suffixes: vec!["rs".to_string()],
2971 ..Default::default()
2972 },
2973 Some(tree_sitter_rust::language()),
2974 );
2975 let mut fake_language_servers = language.set_fake_lsp_adapter(FakeLspAdapter {
2976 name: "the-language-server",
2977 ..Default::default()
2978 });
2979 client_a.language_registry.add(Arc::new(language));
2980
2981 client_a
2982 .fs
2983 .insert_tree(
2984 "/dir",
2985 json!({
2986 "main.rs": "const ONE: usize = 1;",
2987 }),
2988 )
2989 .await;
2990 let (project_a, worktree_id) = client_a.build_local_project("/dir", cx_a).await;
2991
2992 let _buffer_a = project_a
2993 .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))
2994 .await
2995 .unwrap();
2996
2997 let fake_language_server = fake_language_servers.next().await.unwrap();
2998 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
2999 token: lsp::NumberOrString::String("the-token".to_string()),
3000 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
3001 lsp::WorkDoneProgressReport {
3002 message: Some("the-message".to_string()),
3003 ..Default::default()
3004 },
3005 )),
3006 });
3007 deterministic.run_until_parked();
3008 project_a.read_with(cx_a, |project, _| {
3009 let status = project.language_server_statuses().next().unwrap();
3010 assert_eq!(status.name, "the-language-server");
3011 assert_eq!(status.pending_work.len(), 1);
3012 assert_eq!(
3013 status.pending_work["the-token"].message.as_ref().unwrap(),
3014 "the-message"
3015 );
3016 });
3017
3018 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
3019 project_b.read_with(cx_b, |project, _| {
3020 let status = project.language_server_statuses().next().unwrap();
3021 assert_eq!(status.name, "the-language-server");
3022 });
3023
3024 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
3025 token: lsp::NumberOrString::String("the-token".to_string()),
3026 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
3027 lsp::WorkDoneProgressReport {
3028 message: Some("the-message-2".to_string()),
3029 ..Default::default()
3030 },
3031 )),
3032 });
3033 deterministic.run_until_parked();
3034 project_a.read_with(cx_a, |project, _| {
3035 let status = project.language_server_statuses().next().unwrap();
3036 assert_eq!(status.name, "the-language-server");
3037 assert_eq!(status.pending_work.len(), 1);
3038 assert_eq!(
3039 status.pending_work["the-token"].message.as_ref().unwrap(),
3040 "the-message-2"
3041 );
3042 });
3043 project_b.read_with(cx_b, |project, _| {
3044 let status = project.language_server_statuses().next().unwrap();
3045 assert_eq!(status.name, "the-language-server");
3046 assert_eq!(status.pending_work.len(), 1);
3047 assert_eq!(
3048 status.pending_work["the-token"].message.as_ref().unwrap(),
3049 "the-message-2"
3050 );
3051 });
3052}
3053
3054#[gpui::test(iterations = 10)]
3055async fn test_basic_chat(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
3056 cx_a.foreground().forbid_parking();
3057 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
3058 let client_a = server.create_client(cx_a, "user_a").await;
3059 let client_b = server.create_client(cx_b, "user_b").await;
3060
3061 // Create an org that includes these 2 users.
3062 let db = &server.app_state.db;
3063 let org_id = db.create_org("Test Org", "test-org").await.unwrap();
3064 db.add_org_member(org_id, client_a.current_user_id(&cx_a), false)
3065 .await
3066 .unwrap();
3067 db.add_org_member(org_id, client_b.current_user_id(&cx_b), false)
3068 .await
3069 .unwrap();
3070
3071 // Create a channel that includes all the users.
3072 let channel_id = db.create_org_channel(org_id, "test-channel").await.unwrap();
3073 db.add_channel_member(channel_id, client_a.current_user_id(&cx_a), false)
3074 .await
3075 .unwrap();
3076 db.add_channel_member(channel_id, client_b.current_user_id(&cx_b), false)
3077 .await
3078 .unwrap();
3079 db.create_channel_message(
3080 channel_id,
3081 client_b.current_user_id(&cx_b),
3082 "hello A, it's B.",
3083 OffsetDateTime::now_utc(),
3084 1,
3085 )
3086 .await
3087 .unwrap();
3088
3089 let channels_a =
3090 cx_a.add_model(|cx| ChannelList::new(client_a.user_store.clone(), client_a.clone(), cx));
3091 channels_a
3092 .condition(cx_a, |list, _| list.available_channels().is_some())
3093 .await;
3094 channels_a.read_with(cx_a, |list, _| {
3095 assert_eq!(
3096 list.available_channels().unwrap(),
3097 &[ChannelDetails {
3098 id: channel_id.to_proto(),
3099 name: "test-channel".to_string()
3100 }]
3101 )
3102 });
3103 let channel_a = channels_a.update(cx_a, |this, cx| {
3104 this.get_channel(channel_id.to_proto(), cx).unwrap()
3105 });
3106 channel_a.read_with(cx_a, |channel, _| assert!(channel.messages().is_empty()));
3107 channel_a
3108 .condition(&cx_a, |channel, _| {
3109 channel_messages(channel)
3110 == [("user_b".to_string(), "hello A, it's B.".to_string(), false)]
3111 })
3112 .await;
3113
3114 let channels_b =
3115 cx_b.add_model(|cx| ChannelList::new(client_b.user_store.clone(), client_b.clone(), cx));
3116 channels_b
3117 .condition(cx_b, |list, _| list.available_channels().is_some())
3118 .await;
3119 channels_b.read_with(cx_b, |list, _| {
3120 assert_eq!(
3121 list.available_channels().unwrap(),
3122 &[ChannelDetails {
3123 id: channel_id.to_proto(),
3124 name: "test-channel".to_string()
3125 }]
3126 )
3127 });
3128
3129 let channel_b = channels_b.update(cx_b, |this, cx| {
3130 this.get_channel(channel_id.to_proto(), cx).unwrap()
3131 });
3132 channel_b.read_with(cx_b, |channel, _| assert!(channel.messages().is_empty()));
3133 channel_b
3134 .condition(&cx_b, |channel, _| {
3135 channel_messages(channel)
3136 == [("user_b".to_string(), "hello A, it's B.".to_string(), false)]
3137 })
3138 .await;
3139
3140 channel_a
3141 .update(cx_a, |channel, cx| {
3142 channel
3143 .send_message("oh, hi B.".to_string(), cx)
3144 .unwrap()
3145 .detach();
3146 let task = channel.send_message("sup".to_string(), cx).unwrap();
3147 assert_eq!(
3148 channel_messages(channel),
3149 &[
3150 ("user_b".to_string(), "hello A, it's B.".to_string(), false),
3151 ("user_a".to_string(), "oh, hi B.".to_string(), true),
3152 ("user_a".to_string(), "sup".to_string(), true)
3153 ]
3154 );
3155 task
3156 })
3157 .await
3158 .unwrap();
3159
3160 channel_b
3161 .condition(&cx_b, |channel, _| {
3162 channel_messages(channel)
3163 == [
3164 ("user_b".to_string(), "hello A, it's B.".to_string(), false),
3165 ("user_a".to_string(), "oh, hi B.".to_string(), false),
3166 ("user_a".to_string(), "sup".to_string(), false),
3167 ]
3168 })
3169 .await;
3170
3171 assert_eq!(
3172 server
3173 .state()
3174 .await
3175 .channel(channel_id)
3176 .unwrap()
3177 .connection_ids
3178 .len(),
3179 2
3180 );
3181 cx_b.update(|_| drop(channel_b));
3182 server
3183 .condition(|state| state.channel(channel_id).unwrap().connection_ids.len() == 1)
3184 .await;
3185
3186 cx_a.update(|_| drop(channel_a));
3187 server
3188 .condition(|state| state.channel(channel_id).is_none())
3189 .await;
3190}
3191
3192#[gpui::test(iterations = 10)]
3193async fn test_chat_message_validation(cx_a: &mut TestAppContext) {
3194 cx_a.foreground().forbid_parking();
3195 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
3196 let client_a = server.create_client(cx_a, "user_a").await;
3197
3198 let db = &server.app_state.db;
3199 let org_id = db.create_org("Test Org", "test-org").await.unwrap();
3200 let channel_id = db.create_org_channel(org_id, "test-channel").await.unwrap();
3201 db.add_org_member(org_id, client_a.current_user_id(&cx_a), false)
3202 .await
3203 .unwrap();
3204 db.add_channel_member(channel_id, client_a.current_user_id(&cx_a), false)
3205 .await
3206 .unwrap();
3207
3208 let channels_a =
3209 cx_a.add_model(|cx| ChannelList::new(client_a.user_store.clone(), client_a.clone(), cx));
3210 channels_a
3211 .condition(cx_a, |list, _| list.available_channels().is_some())
3212 .await;
3213 let channel_a = channels_a.update(cx_a, |this, cx| {
3214 this.get_channel(channel_id.to_proto(), cx).unwrap()
3215 });
3216
3217 // Messages aren't allowed to be too long.
3218 channel_a
3219 .update(cx_a, |channel, cx| {
3220 let long_body = "this is long.\n".repeat(1024);
3221 channel.send_message(long_body, cx).unwrap()
3222 })
3223 .await
3224 .unwrap_err();
3225
3226 // Messages aren't allowed to be blank.
3227 channel_a.update(cx_a, |channel, cx| {
3228 channel.send_message(String::new(), cx).unwrap_err()
3229 });
3230
3231 // Leading and trailing whitespace are trimmed.
3232 channel_a
3233 .update(cx_a, |channel, cx| {
3234 channel
3235 .send_message("\n surrounded by whitespace \n".to_string(), cx)
3236 .unwrap()
3237 })
3238 .await
3239 .unwrap();
3240 assert_eq!(
3241 db.get_channel_messages(channel_id, 10, None)
3242 .await
3243 .unwrap()
3244 .iter()
3245 .map(|m| &m.body)
3246 .collect::<Vec<_>>(),
3247 &["surrounded by whitespace"]
3248 );
3249}
3250
3251#[gpui::test(iterations = 10)]
3252async fn test_chat_reconnection(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
3253 cx_a.foreground().forbid_parking();
3254 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
3255 let client_a = server.create_client(cx_a, "user_a").await;
3256 let client_b = server.create_client(cx_b, "user_b").await;
3257
3258 let mut status_b = client_b.status();
3259
3260 // Create an org that includes these 2 users.
3261 let db = &server.app_state.db;
3262 let org_id = db.create_org("Test Org", "test-org").await.unwrap();
3263 db.add_org_member(org_id, client_a.current_user_id(&cx_a), false)
3264 .await
3265 .unwrap();
3266 db.add_org_member(org_id, client_b.current_user_id(&cx_b), false)
3267 .await
3268 .unwrap();
3269
3270 // Create a channel that includes all the users.
3271 let channel_id = db.create_org_channel(org_id, "test-channel").await.unwrap();
3272 db.add_channel_member(channel_id, client_a.current_user_id(&cx_a), false)
3273 .await
3274 .unwrap();
3275 db.add_channel_member(channel_id, client_b.current_user_id(&cx_b), false)
3276 .await
3277 .unwrap();
3278 db.create_channel_message(
3279 channel_id,
3280 client_b.current_user_id(&cx_b),
3281 "hello A, it's B.",
3282 OffsetDateTime::now_utc(),
3283 2,
3284 )
3285 .await
3286 .unwrap();
3287
3288 let channels_a =
3289 cx_a.add_model(|cx| ChannelList::new(client_a.user_store.clone(), client_a.clone(), cx));
3290 channels_a
3291 .condition(cx_a, |list, _| list.available_channels().is_some())
3292 .await;
3293
3294 channels_a.read_with(cx_a, |list, _| {
3295 assert_eq!(
3296 list.available_channels().unwrap(),
3297 &[ChannelDetails {
3298 id: channel_id.to_proto(),
3299 name: "test-channel".to_string()
3300 }]
3301 )
3302 });
3303 let channel_a = channels_a.update(cx_a, |this, cx| {
3304 this.get_channel(channel_id.to_proto(), cx).unwrap()
3305 });
3306 channel_a.read_with(cx_a, |channel, _| assert!(channel.messages().is_empty()));
3307 channel_a
3308 .condition(&cx_a, |channel, _| {
3309 channel_messages(channel)
3310 == [("user_b".to_string(), "hello A, it's B.".to_string(), false)]
3311 })
3312 .await;
3313
3314 let channels_b =
3315 cx_b.add_model(|cx| ChannelList::new(client_b.user_store.clone(), client_b.clone(), cx));
3316 channels_b
3317 .condition(cx_b, |list, _| list.available_channels().is_some())
3318 .await;
3319 channels_b.read_with(cx_b, |list, _| {
3320 assert_eq!(
3321 list.available_channels().unwrap(),
3322 &[ChannelDetails {
3323 id: channel_id.to_proto(),
3324 name: "test-channel".to_string()
3325 }]
3326 )
3327 });
3328
3329 let channel_b = channels_b.update(cx_b, |this, cx| {
3330 this.get_channel(channel_id.to_proto(), cx).unwrap()
3331 });
3332 channel_b.read_with(cx_b, |channel, _| assert!(channel.messages().is_empty()));
3333 channel_b
3334 .condition(&cx_b, |channel, _| {
3335 channel_messages(channel)
3336 == [("user_b".to_string(), "hello A, it's B.".to_string(), false)]
3337 })
3338 .await;
3339
3340 // Disconnect client B, ensuring we can still access its cached channel data.
3341 server.forbid_connections();
3342 server.disconnect_client(client_b.current_user_id(&cx_b));
3343 cx_b.foreground().advance_clock(rpc::RECEIVE_TIMEOUT);
3344 while !matches!(
3345 status_b.next().await,
3346 Some(client::Status::ReconnectionError { .. })
3347 ) {}
3348
3349 channels_b.read_with(cx_b, |channels, _| {
3350 assert_eq!(
3351 channels.available_channels().unwrap(),
3352 [ChannelDetails {
3353 id: channel_id.to_proto(),
3354 name: "test-channel".to_string()
3355 }]
3356 )
3357 });
3358 channel_b.read_with(cx_b, |channel, _| {
3359 assert_eq!(
3360 channel_messages(channel),
3361 [("user_b".to_string(), "hello A, it's B.".to_string(), false)]
3362 )
3363 });
3364
3365 // Send a message from client B while it is disconnected.
3366 channel_b
3367 .update(cx_b, |channel, cx| {
3368 let task = channel
3369 .send_message("can you see this?".to_string(), cx)
3370 .unwrap();
3371 assert_eq!(
3372 channel_messages(channel),
3373 &[
3374 ("user_b".to_string(), "hello A, it's B.".to_string(), false),
3375 ("user_b".to_string(), "can you see this?".to_string(), true)
3376 ]
3377 );
3378 task
3379 })
3380 .await
3381 .unwrap_err();
3382
3383 // Send a message from client A while B is disconnected.
3384 channel_a
3385 .update(cx_a, |channel, cx| {
3386 channel
3387 .send_message("oh, hi B.".to_string(), cx)
3388 .unwrap()
3389 .detach();
3390 let task = channel.send_message("sup".to_string(), cx).unwrap();
3391 assert_eq!(
3392 channel_messages(channel),
3393 &[
3394 ("user_b".to_string(), "hello A, it's B.".to_string(), false),
3395 ("user_a".to_string(), "oh, hi B.".to_string(), true),
3396 ("user_a".to_string(), "sup".to_string(), true)
3397 ]
3398 );
3399 task
3400 })
3401 .await
3402 .unwrap();
3403
3404 // Give client B a chance to reconnect.
3405 server.allow_connections();
3406 cx_b.foreground().advance_clock(Duration::from_secs(10));
3407
3408 // Verify that B sees the new messages upon reconnection, as well as the message client B
3409 // sent while offline.
3410 channel_b
3411 .condition(&cx_b, |channel, _| {
3412 channel_messages(channel)
3413 == [
3414 ("user_b".to_string(), "hello A, it's B.".to_string(), false),
3415 ("user_a".to_string(), "oh, hi B.".to_string(), false),
3416 ("user_a".to_string(), "sup".to_string(), false),
3417 ("user_b".to_string(), "can you see this?".to_string(), false),
3418 ]
3419 })
3420 .await;
3421
3422 // Ensure client A and B can communicate normally after reconnection.
3423 channel_a
3424 .update(cx_a, |channel, cx| {
3425 channel.send_message("you online?".to_string(), cx).unwrap()
3426 })
3427 .await
3428 .unwrap();
3429 channel_b
3430 .condition(&cx_b, |channel, _| {
3431 channel_messages(channel)
3432 == [
3433 ("user_b".to_string(), "hello A, it's B.".to_string(), false),
3434 ("user_a".to_string(), "oh, hi B.".to_string(), false),
3435 ("user_a".to_string(), "sup".to_string(), false),
3436 ("user_b".to_string(), "can you see this?".to_string(), false),
3437 ("user_a".to_string(), "you online?".to_string(), false),
3438 ]
3439 })
3440 .await;
3441
3442 channel_b
3443 .update(cx_b, |channel, cx| {
3444 channel.send_message("yep".to_string(), cx).unwrap()
3445 })
3446 .await
3447 .unwrap();
3448 channel_a
3449 .condition(&cx_a, |channel, _| {
3450 channel_messages(channel)
3451 == [
3452 ("user_b".to_string(), "hello A, it's B.".to_string(), false),
3453 ("user_a".to_string(), "oh, hi B.".to_string(), false),
3454 ("user_a".to_string(), "sup".to_string(), false),
3455 ("user_b".to_string(), "can you see this?".to_string(), false),
3456 ("user_a".to_string(), "you online?".to_string(), false),
3457 ("user_b".to_string(), "yep".to_string(), false),
3458 ]
3459 })
3460 .await;
3461}
3462
3463#[gpui::test(iterations = 10)]
3464async fn test_contacts(
3465 deterministic: Arc<Deterministic>,
3466 cx_a: &mut TestAppContext,
3467 cx_b: &mut TestAppContext,
3468 cx_c: &mut TestAppContext,
3469) {
3470 cx_a.foreground().forbid_parking();
3471 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
3472 let client_a = server.create_client(cx_a, "user_a").await;
3473 let client_b = server.create_client(cx_b, "user_b").await;
3474 let client_c = server.create_client(cx_c, "user_c").await;
3475 server
3476 .make_contacts(vec![
3477 (&client_a, cx_a),
3478 (&client_b, cx_b),
3479 (&client_c, cx_c),
3480 ])
3481 .await;
3482
3483 deterministic.run_until_parked();
3484 for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] {
3485 client.user_store.read_with(*cx, |store, _| {
3486 assert_eq!(
3487 contacts(store),
3488 [
3489 ("user_a", true, vec![]),
3490 ("user_b", true, vec![]),
3491 ("user_c", true, vec![])
3492 ],
3493 "{} has the wrong contacts",
3494 client.username
3495 )
3496 });
3497 }
3498
3499 // Share a project as client A.
3500 client_a.fs.create_dir(Path::new("/a")).await.unwrap();
3501 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
3502
3503 deterministic.run_until_parked();
3504 for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] {
3505 client.user_store.read_with(*cx, |store, _| {
3506 assert_eq!(
3507 contacts(store),
3508 [
3509 ("user_a", true, vec![("a", vec![])]),
3510 ("user_b", true, vec![]),
3511 ("user_c", true, vec![])
3512 ],
3513 "{} has the wrong contacts",
3514 client.username
3515 )
3516 });
3517 }
3518
3519 let _project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
3520
3521 deterministic.run_until_parked();
3522 for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] {
3523 client.user_store.read_with(*cx, |store, _| {
3524 assert_eq!(
3525 contacts(store),
3526 [
3527 ("user_a", true, vec![("a", vec!["user_b"])]),
3528 ("user_b", true, vec![]),
3529 ("user_c", true, vec![])
3530 ],
3531 "{} has the wrong contacts",
3532 client.username
3533 )
3534 });
3535 }
3536
3537 // Add a local project as client B
3538 client_a.fs.create_dir("/b".as_ref()).await.unwrap();
3539 let (_project_b, _) = client_b.build_local_project("/b", cx_b).await;
3540
3541 deterministic.run_until_parked();
3542 for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] {
3543 client.user_store.read_with(*cx, |store, _| {
3544 assert_eq!(
3545 contacts(store),
3546 [
3547 ("user_a", true, vec![("a", vec!["user_b"])]),
3548 ("user_b", true, vec![("b", vec![])]),
3549 ("user_c", true, vec![])
3550 ],
3551 "{} has the wrong contacts",
3552 client.username
3553 )
3554 });
3555 }
3556
3557 project_a
3558 .condition(&cx_a, |project, _| {
3559 project.collaborators().contains_key(&client_b.peer_id)
3560 })
3561 .await;
3562
3563 cx_a.update(move |_| drop(project_a));
3564 deterministic.run_until_parked();
3565 for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] {
3566 client.user_store.read_with(*cx, |store, _| {
3567 assert_eq!(
3568 contacts(store),
3569 [
3570 ("user_a", true, vec![]),
3571 ("user_b", true, vec![("b", vec![])]),
3572 ("user_c", true, vec![])
3573 ],
3574 "{} has the wrong contacts",
3575 client.username
3576 )
3577 });
3578 }
3579
3580 server.disconnect_client(client_c.current_user_id(cx_c));
3581 server.forbid_connections();
3582 deterministic.advance_clock(rpc::RECEIVE_TIMEOUT);
3583 for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b)] {
3584 client.user_store.read_with(*cx, |store, _| {
3585 assert_eq!(
3586 contacts(store),
3587 [
3588 ("user_a", true, vec![]),
3589 ("user_b", true, vec![("b", vec![])]),
3590 ("user_c", false, vec![])
3591 ],
3592 "{} has the wrong contacts",
3593 client.username
3594 )
3595 });
3596 }
3597 client_c
3598 .user_store
3599 .read_with(cx_c, |store, _| assert_eq!(contacts(store), []));
3600
3601 server.allow_connections();
3602 client_c
3603 .authenticate_and_connect(false, &cx_c.to_async())
3604 .await
3605 .unwrap();
3606
3607 deterministic.run_until_parked();
3608 for (client, cx) in [(&client_a, &cx_a), (&client_b, &cx_b), (&client_c, &cx_c)] {
3609 client.user_store.read_with(*cx, |store, _| {
3610 assert_eq!(
3611 contacts(store),
3612 [
3613 ("user_a", true, vec![]),
3614 ("user_b", true, vec![("b", vec![])]),
3615 ("user_c", true, vec![])
3616 ],
3617 "{} has the wrong contacts",
3618 client.username
3619 )
3620 });
3621 }
3622
3623 fn contacts(user_store: &UserStore) -> Vec<(&str, bool, Vec<(&str, Vec<&str>)>)> {
3624 user_store
3625 .contacts()
3626 .iter()
3627 .map(|contact| {
3628 let projects = contact
3629 .projects
3630 .iter()
3631 .map(|p| {
3632 (
3633 p.visible_worktree_root_names[0].as_str(),
3634 p.guests.iter().map(|p| p.github_login.as_str()).collect(),
3635 )
3636 })
3637 .collect();
3638 (contact.user.github_login.as_str(), contact.online, projects)
3639 })
3640 .collect()
3641 }
3642}
3643
3644#[gpui::test(iterations = 10)]
3645async fn test_contact_requests(
3646 executor: Arc<Deterministic>,
3647 cx_a: &mut TestAppContext,
3648 cx_a2: &mut TestAppContext,
3649 cx_b: &mut TestAppContext,
3650 cx_b2: &mut TestAppContext,
3651 cx_c: &mut TestAppContext,
3652 cx_c2: &mut TestAppContext,
3653) {
3654 cx_a.foreground().forbid_parking();
3655
3656 // Connect to a server as 3 clients.
3657 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
3658 let client_a = server.create_client(cx_a, "user_a").await;
3659 let client_a2 = server.create_client(cx_a2, "user_a").await;
3660 let client_b = server.create_client(cx_b, "user_b").await;
3661 let client_b2 = server.create_client(cx_b2, "user_b").await;
3662 let client_c = server.create_client(cx_c, "user_c").await;
3663 let client_c2 = server.create_client(cx_c2, "user_c").await;
3664
3665 assert_eq!(client_a.user_id().unwrap(), client_a2.user_id().unwrap());
3666 assert_eq!(client_b.user_id().unwrap(), client_b2.user_id().unwrap());
3667 assert_eq!(client_c.user_id().unwrap(), client_c2.user_id().unwrap());
3668
3669 // User A and User C request that user B become their contact.
3670 client_a
3671 .user_store
3672 .update(cx_a, |store, cx| {
3673 store.request_contact(client_b.user_id().unwrap(), cx)
3674 })
3675 .await
3676 .unwrap();
3677 client_c
3678 .user_store
3679 .update(cx_c, |store, cx| {
3680 store.request_contact(client_b.user_id().unwrap(), cx)
3681 })
3682 .await
3683 .unwrap();
3684 executor.run_until_parked();
3685
3686 // All users see the pending request appear in all their clients.
3687 assert_eq!(
3688 client_a.summarize_contacts(&cx_a).outgoing_requests,
3689 &["user_b"]
3690 );
3691 assert_eq!(
3692 client_a2.summarize_contacts(&cx_a2).outgoing_requests,
3693 &["user_b"]
3694 );
3695 assert_eq!(
3696 client_b.summarize_contacts(&cx_b).incoming_requests,
3697 &["user_a", "user_c"]
3698 );
3699 assert_eq!(
3700 client_b2.summarize_contacts(&cx_b2).incoming_requests,
3701 &["user_a", "user_c"]
3702 );
3703 assert_eq!(
3704 client_c.summarize_contacts(&cx_c).outgoing_requests,
3705 &["user_b"]
3706 );
3707 assert_eq!(
3708 client_c2.summarize_contacts(&cx_c2).outgoing_requests,
3709 &["user_b"]
3710 );
3711
3712 // Contact requests are present upon connecting (tested here via disconnect/reconnect)
3713 disconnect_and_reconnect(&client_a, cx_a).await;
3714 disconnect_and_reconnect(&client_b, cx_b).await;
3715 disconnect_and_reconnect(&client_c, cx_c).await;
3716 executor.run_until_parked();
3717 assert_eq!(
3718 client_a.summarize_contacts(&cx_a).outgoing_requests,
3719 &["user_b"]
3720 );
3721 assert_eq!(
3722 client_b.summarize_contacts(&cx_b).incoming_requests,
3723 &["user_a", "user_c"]
3724 );
3725 assert_eq!(
3726 client_c.summarize_contacts(&cx_c).outgoing_requests,
3727 &["user_b"]
3728 );
3729
3730 // User B accepts the request from user A.
3731 client_b
3732 .user_store
3733 .update(cx_b, |store, cx| {
3734 store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx)
3735 })
3736 .await
3737 .unwrap();
3738
3739 executor.run_until_parked();
3740
3741 // User B sees user A as their contact now in all client, and the incoming request from them is removed.
3742 let contacts_b = client_b.summarize_contacts(&cx_b);
3743 assert_eq!(contacts_b.current, &["user_a", "user_b"]);
3744 assert_eq!(contacts_b.incoming_requests, &["user_c"]);
3745 let contacts_b2 = client_b2.summarize_contacts(&cx_b2);
3746 assert_eq!(contacts_b2.current, &["user_a", "user_b"]);
3747 assert_eq!(contacts_b2.incoming_requests, &["user_c"]);
3748
3749 // User A sees user B as their contact now in all clients, and the outgoing request to them is removed.
3750 let contacts_a = client_a.summarize_contacts(&cx_a);
3751 assert_eq!(contacts_a.current, &["user_a", "user_b"]);
3752 assert!(contacts_a.outgoing_requests.is_empty());
3753 let contacts_a2 = client_a2.summarize_contacts(&cx_a2);
3754 assert_eq!(contacts_a2.current, &["user_a", "user_b"]);
3755 assert!(contacts_a2.outgoing_requests.is_empty());
3756
3757 // Contacts are present upon connecting (tested here via disconnect/reconnect)
3758 disconnect_and_reconnect(&client_a, cx_a).await;
3759 disconnect_and_reconnect(&client_b, cx_b).await;
3760 disconnect_and_reconnect(&client_c, cx_c).await;
3761 executor.run_until_parked();
3762 assert_eq!(
3763 client_a.summarize_contacts(&cx_a).current,
3764 &["user_a", "user_b"]
3765 );
3766 assert_eq!(
3767 client_b.summarize_contacts(&cx_b).current,
3768 &["user_a", "user_b"]
3769 );
3770 assert_eq!(
3771 client_b.summarize_contacts(&cx_b).incoming_requests,
3772 &["user_c"]
3773 );
3774 assert_eq!(client_c.summarize_contacts(&cx_c).current, &["user_c"]);
3775 assert_eq!(
3776 client_c.summarize_contacts(&cx_c).outgoing_requests,
3777 &["user_b"]
3778 );
3779
3780 // User B rejects the request from user C.
3781 client_b
3782 .user_store
3783 .update(cx_b, |store, cx| {
3784 store.respond_to_contact_request(client_c.user_id().unwrap(), false, cx)
3785 })
3786 .await
3787 .unwrap();
3788
3789 executor.run_until_parked();
3790
3791 // User B doesn't see user C as their contact, and the incoming request from them is removed.
3792 let contacts_b = client_b.summarize_contacts(&cx_b);
3793 assert_eq!(contacts_b.current, &["user_a", "user_b"]);
3794 assert!(contacts_b.incoming_requests.is_empty());
3795 let contacts_b2 = client_b2.summarize_contacts(&cx_b2);
3796 assert_eq!(contacts_b2.current, &["user_a", "user_b"]);
3797 assert!(contacts_b2.incoming_requests.is_empty());
3798
3799 // User C doesn't see user B as their contact, and the outgoing request to them is removed.
3800 let contacts_c = client_c.summarize_contacts(&cx_c);
3801 assert_eq!(contacts_c.current, &["user_c"]);
3802 assert!(contacts_c.outgoing_requests.is_empty());
3803 let contacts_c2 = client_c2.summarize_contacts(&cx_c2);
3804 assert_eq!(contacts_c2.current, &["user_c"]);
3805 assert!(contacts_c2.outgoing_requests.is_empty());
3806
3807 // Incoming/outgoing requests are not present upon connecting (tested here via disconnect/reconnect)
3808 disconnect_and_reconnect(&client_a, cx_a).await;
3809 disconnect_and_reconnect(&client_b, cx_b).await;
3810 disconnect_and_reconnect(&client_c, cx_c).await;
3811 executor.run_until_parked();
3812 assert_eq!(
3813 client_a.summarize_contacts(&cx_a).current,
3814 &["user_a", "user_b"]
3815 );
3816 assert_eq!(
3817 client_b.summarize_contacts(&cx_b).current,
3818 &["user_a", "user_b"]
3819 );
3820 assert!(client_b
3821 .summarize_contacts(&cx_b)
3822 .incoming_requests
3823 .is_empty());
3824 assert_eq!(client_c.summarize_contacts(&cx_c).current, &["user_c"]);
3825 assert!(client_c
3826 .summarize_contacts(&cx_c)
3827 .outgoing_requests
3828 .is_empty());
3829
3830 async fn disconnect_and_reconnect(client: &TestClient, cx: &mut TestAppContext) {
3831 client.disconnect(&cx.to_async()).unwrap();
3832 client.clear_contacts(cx).await;
3833 client
3834 .authenticate_and_connect(false, &cx.to_async())
3835 .await
3836 .unwrap();
3837 }
3838}
3839
3840#[gpui::test(iterations = 10)]
3841async fn test_following(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
3842 cx_a.foreground().forbid_parking();
3843 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
3844 let client_a = server.create_client(cx_a, "user_a").await;
3845 let client_b = server.create_client(cx_b, "user_b").await;
3846 server
3847 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
3848 .await;
3849 cx_a.update(editor::init);
3850 cx_b.update(editor::init);
3851
3852 client_a
3853 .fs
3854 .insert_tree(
3855 "/a",
3856 json!({
3857 "1.txt": "one",
3858 "2.txt": "two",
3859 "3.txt": "three",
3860 }),
3861 )
3862 .await;
3863 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
3864
3865 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
3866
3867 // Client A opens some editors.
3868 let workspace_a = client_a.build_workspace(&project_a, cx_a);
3869 let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
3870 let editor_a1 = workspace_a
3871 .update(cx_a, |workspace, cx| {
3872 workspace.open_path((worktree_id, "1.txt"), true, cx)
3873 })
3874 .await
3875 .unwrap()
3876 .downcast::<Editor>()
3877 .unwrap();
3878 let editor_a2 = workspace_a
3879 .update(cx_a, |workspace, cx| {
3880 workspace.open_path((worktree_id, "2.txt"), true, cx)
3881 })
3882 .await
3883 .unwrap()
3884 .downcast::<Editor>()
3885 .unwrap();
3886
3887 // Client B opens an editor.
3888 let workspace_b = client_b.build_workspace(&project_b, cx_b);
3889 let editor_b1 = workspace_b
3890 .update(cx_b, |workspace, cx| {
3891 workspace.open_path((worktree_id, "1.txt"), true, cx)
3892 })
3893 .await
3894 .unwrap()
3895 .downcast::<Editor>()
3896 .unwrap();
3897
3898 let client_a_id = project_b.read_with(cx_b, |project, _| {
3899 project.collaborators().values().next().unwrap().peer_id
3900 });
3901 let client_b_id = project_a.read_with(cx_a, |project, _| {
3902 project.collaborators().values().next().unwrap().peer_id
3903 });
3904
3905 // When client B starts following client A, all visible view states are replicated to client B.
3906 editor_a1.update(cx_a, |editor, cx| {
3907 editor.change_selections(None, cx, |s| s.select_ranges([0..1]))
3908 });
3909 editor_a2.update(cx_a, |editor, cx| {
3910 editor.change_selections(None, cx, |s| s.select_ranges([2..3]))
3911 });
3912 workspace_b
3913 .update(cx_b, |workspace, cx| {
3914 workspace
3915 .toggle_follow(&ToggleFollow(client_a_id), cx)
3916 .unwrap()
3917 })
3918 .await
3919 .unwrap();
3920
3921 let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
3922 workspace
3923 .active_item(cx)
3924 .unwrap()
3925 .downcast::<Editor>()
3926 .unwrap()
3927 });
3928 assert!(cx_b.read(|cx| editor_b2.is_focused(cx)));
3929 assert_eq!(
3930 editor_b2.read_with(cx_b, |editor, cx| editor.project_path(cx)),
3931 Some((worktree_id, "2.txt").into())
3932 );
3933 assert_eq!(
3934 editor_b2.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
3935 vec![2..3]
3936 );
3937 assert_eq!(
3938 editor_b1.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
3939 vec![0..1]
3940 );
3941
3942 // When client A activates a different editor, client B does so as well.
3943 workspace_a.update(cx_a, |workspace, cx| {
3944 workspace.activate_item(&editor_a1, cx)
3945 });
3946 workspace_b
3947 .condition(cx_b, |workspace, cx| {
3948 workspace.active_item(cx).unwrap().id() == editor_b1.id()
3949 })
3950 .await;
3951
3952 // When client A navigates back and forth, client B does so as well.
3953 workspace_a
3954 .update(cx_a, |workspace, cx| {
3955 workspace::Pane::go_back(workspace, None, cx)
3956 })
3957 .await;
3958 workspace_b
3959 .condition(cx_b, |workspace, cx| {
3960 workspace.active_item(cx).unwrap().id() == editor_b2.id()
3961 })
3962 .await;
3963
3964 workspace_a
3965 .update(cx_a, |workspace, cx| {
3966 workspace::Pane::go_forward(workspace, None, cx)
3967 })
3968 .await;
3969 workspace_b
3970 .condition(cx_b, |workspace, cx| {
3971 workspace.active_item(cx).unwrap().id() == editor_b1.id()
3972 })
3973 .await;
3974
3975 // Changes to client A's editor are reflected on client B.
3976 editor_a1.update(cx_a, |editor, cx| {
3977 editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2]));
3978 });
3979 editor_b1
3980 .condition(cx_b, |editor, cx| {
3981 editor.selections.ranges(cx) == vec![1..1, 2..2]
3982 })
3983 .await;
3984
3985 editor_a1.update(cx_a, |editor, cx| editor.set_text("TWO", cx));
3986 editor_b1
3987 .condition(cx_b, |editor, cx| editor.text(cx) == "TWO")
3988 .await;
3989
3990 editor_a1.update(cx_a, |editor, cx| {
3991 editor.change_selections(None, cx, |s| s.select_ranges([3..3]));
3992 editor.set_scroll_position(vec2f(0., 100.), cx);
3993 });
3994 editor_b1
3995 .condition(cx_b, |editor, cx| {
3996 editor.selections.ranges(cx) == vec![3..3]
3997 })
3998 .await;
3999
4000 // After unfollowing, client B stops receiving updates from client A.
4001 workspace_b.update(cx_b, |workspace, cx| {
4002 workspace.unfollow(&workspace.active_pane().clone(), cx)
4003 });
4004 workspace_a.update(cx_a, |workspace, cx| {
4005 workspace.activate_item(&editor_a2, cx)
4006 });
4007 cx_a.foreground().run_until_parked();
4008 assert_eq!(
4009 workspace_b.read_with(cx_b, |workspace, cx| workspace
4010 .active_item(cx)
4011 .unwrap()
4012 .id()),
4013 editor_b1.id()
4014 );
4015
4016 // Client A starts following client B.
4017 workspace_a
4018 .update(cx_a, |workspace, cx| {
4019 workspace
4020 .toggle_follow(&ToggleFollow(client_b_id), cx)
4021 .unwrap()
4022 })
4023 .await
4024 .unwrap();
4025 assert_eq!(
4026 workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
4027 Some(client_b_id)
4028 );
4029 assert_eq!(
4030 workspace_a.read_with(cx_a, |workspace, cx| workspace
4031 .active_item(cx)
4032 .unwrap()
4033 .id()),
4034 editor_a1.id()
4035 );
4036
4037 // Following interrupts when client B disconnects.
4038 client_b.disconnect(&cx_b.to_async()).unwrap();
4039 cx_a.foreground().run_until_parked();
4040 assert_eq!(
4041 workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
4042 None
4043 );
4044}
4045
4046#[gpui::test(iterations = 10)]
4047async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
4048 cx_a.foreground().forbid_parking();
4049 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
4050 let client_a = server.create_client(cx_a, "user_a").await;
4051 let client_b = server.create_client(cx_b, "user_b").await;
4052 server
4053 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
4054 .await;
4055 cx_a.update(editor::init);
4056 cx_b.update(editor::init);
4057
4058 // Client A shares a project.
4059 client_a
4060 .fs
4061 .insert_tree(
4062 "/a",
4063 json!({
4064 "1.txt": "one",
4065 "2.txt": "two",
4066 "3.txt": "three",
4067 "4.txt": "four",
4068 }),
4069 )
4070 .await;
4071 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
4072
4073 // Client B joins the project.
4074 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
4075
4076 // Client A opens some editors.
4077 let workspace_a = client_a.build_workspace(&project_a, cx_a);
4078 let pane_a1 = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
4079 let _editor_a1 = workspace_a
4080 .update(cx_a, |workspace, cx| {
4081 workspace.open_path((worktree_id, "1.txt"), true, cx)
4082 })
4083 .await
4084 .unwrap()
4085 .downcast::<Editor>()
4086 .unwrap();
4087
4088 // Client B opens an editor.
4089 let workspace_b = client_b.build_workspace(&project_b, cx_b);
4090 let pane_b1 = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
4091 let _editor_b1 = workspace_b
4092 .update(cx_b, |workspace, cx| {
4093 workspace.open_path((worktree_id, "2.txt"), true, cx)
4094 })
4095 .await
4096 .unwrap()
4097 .downcast::<Editor>()
4098 .unwrap();
4099
4100 // Clients A and B follow each other in split panes
4101 workspace_a.update(cx_a, |workspace, cx| {
4102 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
4103 assert_ne!(*workspace.active_pane(), pane_a1);
4104 });
4105 workspace_a
4106 .update(cx_a, |workspace, cx| {
4107 let leader_id = *project_a.read(cx).collaborators().keys().next().unwrap();
4108 workspace
4109 .toggle_follow(&workspace::ToggleFollow(leader_id), cx)
4110 .unwrap()
4111 })
4112 .await
4113 .unwrap();
4114 workspace_b.update(cx_b, |workspace, cx| {
4115 workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
4116 assert_ne!(*workspace.active_pane(), pane_b1);
4117 });
4118 workspace_b
4119 .update(cx_b, |workspace, cx| {
4120 let leader_id = *project_b.read(cx).collaborators().keys().next().unwrap();
4121 workspace
4122 .toggle_follow(&workspace::ToggleFollow(leader_id), cx)
4123 .unwrap()
4124 })
4125 .await
4126 .unwrap();
4127
4128 workspace_a
4129 .update(cx_a, |workspace, cx| {
4130 workspace.activate_next_pane(cx);
4131 assert_eq!(*workspace.active_pane(), pane_a1);
4132 workspace.open_path((worktree_id, "3.txt"), true, cx)
4133 })
4134 .await
4135 .unwrap();
4136 workspace_b
4137 .update(cx_b, |workspace, cx| {
4138 workspace.activate_next_pane(cx);
4139 assert_eq!(*workspace.active_pane(), pane_b1);
4140 workspace.open_path((worktree_id, "4.txt"), true, cx)
4141 })
4142 .await
4143 .unwrap();
4144 cx_a.foreground().run_until_parked();
4145
4146 // Ensure leader updates don't change the active pane of followers
4147 workspace_a.read_with(cx_a, |workspace, _| {
4148 assert_eq!(*workspace.active_pane(), pane_a1);
4149 });
4150 workspace_b.read_with(cx_b, |workspace, _| {
4151 assert_eq!(*workspace.active_pane(), pane_b1);
4152 });
4153
4154 // Ensure peers following each other doesn't cause an infinite loop.
4155 assert_eq!(
4156 workspace_a.read_with(cx_a, |workspace, cx| workspace
4157 .active_item(cx)
4158 .unwrap()
4159 .project_path(cx)),
4160 Some((worktree_id, "3.txt").into())
4161 );
4162 workspace_a.update(cx_a, |workspace, cx| {
4163 assert_eq!(
4164 workspace.active_item(cx).unwrap().project_path(cx),
4165 Some((worktree_id, "3.txt").into())
4166 );
4167 workspace.activate_next_pane(cx);
4168 assert_eq!(
4169 workspace.active_item(cx).unwrap().project_path(cx),
4170 Some((worktree_id, "4.txt").into())
4171 );
4172 });
4173 workspace_b.update(cx_b, |workspace, cx| {
4174 assert_eq!(
4175 workspace.active_item(cx).unwrap().project_path(cx),
4176 Some((worktree_id, "4.txt").into())
4177 );
4178 workspace.activate_next_pane(cx);
4179 assert_eq!(
4180 workspace.active_item(cx).unwrap().project_path(cx),
4181 Some((worktree_id, "3.txt").into())
4182 );
4183 });
4184}
4185
4186#[gpui::test(iterations = 10)]
4187async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
4188 cx_a.foreground().forbid_parking();
4189
4190 // 2 clients connect to a server.
4191 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
4192 let client_a = server.create_client(cx_a, "user_a").await;
4193 let client_b = server.create_client(cx_b, "user_b").await;
4194 server
4195 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
4196 .await;
4197 cx_a.update(editor::init);
4198 cx_b.update(editor::init);
4199
4200 // Client A shares a project.
4201 client_a
4202 .fs
4203 .insert_tree(
4204 "/a",
4205 json!({
4206 "1.txt": "one",
4207 "2.txt": "two",
4208 "3.txt": "three",
4209 }),
4210 )
4211 .await;
4212 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
4213 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
4214
4215 // Client A opens some editors.
4216 let workspace_a = client_a.build_workspace(&project_a, cx_a);
4217 let _editor_a1 = workspace_a
4218 .update(cx_a, |workspace, cx| {
4219 workspace.open_path((worktree_id, "1.txt"), true, cx)
4220 })
4221 .await
4222 .unwrap()
4223 .downcast::<Editor>()
4224 .unwrap();
4225
4226 // Client B starts following client A.
4227 let workspace_b = client_b.build_workspace(&project_b, cx_b);
4228 let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
4229 let leader_id = project_b.read_with(cx_b, |project, _| {
4230 project.collaborators().values().next().unwrap().peer_id
4231 });
4232 workspace_b
4233 .update(cx_b, |workspace, cx| {
4234 workspace
4235 .toggle_follow(&ToggleFollow(leader_id), cx)
4236 .unwrap()
4237 })
4238 .await
4239 .unwrap();
4240 assert_eq!(
4241 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
4242 Some(leader_id)
4243 );
4244 let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
4245 workspace
4246 .active_item(cx)
4247 .unwrap()
4248 .downcast::<Editor>()
4249 .unwrap()
4250 });
4251
4252 // When client B moves, it automatically stops following client A.
4253 editor_b2.update(cx_b, |editor, cx| editor.move_right(&editor::MoveRight, cx));
4254 assert_eq!(
4255 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
4256 None
4257 );
4258
4259 workspace_b
4260 .update(cx_b, |workspace, cx| {
4261 workspace
4262 .toggle_follow(&ToggleFollow(leader_id), cx)
4263 .unwrap()
4264 })
4265 .await
4266 .unwrap();
4267 assert_eq!(
4268 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
4269 Some(leader_id)
4270 );
4271
4272 // When client B edits, it automatically stops following client A.
4273 editor_b2.update(cx_b, |editor, cx| editor.insert("X", cx));
4274 assert_eq!(
4275 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
4276 None
4277 );
4278
4279 workspace_b
4280 .update(cx_b, |workspace, cx| {
4281 workspace
4282 .toggle_follow(&ToggleFollow(leader_id), cx)
4283 .unwrap()
4284 })
4285 .await
4286 .unwrap();
4287 assert_eq!(
4288 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
4289 Some(leader_id)
4290 );
4291
4292 // When client B scrolls, it automatically stops following client A.
4293 editor_b2.update(cx_b, |editor, cx| {
4294 editor.set_scroll_position(vec2f(0., 3.), cx)
4295 });
4296 assert_eq!(
4297 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
4298 None
4299 );
4300
4301 workspace_b
4302 .update(cx_b, |workspace, cx| {
4303 workspace
4304 .toggle_follow(&ToggleFollow(leader_id), cx)
4305 .unwrap()
4306 })
4307 .await
4308 .unwrap();
4309 assert_eq!(
4310 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
4311 Some(leader_id)
4312 );
4313
4314 // When client B activates a different pane, it continues following client A in the original pane.
4315 workspace_b.update(cx_b, |workspace, cx| {
4316 workspace.split_pane(pane_b.clone(), SplitDirection::Right, cx)
4317 });
4318 assert_eq!(
4319 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
4320 Some(leader_id)
4321 );
4322
4323 workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
4324 assert_eq!(
4325 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
4326 Some(leader_id)
4327 );
4328
4329 // When client B activates a different item in the original pane, it automatically stops following client A.
4330 workspace_b
4331 .update(cx_b, |workspace, cx| {
4332 workspace.open_path((worktree_id, "2.txt"), true, cx)
4333 })
4334 .await
4335 .unwrap();
4336 assert_eq!(
4337 workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
4338 None
4339 );
4340}
4341
4342#[gpui::test(iterations = 10)]
4343async fn test_peers_simultaneously_following_each_other(
4344 deterministic: Arc<Deterministic>,
4345 cx_a: &mut TestAppContext,
4346 cx_b: &mut TestAppContext,
4347) {
4348 deterministic.forbid_parking();
4349
4350 let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
4351 let client_a = server.create_client(cx_a, "user_a").await;
4352 let client_b = server.create_client(cx_b, "user_b").await;
4353 server
4354 .make_contacts(vec![(&client_a, cx_a), (&client_b, cx_b)])
4355 .await;
4356 cx_a.update(editor::init);
4357 cx_b.update(editor::init);
4358
4359 client_a.fs.insert_tree("/a", json!({})).await;
4360 let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
4361 let workspace_a = client_a.build_workspace(&project_a, cx_a);
4362
4363 let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
4364 let workspace_b = client_b.build_workspace(&project_b, cx_b);
4365
4366 deterministic.run_until_parked();
4367 let client_a_id = project_b.read_with(cx_b, |project, _| {
4368 project.collaborators().values().next().unwrap().peer_id
4369 });
4370 let client_b_id = project_a.read_with(cx_a, |project, _| {
4371 project.collaborators().values().next().unwrap().peer_id
4372 });
4373
4374 let a_follow_b = workspace_a.update(cx_a, |workspace, cx| {
4375 workspace
4376 .toggle_follow(&ToggleFollow(client_b_id), cx)
4377 .unwrap()
4378 });
4379 let b_follow_a = workspace_b.update(cx_b, |workspace, cx| {
4380 workspace
4381 .toggle_follow(&ToggleFollow(client_a_id), cx)
4382 .unwrap()
4383 });
4384
4385 futures::try_join!(a_follow_b, b_follow_a).unwrap();
4386 workspace_a.read_with(cx_a, |workspace, _| {
4387 assert_eq!(
4388 workspace.leader_for_pane(&workspace.active_pane()),
4389 Some(client_b_id)
4390 );
4391 });
4392 workspace_b.read_with(cx_b, |workspace, _| {
4393 assert_eq!(
4394 workspace.leader_for_pane(&workspace.active_pane()),
4395 Some(client_a_id)
4396 );
4397 });
4398}
4399
4400#[gpui::test(iterations = 100)]
4401async fn test_random_collaboration(
4402 cx: &mut TestAppContext,
4403 deterministic: Arc<Deterministic>,
4404 rng: StdRng,
4405) {
4406 deterministic.forbid_parking();
4407 let max_peers = env::var("MAX_PEERS")
4408 .map(|i| i.parse().expect("invalid `MAX_PEERS` variable"))
4409 .unwrap_or(5);
4410 assert!(max_peers <= 5);
4411
4412 let max_operations = env::var("OPERATIONS")
4413 .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
4414 .unwrap_or(10);
4415
4416 let rng = Arc::new(Mutex::new(rng));
4417
4418 let guest_lang_registry = Arc::new(LanguageRegistry::test());
4419 let host_language_registry = Arc::new(LanguageRegistry::test());
4420
4421 let fs = FakeFs::new(cx.background());
4422 fs.insert_tree("/_collab", json!({"init": ""})).await;
4423
4424 let mut server = TestServer::start(cx.foreground(), cx.background()).await;
4425 let db = server.app_state.db.clone();
4426 let host_user_id = db.create_user("host", None, false).await.unwrap();
4427 for username in ["guest-1", "guest-2", "guest-3", "guest-4"] {
4428 let guest_user_id = db.create_user(username, None, false).await.unwrap();
4429 server
4430 .app_state
4431 .db
4432 .send_contact_request(guest_user_id, host_user_id)
4433 .await
4434 .unwrap();
4435 server
4436 .app_state
4437 .db
4438 .respond_to_contact_request(host_user_id, guest_user_id, true)
4439 .await
4440 .unwrap();
4441 }
4442
4443 let mut clients = Vec::new();
4444 let mut user_ids = Vec::new();
4445 let mut op_start_signals = Vec::new();
4446
4447 let mut next_entity_id = 100000;
4448 let mut host_cx = TestAppContext::new(
4449 cx.foreground_platform(),
4450 cx.platform(),
4451 deterministic.build_foreground(next_entity_id),
4452 deterministic.build_background(),
4453 cx.font_cache(),
4454 cx.leak_detector(),
4455 next_entity_id,
4456 );
4457 let host = server.create_client(&mut host_cx, "host").await;
4458 let host_project = host_cx.update(|cx| {
4459 Project::local(
4460 true,
4461 host.client.clone(),
4462 host.user_store.clone(),
4463 host.project_store.clone(),
4464 host_language_registry.clone(),
4465 fs.clone(),
4466 cx,
4467 )
4468 });
4469 let host_project_id = host_project
4470 .update(&mut host_cx, |p, _| p.next_remote_id())
4471 .await;
4472
4473 let (collab_worktree, _) = host_project
4474 .update(&mut host_cx, |project, cx| {
4475 project.find_or_create_local_worktree("/_collab", true, cx)
4476 })
4477 .await
4478 .unwrap();
4479 collab_worktree
4480 .read_with(&host_cx, |tree, _| tree.as_local().unwrap().scan_complete())
4481 .await;
4482
4483 // Set up fake language servers.
4484 let mut language = Language::new(
4485 LanguageConfig {
4486 name: "Rust".into(),
4487 path_suffixes: vec!["rs".to_string()],
4488 ..Default::default()
4489 },
4490 None,
4491 );
4492 let _fake_servers = language.set_fake_lsp_adapter(FakeLspAdapter {
4493 name: "the-fake-language-server",
4494 capabilities: lsp::LanguageServer::full_capabilities(),
4495 initializer: Some(Box::new({
4496 let rng = rng.clone();
4497 let fs = fs.clone();
4498 let project = host_project.downgrade();
4499 move |fake_server: &mut FakeLanguageServer| {
4500 fake_server.handle_request::<lsp::request::Completion, _, _>(|_, _| async move {
4501 Ok(Some(lsp::CompletionResponse::Array(vec![
4502 lsp::CompletionItem {
4503 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
4504 range: lsp::Range::new(
4505 lsp::Position::new(0, 0),
4506 lsp::Position::new(0, 0),
4507 ),
4508 new_text: "the-new-text".to_string(),
4509 })),
4510 ..Default::default()
4511 },
4512 ])))
4513 });
4514
4515 fake_server.handle_request::<lsp::request::CodeActionRequest, _, _>(
4516 |_, _| async move {
4517 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
4518 lsp::CodeAction {
4519 title: "the-code-action".to_string(),
4520 ..Default::default()
4521 },
4522 )]))
4523 },
4524 );
4525
4526 fake_server.handle_request::<lsp::request::PrepareRenameRequest, _, _>(
4527 |params, _| async move {
4528 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
4529 params.position,
4530 params.position,
4531 ))))
4532 },
4533 );
4534
4535 fake_server.handle_request::<lsp::request::GotoDefinition, _, _>({
4536 let fs = fs.clone();
4537 let rng = rng.clone();
4538 move |_, _| {
4539 let fs = fs.clone();
4540 let rng = rng.clone();
4541 async move {
4542 let files = fs.files().await;
4543 let mut rng = rng.lock();
4544 let count = rng.gen_range::<usize, _>(1..3);
4545 let files = (0..count)
4546 .map(|_| files.choose(&mut *rng).unwrap())
4547 .collect::<Vec<_>>();
4548 log::info!("LSP: Returning definitions in files {:?}", &files);
4549 Ok(Some(lsp::GotoDefinitionResponse::Array(
4550 files
4551 .into_iter()
4552 .map(|file| lsp::Location {
4553 uri: lsp::Url::from_file_path(file).unwrap(),
4554 range: Default::default(),
4555 })
4556 .collect(),
4557 )))
4558 }
4559 }
4560 });
4561
4562 fake_server.handle_request::<lsp::request::DocumentHighlightRequest, _, _>({
4563 let rng = rng.clone();
4564 let project = project.clone();
4565 move |params, mut cx| {
4566 let highlights = if let Some(project) = project.upgrade(&cx) {
4567 project.update(&mut cx, |project, cx| {
4568 let path = params
4569 .text_document_position_params
4570 .text_document
4571 .uri
4572 .to_file_path()
4573 .unwrap();
4574 let (worktree, relative_path) =
4575 project.find_local_worktree(&path, cx)?;
4576 let project_path =
4577 ProjectPath::from((worktree.read(cx).id(), relative_path));
4578 let buffer = project.get_open_buffer(&project_path, cx)?.read(cx);
4579
4580 let mut highlights = Vec::new();
4581 let highlight_count = rng.lock().gen_range(1..=5);
4582 let mut prev_end = 0;
4583 for _ in 0..highlight_count {
4584 let range =
4585 buffer.random_byte_range(prev_end, &mut *rng.lock());
4586
4587 highlights.push(lsp::DocumentHighlight {
4588 range: range_to_lsp(range.to_point_utf16(buffer)),
4589 kind: Some(lsp::DocumentHighlightKind::READ),
4590 });
4591 prev_end = range.end;
4592 }
4593 Some(highlights)
4594 })
4595 } else {
4596 None
4597 };
4598 async move { Ok(highlights) }
4599 }
4600 });
4601 }
4602 })),
4603 ..Default::default()
4604 });
4605 host_language_registry.add(Arc::new(language));
4606
4607 let op_start_signal = futures::channel::mpsc::unbounded();
4608 user_ids.push(host.current_user_id(&host_cx));
4609 op_start_signals.push(op_start_signal.0);
4610 clients.push(host_cx.foreground().spawn(host.simulate_host(
4611 host_project,
4612 op_start_signal.1,
4613 rng.clone(),
4614 host_cx,
4615 )));
4616
4617 let disconnect_host_at = if rng.lock().gen_bool(0.2) {
4618 rng.lock().gen_range(0..max_operations)
4619 } else {
4620 max_operations
4621 };
4622 let mut available_guests = vec![
4623 "guest-1".to_string(),
4624 "guest-2".to_string(),
4625 "guest-3".to_string(),
4626 "guest-4".to_string(),
4627 ];
4628 let mut operations = 0;
4629 while operations < max_operations {
4630 if operations == disconnect_host_at {
4631 server.disconnect_client(user_ids[0]);
4632 deterministic.advance_clock(RECEIVE_TIMEOUT);
4633 drop(op_start_signals);
4634
4635 deterministic.start_waiting();
4636 let mut clients = futures::future::join_all(clients).await;
4637 deterministic.finish_waiting();
4638 deterministic.run_until_parked();
4639
4640 let (host, host_project, mut host_cx, host_err) = clients.remove(0);
4641 if let Some(host_err) = host_err {
4642 log::error!("host error - {:?}", host_err);
4643 }
4644 host_project.read_with(&host_cx, |project, _| assert!(!project.is_shared()));
4645 for (guest, guest_project, mut guest_cx, guest_err) in clients {
4646 if let Some(guest_err) = guest_err {
4647 log::error!("{} error - {:?}", guest.username, guest_err);
4648 }
4649
4650 let contacts = server
4651 .app_state
4652 .db
4653 .get_contacts(guest.current_user_id(&guest_cx))
4654 .await
4655 .unwrap();
4656 let contacts = server
4657 .store
4658 .read()
4659 .await
4660 .build_initial_contacts_update(contacts)
4661 .contacts;
4662 assert!(!contacts
4663 .iter()
4664 .flat_map(|contact| &contact.projects)
4665 .any(|project| project.id == host_project_id));
4666 guest_project.read_with(&guest_cx, |project, _| assert!(project.is_read_only()));
4667 guest_cx.update(|_| drop((guest, guest_project)));
4668 }
4669 host_cx.update(|_| drop((host, host_project)));
4670
4671 return;
4672 }
4673
4674 let distribution = rng.lock().gen_range(0..100);
4675 match distribution {
4676 0..=19 if !available_guests.is_empty() => {
4677 let guest_ix = rng.lock().gen_range(0..available_guests.len());
4678 let guest_username = available_guests.remove(guest_ix);
4679 log::info!("Adding new connection for {}", guest_username);
4680 next_entity_id += 100000;
4681 let mut guest_cx = TestAppContext::new(
4682 cx.foreground_platform(),
4683 cx.platform(),
4684 deterministic.build_foreground(next_entity_id),
4685 deterministic.build_background(),
4686 cx.font_cache(),
4687 cx.leak_detector(),
4688 next_entity_id,
4689 );
4690
4691 deterministic.start_waiting();
4692 let guest = server.create_client(&mut guest_cx, &guest_username).await;
4693 let guest_project = Project::remote(
4694 host_project_id,
4695 guest.client.clone(),
4696 guest.user_store.clone(),
4697 guest.project_store.clone(),
4698 guest_lang_registry.clone(),
4699 FakeFs::new(cx.background()),
4700 guest_cx.to_async(),
4701 )
4702 .await
4703 .unwrap();
4704 deterministic.finish_waiting();
4705
4706 let op_start_signal = futures::channel::mpsc::unbounded();
4707 user_ids.push(guest.current_user_id(&guest_cx));
4708 op_start_signals.push(op_start_signal.0);
4709 clients.push(guest_cx.foreground().spawn(guest.simulate_guest(
4710 guest_username.clone(),
4711 guest_project,
4712 op_start_signal.1,
4713 rng.clone(),
4714 guest_cx,
4715 )));
4716
4717 log::info!("Added connection for {}", guest_username);
4718 operations += 1;
4719 }
4720 20..=29 if clients.len() > 1 => {
4721 let guest_ix = rng.lock().gen_range(1..clients.len());
4722 log::info!("Removing guest {}", user_ids[guest_ix]);
4723 let removed_guest_id = user_ids.remove(guest_ix);
4724 let guest = clients.remove(guest_ix);
4725 op_start_signals.remove(guest_ix);
4726 server.forbid_connections();
4727 server.disconnect_client(removed_guest_id);
4728 deterministic.advance_clock(RECEIVE_TIMEOUT);
4729 deterministic.start_waiting();
4730 let (guest, guest_project, mut guest_cx, guest_err) = guest.await;
4731 deterministic.finish_waiting();
4732 server.allow_connections();
4733
4734 if let Some(guest_err) = guest_err {
4735 log::error!("{} error - {:?}", guest.username, guest_err);
4736 }
4737 guest_project.read_with(&guest_cx, |project, _| assert!(project.is_read_only()));
4738 for user_id in &user_ids {
4739 let contacts = server.app_state.db.get_contacts(*user_id).await.unwrap();
4740 let contacts = server
4741 .store
4742 .read()
4743 .await
4744 .build_initial_contacts_update(contacts)
4745 .contacts;
4746 for contact in contacts {
4747 if contact.online {
4748 assert_ne!(
4749 contact.user_id, removed_guest_id.0 as u64,
4750 "removed guest is still a contact of another peer"
4751 );
4752 }
4753 for project in contact.projects {
4754 for project_guest_id in project.guests {
4755 assert_ne!(
4756 project_guest_id, removed_guest_id.0 as u64,
4757 "removed guest appears as still participating on a project"
4758 );
4759 }
4760 }
4761 }
4762 }
4763
4764 log::info!("{} removed", guest.username);
4765 available_guests.push(guest.username.clone());
4766 guest_cx.update(|_| drop((guest, guest_project)));
4767
4768 operations += 1;
4769 }
4770 _ => {
4771 while operations < max_operations && rng.lock().gen_bool(0.7) {
4772 op_start_signals
4773 .choose(&mut *rng.lock())
4774 .unwrap()
4775 .unbounded_send(())
4776 .unwrap();
4777 operations += 1;
4778 }
4779
4780 if rng.lock().gen_bool(0.8) {
4781 deterministic.run_until_parked();
4782 }
4783 }
4784 }
4785 }
4786
4787 drop(op_start_signals);
4788 deterministic.start_waiting();
4789 let mut clients = futures::future::join_all(clients).await;
4790 deterministic.finish_waiting();
4791 deterministic.run_until_parked();
4792
4793 let (host_client, host_project, mut host_cx, host_err) = clients.remove(0);
4794 if let Some(host_err) = host_err {
4795 panic!("host error - {:?}", host_err);
4796 }
4797 let host_worktree_snapshots = host_project.read_with(&host_cx, |project, cx| {
4798 project
4799 .worktrees(cx)
4800 .map(|worktree| {
4801 let snapshot = worktree.read(cx).snapshot();
4802 (snapshot.id(), snapshot)
4803 })
4804 .collect::<BTreeMap<_, _>>()
4805 });
4806
4807 host_project.read_with(&host_cx, |project, cx| project.check_invariants(cx));
4808
4809 for (guest_client, guest_project, mut guest_cx, guest_err) in clients.into_iter() {
4810 if let Some(guest_err) = guest_err {
4811 panic!("{} error - {:?}", guest_client.username, guest_err);
4812 }
4813 let worktree_snapshots = guest_project.read_with(&guest_cx, |project, cx| {
4814 project
4815 .worktrees(cx)
4816 .map(|worktree| {
4817 let worktree = worktree.read(cx);
4818 (worktree.id(), worktree.snapshot())
4819 })
4820 .collect::<BTreeMap<_, _>>()
4821 });
4822
4823 assert_eq!(
4824 worktree_snapshots.keys().collect::<Vec<_>>(),
4825 host_worktree_snapshots.keys().collect::<Vec<_>>(),
4826 "{} has different worktrees than the host",
4827 guest_client.username
4828 );
4829 for (id, host_snapshot) in &host_worktree_snapshots {
4830 let guest_snapshot = &worktree_snapshots[id];
4831 assert_eq!(
4832 guest_snapshot.root_name(),
4833 host_snapshot.root_name(),
4834 "{} has different root name than the host for worktree {}",
4835 guest_client.username,
4836 id
4837 );
4838 assert_eq!(
4839 guest_snapshot.entries(false).collect::<Vec<_>>(),
4840 host_snapshot.entries(false).collect::<Vec<_>>(),
4841 "{} has different snapshot than the host for worktree {}",
4842 guest_client.username,
4843 id
4844 );
4845 assert_eq!(guest_snapshot.scan_id(), host_snapshot.scan_id());
4846 }
4847
4848 guest_project.read_with(&guest_cx, |project, cx| project.check_invariants(cx));
4849
4850 for guest_buffer in &guest_client.buffers {
4851 let buffer_id = guest_buffer.read_with(&guest_cx, |buffer, _| buffer.remote_id());
4852 let host_buffer = host_project.read_with(&host_cx, |project, cx| {
4853 project.buffer_for_id(buffer_id, cx).expect(&format!(
4854 "host does not have buffer for guest:{}, peer:{}, id:{}",
4855 guest_client.username, guest_client.peer_id, buffer_id
4856 ))
4857 });
4858 let path =
4859 host_buffer.read_with(&host_cx, |buffer, cx| buffer.file().unwrap().full_path(cx));
4860
4861 assert_eq!(
4862 guest_buffer.read_with(&guest_cx, |buffer, _| buffer.deferred_ops_len()),
4863 0,
4864 "{}, buffer {}, path {:?} has deferred operations",
4865 guest_client.username,
4866 buffer_id,
4867 path,
4868 );
4869 assert_eq!(
4870 guest_buffer.read_with(&guest_cx, |buffer, _| buffer.text()),
4871 host_buffer.read_with(&host_cx, |buffer, _| buffer.text()),
4872 "{}, buffer {}, path {:?}, differs from the host's buffer",
4873 guest_client.username,
4874 buffer_id,
4875 path
4876 );
4877 }
4878
4879 guest_cx.update(|_| drop((guest_project, guest_client)));
4880 }
4881
4882 host_cx.update(|_| drop((host_client, host_project)));
4883}
4884
4885struct TestServer {
4886 peer: Arc<Peer>,
4887 app_state: Arc<AppState>,
4888 server: Arc<Server>,
4889 foreground: Rc<executor::Foreground>,
4890 notifications: mpsc::UnboundedReceiver<()>,
4891 connection_killers: Arc<Mutex<HashMap<UserId, Arc<AtomicBool>>>>,
4892 forbid_connections: Arc<AtomicBool>,
4893 _test_db: TestDb,
4894}
4895
4896impl TestServer {
4897 async fn start(
4898 foreground: Rc<executor::Foreground>,
4899 background: Arc<executor::Background>,
4900 ) -> Self {
4901 let test_db = TestDb::fake(background.clone());
4902 let app_state = Self::build_app_state(&test_db).await;
4903 let peer = Peer::new();
4904 let notifications = mpsc::unbounded();
4905 let server = Server::new(app_state.clone(), Some(notifications.0));
4906 Self {
4907 peer,
4908 app_state,
4909 server,
4910 foreground,
4911 notifications: notifications.1,
4912 connection_killers: Default::default(),
4913 forbid_connections: Default::default(),
4914 _test_db: test_db,
4915 }
4916 }
4917
4918 async fn create_client(&mut self, cx: &mut TestAppContext, name: &str) -> TestClient {
4919 cx.update(|cx| {
4920 let mut settings = Settings::test(cx);
4921 settings.projects_online_by_default = false;
4922 cx.set_global(settings);
4923 });
4924
4925 let http = FakeHttpClient::with_404_response();
4926 let user_id = if let Ok(Some(user)) = self.app_state.db.get_user_by_github_login(name).await
4927 {
4928 user.id
4929 } else {
4930 self.app_state
4931 .db
4932 .create_user(name, None, false)
4933 .await
4934 .unwrap()
4935 };
4936 let client_name = name.to_string();
4937 let mut client = Client::new(http.clone());
4938 let server = self.server.clone();
4939 let db = self.app_state.db.clone();
4940 let connection_killers = self.connection_killers.clone();
4941 let forbid_connections = self.forbid_connections.clone();
4942 let (connection_id_tx, mut connection_id_rx) = mpsc::channel(16);
4943
4944 Arc::get_mut(&mut client)
4945 .unwrap()
4946 .override_authenticate(move |cx| {
4947 cx.spawn(|_| async move {
4948 let access_token = "the-token".to_string();
4949 Ok(Credentials {
4950 user_id: user_id.0 as u64,
4951 access_token,
4952 })
4953 })
4954 })
4955 .override_establish_connection(move |credentials, cx| {
4956 assert_eq!(credentials.user_id, user_id.0 as u64);
4957 assert_eq!(credentials.access_token, "the-token");
4958
4959 let server = server.clone();
4960 let db = db.clone();
4961 let connection_killers = connection_killers.clone();
4962 let forbid_connections = forbid_connections.clone();
4963 let client_name = client_name.clone();
4964 let connection_id_tx = connection_id_tx.clone();
4965 cx.spawn(move |cx| async move {
4966 if forbid_connections.load(SeqCst) {
4967 Err(EstablishConnectionError::other(anyhow!(
4968 "server is forbidding connections"
4969 )))
4970 } else {
4971 let (client_conn, server_conn, killed) =
4972 Connection::in_memory(cx.background());
4973 connection_killers.lock().insert(user_id, killed);
4974 let user = db.get_user_by_id(user_id).await.unwrap().unwrap();
4975 cx.background()
4976 .spawn(server.handle_connection(
4977 server_conn,
4978 client_name,
4979 user,
4980 Some(connection_id_tx),
4981 cx.background(),
4982 ))
4983 .detach();
4984 Ok(client_conn)
4985 }
4986 })
4987 });
4988
4989 let fs = FakeFs::new(cx.background());
4990 let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx));
4991 let project_store = cx.add_model(|_| ProjectStore::new(project::Db::open_fake()));
4992 let app_state = Arc::new(workspace::AppState {
4993 client: client.clone(),
4994 user_store: user_store.clone(),
4995 project_store: project_store.clone(),
4996 languages: Arc::new(LanguageRegistry::new(Task::ready(()))),
4997 themes: ThemeRegistry::new((), cx.font_cache()),
4998 fs: fs.clone(),
4999 build_window_options: || Default::default(),
5000 initialize_workspace: |_, _, _| unimplemented!(),
5001 });
5002
5003 Channel::init(&client);
5004 Project::init(&client);
5005 cx.update(|cx| workspace::init(app_state.clone(), cx));
5006
5007 client
5008 .authenticate_and_connect(false, &cx.to_async())
5009 .await
5010 .unwrap();
5011 let peer_id = PeerId(connection_id_rx.next().await.unwrap().0);
5012
5013 let client = TestClient {
5014 client,
5015 peer_id,
5016 username: name.to_string(),
5017 user_store,
5018 project_store,
5019 fs,
5020 language_registry: Arc::new(LanguageRegistry::test()),
5021 buffers: Default::default(),
5022 };
5023 client.wait_for_current_user(cx).await;
5024 client
5025 }
5026
5027 fn disconnect_client(&self, user_id: UserId) {
5028 self.connection_killers
5029 .lock()
5030 .remove(&user_id)
5031 .unwrap()
5032 .store(true, SeqCst);
5033 }
5034
5035 fn forbid_connections(&self) {
5036 self.forbid_connections.store(true, SeqCst);
5037 }
5038
5039 fn allow_connections(&self) {
5040 self.forbid_connections.store(false, SeqCst);
5041 }
5042
5043 async fn make_contacts(&self, mut clients: Vec<(&TestClient, &mut TestAppContext)>) {
5044 while let Some((client_a, cx_a)) = clients.pop() {
5045 for (client_b, cx_b) in &mut clients {
5046 client_a
5047 .user_store
5048 .update(cx_a, |store, cx| {
5049 store.request_contact(client_b.user_id().unwrap(), cx)
5050 })
5051 .await
5052 .unwrap();
5053 cx_a.foreground().run_until_parked();
5054 client_b
5055 .user_store
5056 .update(*cx_b, |store, cx| {
5057 store.respond_to_contact_request(client_a.user_id().unwrap(), true, cx)
5058 })
5059 .await
5060 .unwrap();
5061 }
5062 }
5063 }
5064
5065 async fn build_app_state(test_db: &TestDb) -> Arc<AppState> {
5066 Arc::new(AppState {
5067 db: test_db.db().clone(),
5068 api_token: Default::default(),
5069 invite_link_prefix: Default::default(),
5070 })
5071 }
5072
5073 async fn state<'a>(&'a self) -> RwLockReadGuard<'a, Store> {
5074 self.server.store.read().await
5075 }
5076
5077 async fn condition<F>(&mut self, mut predicate: F)
5078 where
5079 F: FnMut(&Store) -> bool,
5080 {
5081 assert!(
5082 self.foreground.parking_forbidden(),
5083 "you must call forbid_parking to use server conditions so we don't block indefinitely"
5084 );
5085 while !(predicate)(&*self.server.store.read().await) {
5086 self.foreground.start_waiting();
5087 self.notifications.next().await;
5088 self.foreground.finish_waiting();
5089 }
5090 }
5091}
5092
5093impl Deref for TestServer {
5094 type Target = Server;
5095
5096 fn deref(&self) -> &Self::Target {
5097 &self.server
5098 }
5099}
5100
5101impl Drop for TestServer {
5102 fn drop(&mut self) {
5103 self.peer.reset();
5104 }
5105}
5106
5107struct TestClient {
5108 client: Arc<Client>,
5109 username: String,
5110 pub peer_id: PeerId,
5111 pub user_store: ModelHandle<UserStore>,
5112 pub project_store: ModelHandle<ProjectStore>,
5113 language_registry: Arc<LanguageRegistry>,
5114 fs: Arc<FakeFs>,
5115 buffers: HashSet<ModelHandle<language::Buffer>>,
5116}
5117
5118impl Deref for TestClient {
5119 type Target = Arc<Client>;
5120
5121 fn deref(&self) -> &Self::Target {
5122 &self.client
5123 }
5124}
5125
5126struct ContactsSummary {
5127 pub current: Vec<String>,
5128 pub outgoing_requests: Vec<String>,
5129 pub incoming_requests: Vec<String>,
5130}
5131
5132impl TestClient {
5133 pub fn current_user_id(&self, cx: &TestAppContext) -> UserId {
5134 UserId::from_proto(
5135 self.user_store
5136 .read_with(cx, |user_store, _| user_store.current_user().unwrap().id),
5137 )
5138 }
5139
5140 async fn wait_for_current_user(&self, cx: &TestAppContext) {
5141 let mut authed_user = self
5142 .user_store
5143 .read_with(cx, |user_store, _| user_store.watch_current_user());
5144 while authed_user.next().await.unwrap().is_none() {}
5145 }
5146
5147 async fn clear_contacts(&self, cx: &mut TestAppContext) {
5148 self.user_store
5149 .update(cx, |store, _| store.clear_contacts())
5150 .await;
5151 }
5152
5153 fn summarize_contacts(&self, cx: &TestAppContext) -> ContactsSummary {
5154 self.user_store.read_with(cx, |store, _| ContactsSummary {
5155 current: store
5156 .contacts()
5157 .iter()
5158 .map(|contact| contact.user.github_login.clone())
5159 .collect(),
5160 outgoing_requests: store
5161 .outgoing_contact_requests()
5162 .iter()
5163 .map(|user| user.github_login.clone())
5164 .collect(),
5165 incoming_requests: store
5166 .incoming_contact_requests()
5167 .iter()
5168 .map(|user| user.github_login.clone())
5169 .collect(),
5170 })
5171 }
5172
5173 async fn build_local_project(
5174 &self,
5175 root_path: impl AsRef<Path>,
5176 cx: &mut TestAppContext,
5177 ) -> (ModelHandle<Project>, WorktreeId) {
5178 let project = cx.update(|cx| {
5179 Project::local(
5180 true,
5181 self.client.clone(),
5182 self.user_store.clone(),
5183 self.project_store.clone(),
5184 self.language_registry.clone(),
5185 self.fs.clone(),
5186 cx,
5187 )
5188 });
5189 let (worktree, _) = project
5190 .update(cx, |p, cx| {
5191 p.find_or_create_local_worktree(root_path, true, cx)
5192 })
5193 .await
5194 .unwrap();
5195 worktree
5196 .read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete())
5197 .await;
5198 project
5199 .update(cx, |project, _| project.next_remote_id())
5200 .await;
5201 (project, worktree.read_with(cx, |tree, _| tree.id()))
5202 }
5203
5204 async fn build_remote_project(
5205 &self,
5206 host_project: &ModelHandle<Project>,
5207 host_cx: &mut TestAppContext,
5208 guest_cx: &mut TestAppContext,
5209 ) -> ModelHandle<Project> {
5210 let host_project_id = host_project
5211 .read_with(host_cx, |project, _| project.next_remote_id())
5212 .await;
5213 let guest_user_id = self.user_id().unwrap();
5214 let languages = host_project.read_with(host_cx, |project, _| project.languages().clone());
5215 let project_b = guest_cx.spawn(|cx| {
5216 Project::remote(
5217 host_project_id,
5218 self.client.clone(),
5219 self.user_store.clone(),
5220 self.project_store.clone(),
5221 languages,
5222 FakeFs::new(cx.background()),
5223 cx,
5224 )
5225 });
5226 host_cx.foreground().run_until_parked();
5227 host_project.update(host_cx, |project, cx| {
5228 project.respond_to_join_request(guest_user_id, true, cx)
5229 });
5230 let project = project_b.await.unwrap();
5231 project
5232 }
5233
5234 fn build_workspace(
5235 &self,
5236 project: &ModelHandle<Project>,
5237 cx: &mut TestAppContext,
5238 ) -> ViewHandle<Workspace> {
5239 let (window_id, _) = cx.add_window(|_| EmptyView);
5240 cx.add_view(window_id, |cx| Workspace::new(project.clone(), cx))
5241 }
5242
5243 async fn simulate_host(
5244 mut self,
5245 project: ModelHandle<Project>,
5246 op_start_signal: futures::channel::mpsc::UnboundedReceiver<()>,
5247 rng: Arc<Mutex<StdRng>>,
5248 mut cx: TestAppContext,
5249 ) -> (
5250 Self,
5251 ModelHandle<Project>,
5252 TestAppContext,
5253 Option<anyhow::Error>,
5254 ) {
5255 async fn simulate_host_internal(
5256 client: &mut TestClient,
5257 project: ModelHandle<Project>,
5258 mut op_start_signal: futures::channel::mpsc::UnboundedReceiver<()>,
5259 rng: Arc<Mutex<StdRng>>,
5260 cx: &mut TestAppContext,
5261 ) -> anyhow::Result<()> {
5262 let fs = project.read_with(cx, |project, _| project.fs().clone());
5263
5264 cx.update(|cx| {
5265 cx.subscribe(&project, move |project, event, cx| {
5266 if let project::Event::ContactRequestedJoin(user) = event {
5267 log::info!("Host: accepting join request from {}", user.github_login);
5268 project.update(cx, |project, cx| {
5269 project.respond_to_join_request(user.id, true, cx)
5270 });
5271 }
5272 })
5273 .detach();
5274 });
5275
5276 while op_start_signal.next().await.is_some() {
5277 let distribution = rng.lock().gen_range::<usize, _>(0..100);
5278 let files = fs.as_fake().files().await;
5279 match distribution {
5280 0..=19 if !files.is_empty() => {
5281 let path = files.choose(&mut *rng.lock()).unwrap();
5282 let mut path = path.as_path();
5283 while let Some(parent_path) = path.parent() {
5284 path = parent_path;
5285 if rng.lock().gen() {
5286 break;
5287 }
5288 }
5289
5290 log::info!("Host: find/create local worktree {:?}", path);
5291 let find_or_create_worktree = project.update(cx, |project, cx| {
5292 project.find_or_create_local_worktree(path, true, cx)
5293 });
5294 if rng.lock().gen() {
5295 cx.background().spawn(find_or_create_worktree).detach();
5296 } else {
5297 find_or_create_worktree.await?;
5298 }
5299 }
5300 20..=79 if !files.is_empty() => {
5301 let buffer = if client.buffers.is_empty() || rng.lock().gen() {
5302 let file = files.choose(&mut *rng.lock()).unwrap();
5303 let (worktree, path) = project
5304 .update(cx, |project, cx| {
5305 project.find_or_create_local_worktree(file.clone(), true, cx)
5306 })
5307 .await?;
5308 let project_path =
5309 worktree.read_with(cx, |worktree, _| (worktree.id(), path));
5310 log::info!(
5311 "Host: opening path {:?}, worktree {}, relative_path {:?}",
5312 file,
5313 project_path.0,
5314 project_path.1
5315 );
5316 let buffer = project
5317 .update(cx, |project, cx| project.open_buffer(project_path, cx))
5318 .await
5319 .unwrap();
5320 client.buffers.insert(buffer.clone());
5321 buffer
5322 } else {
5323 client
5324 .buffers
5325 .iter()
5326 .choose(&mut *rng.lock())
5327 .unwrap()
5328 .clone()
5329 };
5330
5331 if rng.lock().gen_bool(0.1) {
5332 cx.update(|cx| {
5333 log::info!(
5334 "Host: dropping buffer {:?}",
5335 buffer.read(cx).file().unwrap().full_path(cx)
5336 );
5337 client.buffers.remove(&buffer);
5338 drop(buffer);
5339 });
5340 } else {
5341 buffer.update(cx, |buffer, cx| {
5342 log::info!(
5343 "Host: updating buffer {:?} ({})",
5344 buffer.file().unwrap().full_path(cx),
5345 buffer.remote_id()
5346 );
5347
5348 if rng.lock().gen_bool(0.7) {
5349 buffer.randomly_edit(&mut *rng.lock(), 5, cx);
5350 } else {
5351 buffer.randomly_undo_redo(&mut *rng.lock(), cx);
5352 }
5353 });
5354 }
5355 }
5356 _ => loop {
5357 let path_component_count = rng.lock().gen_range::<usize, _>(1..=5);
5358 let mut path = PathBuf::new();
5359 path.push("/");
5360 for _ in 0..path_component_count {
5361 let letter = rng.lock().gen_range(b'a'..=b'z');
5362 path.push(std::str::from_utf8(&[letter]).unwrap());
5363 }
5364 path.set_extension("rs");
5365 let parent_path = path.parent().unwrap();
5366
5367 log::info!("Host: creating file {:?}", path,);
5368
5369 if fs.create_dir(&parent_path).await.is_ok()
5370 && fs.create_file(&path, Default::default()).await.is_ok()
5371 {
5372 break;
5373 } else {
5374 log::info!("Host: cannot create file");
5375 }
5376 },
5377 }
5378
5379 cx.background().simulate_random_delay().await;
5380 }
5381
5382 Ok(())
5383 }
5384
5385 let result =
5386 simulate_host_internal(&mut self, project.clone(), op_start_signal, rng, &mut cx).await;
5387 log::info!("Host done");
5388 (self, project, cx, result.err())
5389 }
5390
5391 pub async fn simulate_guest(
5392 mut self,
5393 guest_username: String,
5394 project: ModelHandle<Project>,
5395 op_start_signal: futures::channel::mpsc::UnboundedReceiver<()>,
5396 rng: Arc<Mutex<StdRng>>,
5397 mut cx: TestAppContext,
5398 ) -> (
5399 Self,
5400 ModelHandle<Project>,
5401 TestAppContext,
5402 Option<anyhow::Error>,
5403 ) {
5404 async fn simulate_guest_internal(
5405 client: &mut TestClient,
5406 guest_username: &str,
5407 project: ModelHandle<Project>,
5408 mut op_start_signal: futures::channel::mpsc::UnboundedReceiver<()>,
5409 rng: Arc<Mutex<StdRng>>,
5410 cx: &mut TestAppContext,
5411 ) -> anyhow::Result<()> {
5412 while op_start_signal.next().await.is_some() {
5413 let buffer = if client.buffers.is_empty() || rng.lock().gen() {
5414 let worktree = if let Some(worktree) = project.read_with(cx, |project, cx| {
5415 project
5416 .worktrees(&cx)
5417 .filter(|worktree| {
5418 let worktree = worktree.read(cx);
5419 worktree.is_visible()
5420 && worktree.entries(false).any(|e| e.is_file())
5421 })
5422 .choose(&mut *rng.lock())
5423 }) {
5424 worktree
5425 } else {
5426 cx.background().simulate_random_delay().await;
5427 continue;
5428 };
5429
5430 let (worktree_root_name, project_path) =
5431 worktree.read_with(cx, |worktree, _| {
5432 let entry = worktree
5433 .entries(false)
5434 .filter(|e| e.is_file())
5435 .choose(&mut *rng.lock())
5436 .unwrap();
5437 (
5438 worktree.root_name().to_string(),
5439 (worktree.id(), entry.path.clone()),
5440 )
5441 });
5442 log::info!(
5443 "{}: opening path {:?} in worktree {} ({})",
5444 guest_username,
5445 project_path.1,
5446 project_path.0,
5447 worktree_root_name,
5448 );
5449 let buffer = project
5450 .update(cx, |project, cx| {
5451 project.open_buffer(project_path.clone(), cx)
5452 })
5453 .await?;
5454 log::info!(
5455 "{}: opened path {:?} in worktree {} ({}) with buffer id {}",
5456 guest_username,
5457 project_path.1,
5458 project_path.0,
5459 worktree_root_name,
5460 buffer.read_with(cx, |buffer, _| buffer.remote_id())
5461 );
5462 client.buffers.insert(buffer.clone());
5463 buffer
5464 } else {
5465 client
5466 .buffers
5467 .iter()
5468 .choose(&mut *rng.lock())
5469 .unwrap()
5470 .clone()
5471 };
5472
5473 let choice = rng.lock().gen_range(0..100);
5474 match choice {
5475 0..=9 => {
5476 cx.update(|cx| {
5477 log::info!(
5478 "{}: dropping buffer {:?}",
5479 guest_username,
5480 buffer.read(cx).file().unwrap().full_path(cx)
5481 );
5482 client.buffers.remove(&buffer);
5483 drop(buffer);
5484 });
5485 }
5486 10..=19 => {
5487 let completions = project.update(cx, |project, cx| {
5488 log::info!(
5489 "{}: requesting completions for buffer {} ({:?})",
5490 guest_username,
5491 buffer.read(cx).remote_id(),
5492 buffer.read(cx).file().unwrap().full_path(cx)
5493 );
5494 let offset = rng.lock().gen_range(0..=buffer.read(cx).len());
5495 project.completions(&buffer, offset, cx)
5496 });
5497 let completions = cx.background().spawn(async move {
5498 completions
5499 .await
5500 .map_err(|err| anyhow!("completions request failed: {:?}", err))
5501 });
5502 if rng.lock().gen_bool(0.3) {
5503 log::info!("{}: detaching completions request", guest_username);
5504 cx.update(|cx| completions.detach_and_log_err(cx));
5505 } else {
5506 completions.await?;
5507 }
5508 }
5509 20..=29 => {
5510 let code_actions = project.update(cx, |project, cx| {
5511 log::info!(
5512 "{}: requesting code actions for buffer {} ({:?})",
5513 guest_username,
5514 buffer.read(cx).remote_id(),
5515 buffer.read(cx).file().unwrap().full_path(cx)
5516 );
5517 let range = buffer.read(cx).random_byte_range(0, &mut *rng.lock());
5518 project.code_actions(&buffer, range, cx)
5519 });
5520 let code_actions = cx.background().spawn(async move {
5521 code_actions
5522 .await
5523 .map_err(|err| anyhow!("code actions request failed: {:?}", err))
5524 });
5525 if rng.lock().gen_bool(0.3) {
5526 log::info!("{}: detaching code actions request", guest_username);
5527 cx.update(|cx| code_actions.detach_and_log_err(cx));
5528 } else {
5529 code_actions.await?;
5530 }
5531 }
5532 30..=39 if buffer.read_with(cx, |buffer, _| buffer.is_dirty()) => {
5533 let (requested_version, save) = buffer.update(cx, |buffer, cx| {
5534 log::info!(
5535 "{}: saving buffer {} ({:?})",
5536 guest_username,
5537 buffer.remote_id(),
5538 buffer.file().unwrap().full_path(cx)
5539 );
5540 (buffer.version(), buffer.save(cx))
5541 });
5542 let save = cx.background().spawn(async move {
5543 let (saved_version, _, _) = save
5544 .await
5545 .map_err(|err| anyhow!("save request failed: {:?}", err))?;
5546 assert!(saved_version.observed_all(&requested_version));
5547 Ok::<_, anyhow::Error>(())
5548 });
5549 if rng.lock().gen_bool(0.3) {
5550 log::info!("{}: detaching save request", guest_username);
5551 cx.update(|cx| save.detach_and_log_err(cx));
5552 } else {
5553 save.await?;
5554 }
5555 }
5556 40..=44 => {
5557 let prepare_rename = project.update(cx, |project, cx| {
5558 log::info!(
5559 "{}: preparing rename for buffer {} ({:?})",
5560 guest_username,
5561 buffer.read(cx).remote_id(),
5562 buffer.read(cx).file().unwrap().full_path(cx)
5563 );
5564 let offset = rng.lock().gen_range(0..=buffer.read(cx).len());
5565 project.prepare_rename(buffer, offset, cx)
5566 });
5567 let prepare_rename = cx.background().spawn(async move {
5568 prepare_rename
5569 .await
5570 .map_err(|err| anyhow!("prepare rename request failed: {:?}", err))
5571 });
5572 if rng.lock().gen_bool(0.3) {
5573 log::info!("{}: detaching prepare rename request", guest_username);
5574 cx.update(|cx| prepare_rename.detach_and_log_err(cx));
5575 } else {
5576 prepare_rename.await?;
5577 }
5578 }
5579 45..=49 => {
5580 let definitions = project.update(cx, |project, cx| {
5581 log::info!(
5582 "{}: requesting definitions for buffer {} ({:?})",
5583 guest_username,
5584 buffer.read(cx).remote_id(),
5585 buffer.read(cx).file().unwrap().full_path(cx)
5586 );
5587 let offset = rng.lock().gen_range(0..=buffer.read(cx).len());
5588 project.definition(&buffer, offset, cx)
5589 });
5590 let definitions = cx.background().spawn(async move {
5591 definitions
5592 .await
5593 .map_err(|err| anyhow!("definitions request failed: {:?}", err))
5594 });
5595 if rng.lock().gen_bool(0.3) {
5596 log::info!("{}: detaching definitions request", guest_username);
5597 cx.update(|cx| definitions.detach_and_log_err(cx));
5598 } else {
5599 client.buffers.extend(
5600 definitions.await?.into_iter().map(|loc| loc.target.buffer),
5601 );
5602 }
5603 }
5604 50..=54 => {
5605 let highlights = project.update(cx, |project, cx| {
5606 log::info!(
5607 "{}: requesting highlights for buffer {} ({:?})",
5608 guest_username,
5609 buffer.read(cx).remote_id(),
5610 buffer.read(cx).file().unwrap().full_path(cx)
5611 );
5612 let offset = rng.lock().gen_range(0..=buffer.read(cx).len());
5613 project.document_highlights(&buffer, offset, cx)
5614 });
5615 let highlights = cx.background().spawn(async move {
5616 highlights
5617 .await
5618 .map_err(|err| anyhow!("highlights request failed: {:?}", err))
5619 });
5620 if rng.lock().gen_bool(0.3) {
5621 log::info!("{}: detaching highlights request", guest_username);
5622 cx.update(|cx| highlights.detach_and_log_err(cx));
5623 } else {
5624 highlights.await?;
5625 }
5626 }
5627 55..=59 => {
5628 let search = project.update(cx, |project, cx| {
5629 let query = rng.lock().gen_range('a'..='z');
5630 log::info!("{}: project-wide search {:?}", guest_username, query);
5631 project.search(SearchQuery::text(query, false, false), cx)
5632 });
5633 let search = cx.background().spawn(async move {
5634 search
5635 .await
5636 .map_err(|err| anyhow!("search request failed: {:?}", err))
5637 });
5638 if rng.lock().gen_bool(0.3) {
5639 log::info!("{}: detaching search request", guest_username);
5640 cx.update(|cx| search.detach_and_log_err(cx));
5641 } else {
5642 client.buffers.extend(search.await?.into_keys());
5643 }
5644 }
5645 60..=69 => {
5646 let worktree = project
5647 .read_with(cx, |project, cx| {
5648 project
5649 .worktrees(&cx)
5650 .filter(|worktree| {
5651 let worktree = worktree.read(cx);
5652 worktree.is_visible()
5653 && worktree.entries(false).any(|e| e.is_file())
5654 && worktree.root_entry().map_or(false, |e| e.is_dir())
5655 })
5656 .choose(&mut *rng.lock())
5657 })
5658 .unwrap();
5659 let (worktree_id, worktree_root_name) = worktree
5660 .read_with(cx, |worktree, _| {
5661 (worktree.id(), worktree.root_name().to_string())
5662 });
5663
5664 let mut new_name = String::new();
5665 for _ in 0..10 {
5666 let letter = rng.lock().gen_range('a'..='z');
5667 new_name.push(letter);
5668 }
5669 let mut new_path = PathBuf::new();
5670 new_path.push(new_name);
5671 new_path.set_extension("rs");
5672 log::info!(
5673 "{}: creating {:?} in worktree {} ({})",
5674 guest_username,
5675 new_path,
5676 worktree_id,
5677 worktree_root_name,
5678 );
5679 project
5680 .update(cx, |project, cx| {
5681 project.create_entry((worktree_id, new_path), false, cx)
5682 })
5683 .unwrap()
5684 .await?;
5685 }
5686 _ => {
5687 buffer.update(cx, |buffer, cx| {
5688 log::info!(
5689 "{}: updating buffer {} ({:?})",
5690 guest_username,
5691 buffer.remote_id(),
5692 buffer.file().unwrap().full_path(cx)
5693 );
5694 if rng.lock().gen_bool(0.7) {
5695 buffer.randomly_edit(&mut *rng.lock(), 5, cx);
5696 } else {
5697 buffer.randomly_undo_redo(&mut *rng.lock(), cx);
5698 }
5699 });
5700 }
5701 }
5702 cx.background().simulate_random_delay().await;
5703 }
5704 Ok(())
5705 }
5706
5707 let result = simulate_guest_internal(
5708 &mut self,
5709 &guest_username,
5710 project.clone(),
5711 op_start_signal,
5712 rng,
5713 &mut cx,
5714 )
5715 .await;
5716 log::info!("{}: done", guest_username);
5717
5718 (self, project, cx, result.err())
5719 }
5720}
5721
5722impl Drop for TestClient {
5723 fn drop(&mut self) {
5724 self.client.tear_down();
5725 }
5726}
5727
5728impl Executor for Arc<gpui::executor::Background> {
5729 type Sleep = gpui::executor::Timer;
5730
5731 fn spawn_detached<F: 'static + Send + Future<Output = ()>>(&self, future: F) {
5732 self.spawn(future).detach();
5733 }
5734
5735 fn sleep(&self, duration: Duration) -> Self::Sleep {
5736 self.as_ref().timer(duration)
5737 }
5738}
5739
5740fn channel_messages(channel: &Channel) -> Vec<(String, String, bool)> {
5741 channel
5742 .messages()
5743 .cursor::<()>()
5744 .map(|m| {
5745 (
5746 m.sender.github_login.clone(),
5747 m.body.clone(),
5748 m.is_pending(),
5749 )
5750 })
5751 .collect()
5752}
5753
5754struct EmptyView;
5755
5756impl gpui::Entity for EmptyView {
5757 type Event = ();
5758}
5759
5760impl gpui::View for EmptyView {
5761 fn ui_name() -> &'static str {
5762 "empty view"
5763 }
5764
5765 fn render(&mut self, _: &mut gpui::RenderContext<Self>) -> gpui::ElementBox {
5766 gpui::Element::boxed(gpui::elements::Empty::new())
5767 }
5768}