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