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