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