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