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