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