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