1use crate::TestServer;
2use call::ActiveCall;
3use collab::rpc::RECONNECT_TIMEOUT;
4use collections::{HashMap, HashSet};
5use editor::{
6 DocumentColorsRenderMode, Editor, LSP_REQUEST_DEBOUNCE_TIMEOUT, MultiBufferOffset, RowInfo,
7 SelectionEffects,
8 actions::{
9 ConfirmCodeAction, ConfirmCompletion, ConfirmRename, ContextMenuFirst, CopyFileLocation,
10 CopyFileName, CopyFileNameWithoutExtension, ExpandMacroRecursively, MoveToEnd, Redo,
11 Rename, SelectAll, ToggleCodeActions, Undo,
12 },
13 test::{
14 editor_test_context::{AssertionContextManager, EditorTestContext},
15 expand_macro_recursively,
16 },
17};
18use fs::Fs;
19use futures::{SinkExt, StreamExt, channel::mpsc, lock::Mutex};
20use git::repository::repo_path;
21use gpui::{
22 App, Rgba, SharedString, TestAppContext, UpdateGlobal, VisualContext, VisualTestContext,
23};
24use indoc::indoc;
25use language::{FakeLspAdapter, language_settings::language_settings, rust_lang};
26use lsp::DEFAULT_LSP_REQUEST_TIMEOUT;
27use multi_buffer::{AnchorRangeExt as _, MultiBufferRow};
28use pretty_assertions::assert_eq;
29use project::{
30 ProgressToken, ProjectPath, SERVER_PROGRESS_THROTTLE_TIMEOUT,
31 lsp_store::lsp_ext_command::{ExpandedMacro, LspExtExpandMacro},
32 trusted_worktrees::{PathTrust, TrustedWorktrees},
33};
34use recent_projects::disconnected_overlay::DisconnectedOverlay;
35use rpc::RECEIVE_TIMEOUT;
36use serde_json::json;
37use settings::{
38 DocumentFoldingRanges, InlayHintSettingsContent, InlineBlameSettings, SemanticTokens,
39 SettingsStore,
40};
41use std::{
42 collections::BTreeSet,
43 num::NonZeroU32,
44 ops::{Deref as _, Range},
45 path::{Path, PathBuf},
46 sync::{
47 Arc,
48 atomic::{self, AtomicBool, AtomicUsize},
49 },
50 time::Duration,
51};
52use text::Point;
53use util::{path, rel_path::rel_path, uri};
54use workspace::{CloseIntent, Workspace};
55
56#[gpui::test(iterations = 10)]
57async fn test_host_disconnect(
58 cx_a: &mut TestAppContext,
59 cx_b: &mut TestAppContext,
60 cx_c: &mut TestAppContext,
61) {
62 let mut server = TestServer::start(cx_a.executor()).await;
63 let client_a = server.create_client(cx_a, "user_a").await;
64 let client_b = server.create_client(cx_b, "user_b").await;
65 let client_c = server.create_client(cx_c, "user_c").await;
66 server
67 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
68 .await;
69
70 cx_b.update(editor::init);
71 cx_b.update(recent_projects::init);
72
73 client_a
74 .fs()
75 .insert_tree(
76 path!("/a"),
77 json!({
78 "a.txt": "a-contents",
79 "b.txt": "b-contents",
80 }),
81 )
82 .await;
83
84 let active_call_a = cx_a.read(ActiveCall::global);
85 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
86
87 let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap());
88 let project_id = active_call_a
89 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
90 .await
91 .unwrap();
92
93 let project_b = client_b.join_remote_project(project_id, cx_b).await;
94 cx_a.background_executor.run_until_parked();
95
96 assert!(worktree_a.read_with(cx_a, |tree, _| tree.has_update_observer()));
97
98 let workspace_b = cx_b.add_window(|window, cx| {
99 Workspace::new(
100 None,
101 project_b.clone(),
102 client_b.app_state.clone(),
103 window,
104 cx,
105 )
106 });
107 let cx_b = &mut VisualTestContext::from_window(*workspace_b, cx_b);
108 let workspace_b_view = workspace_b.root(cx_b).unwrap();
109
110 let editor_b = workspace_b
111 .update(cx_b, |workspace, window, cx| {
112 workspace.open_path((worktree_id, rel_path("b.txt")), None, true, window, cx)
113 })
114 .unwrap()
115 .await
116 .unwrap()
117 .downcast::<Editor>()
118 .unwrap();
119
120 //TODO: focus
121 assert!(cx_b.update_window_entity(&editor_b, |editor, window, _| editor.is_focused(window)));
122 editor_b.update_in(cx_b, |editor, window, cx| editor.insert("X", window, cx));
123
124 cx_b.update(|_, cx| {
125 assert!(workspace_b_view.read(cx).is_edited());
126 });
127
128 // Drop client A's connection. Collaborators should disappear and the project should not be shown as shared.
129 server.forbid_connections();
130 server.disconnect_client(client_a.peer_id().unwrap());
131 cx_a.background_executor
132 .advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
133
134 project_a.read_with(cx_a, |project, _| project.collaborators().is_empty());
135
136 project_a.read_with(cx_a, |project, _| assert!(!project.is_shared()));
137
138 project_b.read_with(cx_b, |project, cx| project.is_read_only(cx));
139
140 assert!(worktree_a.read_with(cx_a, |tree, _| !tree.has_update_observer()));
141
142 // Ensure client B's edited state is reset and that the whole window is blurred.
143 workspace_b
144 .update(cx_b, |workspace, _, cx| {
145 assert!(workspace.active_modal::<DisconnectedOverlay>(cx).is_some());
146 assert!(!workspace.is_edited());
147 })
148 .unwrap();
149
150 // Ensure client B is not prompted to save edits when closing window after disconnecting.
151 let can_close = workspace_b
152 .update(cx_b, |workspace, window, cx| {
153 workspace.prepare_to_close(CloseIntent::Quit, window, cx)
154 })
155 .unwrap()
156 .await
157 .unwrap();
158 assert!(can_close);
159
160 // Allow client A to reconnect to the server.
161 server.allow_connections();
162 cx_a.background_executor.advance_clock(RECONNECT_TIMEOUT);
163
164 // Client B calls client A again after they reconnected.
165 let active_call_b = cx_b.read(ActiveCall::global);
166 active_call_b
167 .update(cx_b, |call, cx| {
168 call.invite(client_a.user_id().unwrap(), None, cx)
169 })
170 .await
171 .unwrap();
172 cx_a.background_executor.run_until_parked();
173 active_call_a
174 .update(cx_a, |call, cx| call.accept_incoming(cx))
175 .await
176 .unwrap();
177
178 active_call_a
179 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
180 .await
181 .unwrap();
182
183 // Drop client A's connection again. We should still unshare it successfully.
184 server.forbid_connections();
185 server.disconnect_client(client_a.peer_id().unwrap());
186 cx_a.background_executor
187 .advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
188
189 project_a.read_with(cx_a, |project, _| assert!(!project.is_shared()));
190}
191
192#[gpui::test]
193async fn test_newline_above_or_below_does_not_move_guest_cursor(
194 cx_a: &mut TestAppContext,
195 cx_b: &mut TestAppContext,
196) {
197 let mut server = TestServer::start(cx_a.executor()).await;
198 let client_a = server.create_client(cx_a, "user_a").await;
199 let client_b = server.create_client(cx_b, "user_b").await;
200 let executor = cx_a.executor();
201 server
202 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
203 .await;
204 let active_call_a = cx_a.read(ActiveCall::global);
205
206 client_a
207 .fs()
208 .insert_tree(path!("/dir"), json!({ "a.txt": "Some text\n" }))
209 .await;
210 let (project_a, worktree_id) = client_a.build_local_project(path!("/dir"), cx_a).await;
211 let project_id = active_call_a
212 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
213 .await
214 .unwrap();
215
216 let project_b = client_b.join_remote_project(project_id, cx_b).await;
217
218 // Open a buffer as client A
219 let buffer_a = project_a
220 .update(cx_a, |p, cx| {
221 p.open_buffer((worktree_id, rel_path("a.txt")), cx)
222 })
223 .await
224 .unwrap();
225 let cx_a = cx_a.add_empty_window();
226 let editor_a = cx_a
227 .new_window_entity(|window, cx| Editor::for_buffer(buffer_a, Some(project_a), window, cx));
228
229 let mut editor_cx_a = EditorTestContext {
230 cx: cx_a.clone(),
231 window: cx_a.window_handle(),
232 editor: editor_a,
233 assertion_cx: AssertionContextManager::new(),
234 };
235
236 let cx_b = cx_b.add_empty_window();
237 // Open a buffer as client B
238 let buffer_b = project_b
239 .update(cx_b, |p, cx| {
240 p.open_buffer((worktree_id, rel_path("a.txt")), cx)
241 })
242 .await
243 .unwrap();
244 let editor_b = cx_b
245 .new_window_entity(|window, cx| Editor::for_buffer(buffer_b, Some(project_b), window, cx));
246
247 let mut editor_cx_b = EditorTestContext {
248 cx: cx_b.clone(),
249 window: cx_b.window_handle(),
250 editor: editor_b,
251 assertion_cx: AssertionContextManager::new(),
252 };
253
254 // Test newline above
255 editor_cx_a.set_selections_state(indoc! {"
256 Some textˇ
257 "});
258 editor_cx_b.set_selections_state(indoc! {"
259 Some textˇ
260 "});
261 editor_cx_a.update_editor(|editor, window, cx| {
262 editor.newline_above(&editor::actions::NewlineAbove, window, cx)
263 });
264 executor.run_until_parked();
265 editor_cx_a.assert_editor_state(indoc! {"
266 ˇ
267 Some text
268 "});
269 editor_cx_b.assert_editor_state(indoc! {"
270
271 Some textˇ
272 "});
273
274 // Test newline below
275 editor_cx_a.set_selections_state(indoc! {"
276
277 Some textˇ
278 "});
279 editor_cx_b.set_selections_state(indoc! {"
280
281 Some textˇ
282 "});
283 editor_cx_a.update_editor(|editor, window, cx| {
284 editor.newline_below(&editor::actions::NewlineBelow, window, cx)
285 });
286 executor.run_until_parked();
287 editor_cx_a.assert_editor_state(indoc! {"
288
289 Some text
290 ˇ
291 "});
292 editor_cx_b.assert_editor_state(indoc! {"
293
294 Some textˇ
295
296 "});
297}
298
299#[gpui::test]
300async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
301 let mut server = TestServer::start(cx_a.executor()).await;
302 let client_a = server.create_client(cx_a, "user_a").await;
303 let client_b = server.create_client(cx_b, "user_b").await;
304 server
305 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
306 .await;
307 let active_call_a = cx_a.read(ActiveCall::global);
308
309 let capabilities = lsp::ServerCapabilities {
310 completion_provider: Some(lsp::CompletionOptions {
311 trigger_characters: Some(vec![".".to_string()]),
312 resolve_provider: Some(true),
313 ..lsp::CompletionOptions::default()
314 }),
315 ..lsp::ServerCapabilities::default()
316 };
317 client_a.language_registry().add(rust_lang());
318 let mut fake_language_servers = [
319 client_a.language_registry().register_fake_lsp(
320 "Rust",
321 FakeLspAdapter {
322 capabilities: capabilities.clone(),
323 initializer: Some(Box::new(|fake_server| {
324 fake_server.set_request_handler::<lsp::request::Completion, _, _>(
325 |params, _| async move {
326 assert_eq!(
327 params.text_document_position.text_document.uri,
328 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
329 );
330 assert_eq!(
331 params.text_document_position.position,
332 lsp::Position::new(0, 14),
333 );
334
335 Ok(Some(lsp::CompletionResponse::Array(vec![
336 lsp::CompletionItem {
337 label: "first_method(…)".into(),
338 detail: Some("fn(&mut self, B) -> C".into()),
339 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
340 new_text: "first_method($1)".to_string(),
341 range: lsp::Range::new(
342 lsp::Position::new(0, 14),
343 lsp::Position::new(0, 14),
344 ),
345 })),
346 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
347 ..Default::default()
348 },
349 lsp::CompletionItem {
350 label: "second_method(…)".into(),
351 detail: Some("fn(&mut self, C) -> D<E>".into()),
352 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
353 new_text: "second_method()".to_string(),
354 range: lsp::Range::new(
355 lsp::Position::new(0, 14),
356 lsp::Position::new(0, 14),
357 ),
358 })),
359 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
360 ..Default::default()
361 },
362 ])))
363 },
364 );
365 })),
366 ..FakeLspAdapter::default()
367 },
368 ),
369 client_a.language_registry().register_fake_lsp(
370 "Rust",
371 FakeLspAdapter {
372 name: "fake-analyzer",
373 capabilities: capabilities.clone(),
374 initializer: Some(Box::new(|fake_server| {
375 fake_server.set_request_handler::<lsp::request::Completion, _, _>(
376 |_, _| async move { Ok(None) },
377 );
378 })),
379 ..FakeLspAdapter::default()
380 },
381 ),
382 ];
383 client_b.language_registry().add(rust_lang());
384 client_b.language_registry().register_fake_lsp_adapter(
385 "Rust",
386 FakeLspAdapter {
387 capabilities: capabilities.clone(),
388 ..FakeLspAdapter::default()
389 },
390 );
391 client_b.language_registry().register_fake_lsp_adapter(
392 "Rust",
393 FakeLspAdapter {
394 name: "fake-analyzer",
395 capabilities,
396 ..FakeLspAdapter::default()
397 },
398 );
399
400 client_a
401 .fs()
402 .insert_tree(
403 path!("/a"),
404 json!({
405 "main.rs": "fn main() { a }",
406 "other.rs": "",
407 }),
408 )
409 .await;
410 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
411 let project_id = active_call_a
412 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
413 .await
414 .unwrap();
415 let project_b = client_b.join_remote_project(project_id, cx_b).await;
416
417 // Open a file in an editor as the guest.
418 let buffer_b = project_b
419 .update(cx_b, |p, cx| {
420 p.open_buffer((worktree_id, rel_path("main.rs")), cx)
421 })
422 .await
423 .unwrap();
424 let cx_b = cx_b.add_empty_window();
425 let editor_b = cx_b.new_window_entity(|window, cx| {
426 Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), window, cx)
427 });
428
429 let fake_language_server = fake_language_servers[0].next().await.unwrap();
430 let second_fake_language_server = fake_language_servers[1].next().await.unwrap();
431 cx_a.background_executor.run_until_parked();
432 cx_b.background_executor.run_until_parked();
433
434 buffer_b.read_with(cx_b, |buffer, _| {
435 assert!(!buffer.completion_triggers().is_empty())
436 });
437
438 // Set up the completion request handlers BEFORE typing the trigger character.
439 // This is critical - the handlers must be in place when the request arrives,
440 // otherwise the requests will time out waiting for a response.
441 let mut first_completion_request = fake_language_server
442 .set_request_handler::<lsp::request::Completion, _, _>(|params, _| async move {
443 assert_eq!(
444 params.text_document_position.text_document.uri,
445 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
446 );
447 assert_eq!(
448 params.text_document_position.position,
449 lsp::Position::new(0, 14),
450 );
451
452 Ok(Some(lsp::CompletionResponse::Array(vec![
453 lsp::CompletionItem {
454 label: "first_method(…)".into(),
455 detail: Some("fn(&mut self, B) -> C".into()),
456 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
457 new_text: "first_method($1)".to_string(),
458 range: lsp::Range::new(
459 lsp::Position::new(0, 14),
460 lsp::Position::new(0, 14),
461 ),
462 })),
463 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
464 ..Default::default()
465 },
466 lsp::CompletionItem {
467 label: "second_method(…)".into(),
468 detail: Some("fn(&mut self, C) -> D<E>".into()),
469 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
470 new_text: "second_method()".to_string(),
471 range: lsp::Range::new(
472 lsp::Position::new(0, 14),
473 lsp::Position::new(0, 14),
474 ),
475 })),
476 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
477 ..Default::default()
478 },
479 ])))
480 });
481 let mut second_completion_request = second_fake_language_server
482 .set_request_handler::<lsp::request::Completion, _, _>(|_, _| async move { Ok(None) });
483 // Type a completion trigger character as the guest.
484 editor_b.update_in(cx_b, |editor, window, cx| {
485 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
486 s.select_ranges([MultiBufferOffset(13)..MultiBufferOffset(13)])
487 });
488 editor.handle_input(".", window, cx);
489 });
490 cx_b.focus(&editor_b);
491
492 // Allow the completion request to propagate from guest to host to LSP.
493 cx_b.background_executor.run_until_parked();
494 cx_a.background_executor.run_until_parked();
495
496 // Wait for the completion requests to be received by the fake language servers.
497 first_completion_request.next().await.unwrap();
498 second_completion_request.next().await.unwrap();
499
500 // Open the buffer on the host.
501 let buffer_a = project_a
502 .update(cx_a, |p, cx| {
503 p.open_buffer((worktree_id, rel_path("main.rs")), cx)
504 })
505 .await
506 .unwrap();
507 cx_a.executor().run_until_parked();
508
509 buffer_a.read_with(cx_a, |buffer, _| {
510 assert_eq!(buffer.text(), "fn main() { a. }")
511 });
512
513 // Confirm a completion on the guest.
514 editor_b.update_in(cx_b, |editor, window, cx| {
515 assert!(editor.context_menu_visible());
516 editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, window, cx);
517 assert_eq!(editor.text(cx), "fn main() { a.first_method() }");
518 });
519
520 // Return a resolved completion from the host's language server.
521 // The resolved completion has an additional text edit.
522 fake_language_server.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(
523 |params, _| async move {
524 assert_eq!(params.label, "first_method(…)");
525 Ok(lsp::CompletionItem {
526 label: "first_method(…)".into(),
527 detail: Some("fn(&mut self, B) -> C".into()),
528 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
529 new_text: "first_method($1)".to_string(),
530 range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
531 })),
532 additional_text_edits: Some(vec![lsp::TextEdit {
533 new_text: "use d::SomeTrait;\n".to_string(),
534 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
535 }]),
536 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
537 ..Default::default()
538 })
539 },
540 );
541
542 // The additional edit is applied.
543 cx_a.executor().run_until_parked();
544 cx_b.executor().run_until_parked();
545
546 buffer_a.read_with(cx_a, |buffer, _| {
547 assert_eq!(
548 buffer.text(),
549 "use d::SomeTrait;\nfn main() { a.first_method() }"
550 );
551 });
552
553 buffer_b.read_with(cx_b, |buffer, _| {
554 assert_eq!(
555 buffer.text(),
556 "use d::SomeTrait;\nfn main() { a.first_method() }"
557 );
558 });
559
560 // Now we do a second completion, this time to ensure that documentation/snippets are
561 // resolved
562 editor_b.update_in(cx_b, |editor, window, cx| {
563 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
564 s.select_ranges([MultiBufferOffset(46)..MultiBufferOffset(46)])
565 });
566 editor.handle_input("; a", window, cx);
567 editor.handle_input(".", window, cx);
568 });
569
570 buffer_b.read_with(cx_b, |buffer, _| {
571 assert_eq!(
572 buffer.text(),
573 "use d::SomeTrait;\nfn main() { a.first_method(); a. }"
574 );
575 });
576
577 let mut completion_response = fake_language_server
578 .set_request_handler::<lsp::request::Completion, _, _>(|params, _| async move {
579 assert_eq!(
580 params.text_document_position.text_document.uri,
581 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
582 );
583 assert_eq!(
584 params.text_document_position.position,
585 lsp::Position::new(1, 32),
586 );
587
588 Ok(Some(lsp::CompletionResponse::Array(vec![
589 lsp::CompletionItem {
590 label: "third_method(…)".into(),
591 detail: Some("fn(&mut self, B, C, D) -> E".into()),
592 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
593 // no snippet placeholders
594 new_text: "third_method".to_string(),
595 range: lsp::Range::new(
596 lsp::Position::new(1, 32),
597 lsp::Position::new(1, 32),
598 ),
599 })),
600 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
601 documentation: None,
602 ..Default::default()
603 },
604 ])))
605 });
606
607 // Second language server also needs to handle the request (returns None)
608 let mut second_completion_response = second_fake_language_server
609 .set_request_handler::<lsp::request::Completion, _, _>(|_, _| async move { Ok(None) });
610
611 // The completion now gets a new `text_edit.new_text` when resolving the completion item
612 let mut resolve_completion_response = fake_language_server
613 .set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(|params, _| async move {
614 assert_eq!(params.label, "third_method(…)");
615 Ok(lsp::CompletionItem {
616 label: "third_method(…)".into(),
617 detail: Some("fn(&mut self, B, C, D) -> E".into()),
618 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
619 // Now it's a snippet
620 new_text: "third_method($1, $2, $3)".to_string(),
621 range: lsp::Range::new(lsp::Position::new(1, 32), lsp::Position::new(1, 32)),
622 })),
623 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
624 documentation: Some(lsp::Documentation::String(
625 "this is the documentation".into(),
626 )),
627 ..Default::default()
628 })
629 });
630
631 cx_b.executor().run_until_parked();
632
633 completion_response.next().await.unwrap();
634 second_completion_response.next().await.unwrap();
635
636 editor_b.update_in(cx_b, |editor, window, cx| {
637 assert!(editor.context_menu_visible());
638 editor.context_menu_first(&ContextMenuFirst {}, window, cx);
639 });
640
641 resolve_completion_response.next().await.unwrap();
642 cx_b.executor().run_until_parked();
643
644 // When accepting the completion, the snippet is insert.
645 editor_b.update_in(cx_b, |editor, window, cx| {
646 assert!(editor.context_menu_visible());
647 editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, window, cx);
648 assert_eq!(
649 editor.text(cx),
650 "use d::SomeTrait;\nfn main() { a.first_method(); a.third_method(, , ) }"
651 );
652 });
653
654 // Ensure buffer is synced before proceeding with the next test
655 cx_a.executor().run_until_parked();
656 cx_b.executor().run_until_parked();
657
658 // Test completions from the second fake language server
659 // Add another completion trigger to test the second language server
660 editor_b.update_in(cx_b, |editor, window, cx| {
661 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
662 s.select_ranges([MultiBufferOffset(68)..MultiBufferOffset(68)])
663 });
664 editor.handle_input("; b", window, cx);
665 editor.handle_input(".", window, cx);
666 });
667
668 buffer_b.read_with(cx_b, |buffer, _| {
669 assert_eq!(
670 buffer.text(),
671 "use d::SomeTrait;\nfn main() { a.first_method(); a.third_method(, , ); b. }"
672 );
673 });
674
675 // Set up completion handlers for both language servers
676 let mut first_lsp_completion = fake_language_server
677 .set_request_handler::<lsp::request::Completion, _, _>(|_, _| async move { Ok(None) });
678
679 let mut second_lsp_completion = second_fake_language_server
680 .set_request_handler::<lsp::request::Completion, _, _>(|params, _| async move {
681 assert_eq!(
682 params.text_document_position.text_document.uri,
683 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
684 );
685 assert_eq!(
686 params.text_document_position.position,
687 lsp::Position::new(1, 54),
688 );
689
690 Ok(Some(lsp::CompletionResponse::Array(vec![
691 lsp::CompletionItem {
692 label: "analyzer_method(…)".into(),
693 detail: Some("fn(&self) -> Result<T>".into()),
694 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
695 new_text: "analyzer_method()".to_string(),
696 range: lsp::Range::new(
697 lsp::Position::new(1, 54),
698 lsp::Position::new(1, 54),
699 ),
700 })),
701 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
702 ..lsp::CompletionItem::default()
703 },
704 ])))
705 });
706
707 // Await both language server responses
708 first_lsp_completion.next().await.unwrap();
709 second_lsp_completion.next().await.unwrap();
710
711 cx_b.executor().run_until_parked();
712
713 // Confirm the completion from the second language server works
714 editor_b.update_in(cx_b, |editor, window, cx| {
715 assert!(editor.context_menu_visible());
716 editor.confirm_completion(&ConfirmCompletion { item_ix: Some(0) }, window, cx);
717 assert_eq!(
718 editor.text(cx),
719 "use d::SomeTrait;\nfn main() { a.first_method(); a.third_method(, , ); b.analyzer_method() }"
720 );
721 });
722}
723
724#[gpui::test(iterations = 10)]
725async fn test_collaborating_with_code_actions(
726 cx_a: &mut TestAppContext,
727 cx_b: &mut TestAppContext,
728) {
729 let mut server = TestServer::start(cx_a.executor()).await;
730 let client_a = server.create_client(cx_a, "user_a").await;
731 //
732 let client_b = server.create_client(cx_b, "user_b").await;
733 server
734 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
735 .await;
736 let active_call_a = cx_a.read(ActiveCall::global);
737
738 cx_b.update(editor::init);
739
740 client_a.language_registry().add(rust_lang());
741 let mut fake_language_servers = client_a
742 .language_registry()
743 .register_fake_lsp("Rust", FakeLspAdapter::default());
744 client_b.language_registry().add(rust_lang());
745 client_b
746 .language_registry()
747 .register_fake_lsp("Rust", FakeLspAdapter::default());
748
749 client_a
750 .fs()
751 .insert_tree(
752 path!("/a"),
753 json!({
754 "main.rs": "mod other;\nfn main() { let foo = other::foo(); }",
755 "other.rs": "pub fn foo() -> usize { 4 }",
756 }),
757 )
758 .await;
759 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
760 let project_id = active_call_a
761 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
762 .await
763 .unwrap();
764
765 // Join the project as client B.
766 let project_b = client_b.join_remote_project(project_id, cx_b).await;
767 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
768 let editor_b = workspace_b
769 .update_in(cx_b, |workspace, window, cx| {
770 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
771 })
772 .await
773 .unwrap()
774 .downcast::<Editor>()
775 .unwrap();
776
777 let mut fake_language_server = fake_language_servers.next().await.unwrap();
778 let mut requests = fake_language_server
779 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
780 assert_eq!(
781 params.text_document.uri,
782 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
783 );
784 assert_eq!(params.range.start, lsp::Position::new(0, 0));
785 assert_eq!(params.range.end, lsp::Position::new(0, 0));
786 Ok(None)
787 });
788 cx_a.background_executor
789 .advance_clock(editor::CODE_ACTIONS_DEBOUNCE_TIMEOUT * 2);
790 requests.next().await;
791
792 // Move cursor to a location that contains code actions.
793 editor_b.update_in(cx_b, |editor, window, cx| {
794 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
795 s.select_ranges([Point::new(1, 31)..Point::new(1, 31)])
796 });
797 });
798 cx_b.focus(&editor_b);
799
800 let mut requests = fake_language_server
801 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(|params, _| async move {
802 assert_eq!(
803 params.text_document.uri,
804 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
805 );
806 assert_eq!(params.range.start, lsp::Position::new(1, 31));
807 assert_eq!(params.range.end, lsp::Position::new(1, 31));
808
809 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
810 lsp::CodeAction {
811 title: "Inline into all callers".to_string(),
812 edit: Some(lsp::WorkspaceEdit {
813 changes: Some(
814 [
815 (
816 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
817 vec![lsp::TextEdit::new(
818 lsp::Range::new(
819 lsp::Position::new(1, 22),
820 lsp::Position::new(1, 34),
821 ),
822 "4".to_string(),
823 )],
824 ),
825 (
826 lsp::Uri::from_file_path(path!("/a/other.rs")).unwrap(),
827 vec![lsp::TextEdit::new(
828 lsp::Range::new(
829 lsp::Position::new(0, 0),
830 lsp::Position::new(0, 27),
831 ),
832 "".to_string(),
833 )],
834 ),
835 ]
836 .into_iter()
837 .collect(),
838 ),
839 ..Default::default()
840 }),
841 data: Some(json!({
842 "codeActionParams": {
843 "range": {
844 "start": {"line": 1, "column": 31},
845 "end": {"line": 1, "column": 31},
846 }
847 }
848 })),
849 ..Default::default()
850 },
851 )]))
852 });
853 cx_a.background_executor
854 .advance_clock(editor::CODE_ACTIONS_DEBOUNCE_TIMEOUT * 2);
855 requests.next().await;
856
857 // Toggle code actions and wait for them to display.
858 editor_b.update_in(cx_b, |editor, window, cx| {
859 editor.toggle_code_actions(
860 &ToggleCodeActions {
861 deployed_from: None,
862 quick_launch: false,
863 },
864 window,
865 cx,
866 );
867 });
868 cx_a.background_executor.run_until_parked();
869
870 editor_b.update(cx_b, |editor, _| assert!(editor.context_menu_visible()));
871
872 fake_language_server.remove_request_handler::<lsp::request::CodeActionRequest>();
873
874 // Confirming the code action will trigger a resolve request.
875 let confirm_action = editor_b
876 .update_in(cx_b, |editor, window, cx| {
877 Editor::confirm_code_action(editor, &ConfirmCodeAction { item_ix: Some(0) }, window, cx)
878 })
879 .unwrap();
880 fake_language_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>(
881 |_, _| async move {
882 Ok(lsp::CodeAction {
883 title: "Inline into all callers".to_string(),
884 edit: Some(lsp::WorkspaceEdit {
885 changes: Some(
886 [
887 (
888 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
889 vec![lsp::TextEdit::new(
890 lsp::Range::new(
891 lsp::Position::new(1, 22),
892 lsp::Position::new(1, 34),
893 ),
894 "4".to_string(),
895 )],
896 ),
897 (
898 lsp::Uri::from_file_path(path!("/a/other.rs")).unwrap(),
899 vec![lsp::TextEdit::new(
900 lsp::Range::new(
901 lsp::Position::new(0, 0),
902 lsp::Position::new(0, 27),
903 ),
904 "".to_string(),
905 )],
906 ),
907 ]
908 .into_iter()
909 .collect(),
910 ),
911 ..Default::default()
912 }),
913 ..Default::default()
914 })
915 },
916 );
917
918 // After the action is confirmed, an editor containing both modified files is opened.
919 confirm_action.await.unwrap();
920
921 let code_action_editor = workspace_b.update(cx_b, |workspace, cx| {
922 workspace
923 .active_item(cx)
924 .unwrap()
925 .downcast::<Editor>()
926 .unwrap()
927 });
928 code_action_editor.update_in(cx_b, |editor, window, cx| {
929 assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
930 editor.undo(&Undo, window, cx);
931 assert_eq!(
932 editor.text(cx),
933 "mod other;\nfn main() { let foo = other::foo(); }\npub fn foo() -> usize { 4 }"
934 );
935 editor.redo(&Redo, window, cx);
936 assert_eq!(editor.text(cx), "mod other;\nfn main() { let foo = 4; }\n");
937 });
938}
939
940#[gpui::test(iterations = 10)]
941async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
942 let mut server = TestServer::start(cx_a.executor()).await;
943 let client_a = server.create_client(cx_a, "user_a").await;
944 let client_b = server.create_client(cx_b, "user_b").await;
945 server
946 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
947 .await;
948 let active_call_a = cx_a.read(ActiveCall::global);
949
950 cx_b.update(editor::init);
951
952 let capabilities = lsp::ServerCapabilities {
953 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
954 prepare_provider: Some(true),
955 work_done_progress_options: Default::default(),
956 })),
957 ..lsp::ServerCapabilities::default()
958 };
959 client_a.language_registry().add(rust_lang());
960 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
961 "Rust",
962 FakeLspAdapter {
963 capabilities: capabilities.clone(),
964 ..FakeLspAdapter::default()
965 },
966 );
967 client_b.language_registry().add(rust_lang());
968 client_b.language_registry().register_fake_lsp_adapter(
969 "Rust",
970 FakeLspAdapter {
971 capabilities,
972 ..FakeLspAdapter::default()
973 },
974 );
975
976 client_a
977 .fs()
978 .insert_tree(
979 path!("/dir"),
980 json!({
981 "one.rs": "const ONE: usize = 1;",
982 "two.rs": "const TWO: usize = one::ONE + one::ONE;"
983 }),
984 )
985 .await;
986 let (project_a, worktree_id) = client_a.build_local_project(path!("/dir"), cx_a).await;
987 let project_id = active_call_a
988 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
989 .await
990 .unwrap();
991 let project_b = client_b.join_remote_project(project_id, cx_b).await;
992
993 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
994 let editor_b = workspace_b
995 .update_in(cx_b, |workspace, window, cx| {
996 workspace.open_path((worktree_id, rel_path("one.rs")), None, true, window, cx)
997 })
998 .await
999 .unwrap()
1000 .downcast::<Editor>()
1001 .unwrap();
1002 let fake_language_server = fake_language_servers.next().await.unwrap();
1003 cx_a.run_until_parked();
1004 cx_b.run_until_parked();
1005
1006 // Move cursor to a location that can be renamed.
1007 let prepare_rename = editor_b.update_in(cx_b, |editor, window, cx| {
1008 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1009 s.select_ranges([MultiBufferOffset(7)..MultiBufferOffset(7)])
1010 });
1011 editor.rename(&Rename, window, cx).unwrap()
1012 });
1013
1014 fake_language_server
1015 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(|params, _| async move {
1016 assert_eq!(
1017 params.text_document.uri.as_str(),
1018 uri!("file:///dir/one.rs")
1019 );
1020 assert_eq!(params.position, lsp::Position::new(0, 7));
1021 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
1022 lsp::Position::new(0, 6),
1023 lsp::Position::new(0, 9),
1024 ))))
1025 })
1026 .next()
1027 .await
1028 .unwrap();
1029 prepare_rename.await.unwrap();
1030 editor_b.update(cx_b, |editor, cx| {
1031 use editor::ToOffset;
1032 let rename = editor.pending_rename().unwrap();
1033 let buffer = editor.buffer().read(cx).snapshot(cx);
1034 assert_eq!(
1035 rename.range.start.to_offset(&buffer)..rename.range.end.to_offset(&buffer),
1036 MultiBufferOffset(6)..MultiBufferOffset(9)
1037 );
1038 rename.editor.update(cx, |rename_editor, cx| {
1039 let rename_selection = rename_editor.selections.newest::<MultiBufferOffset>(&rename_editor.display_snapshot(cx));
1040 assert_eq!(
1041 rename_selection.range(),
1042 MultiBufferOffset(0)..MultiBufferOffset(3),
1043 "Rename that was triggered from zero selection caret, should propose the whole word."
1044 );
1045 rename_editor.buffer().update(cx, |rename_buffer, cx| {
1046 rename_buffer.edit([(MultiBufferOffset(0)..MultiBufferOffset(3), "THREE")], None, cx);
1047 });
1048 });
1049 });
1050
1051 // Cancel the rename, and repeat the same, but use selections instead of cursor movement
1052 editor_b.update_in(cx_b, |editor, window, cx| {
1053 editor.cancel(&editor::actions::Cancel, window, cx);
1054 });
1055 let prepare_rename = editor_b.update_in(cx_b, |editor, window, cx| {
1056 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1057 s.select_ranges([MultiBufferOffset(7)..MultiBufferOffset(8)])
1058 });
1059 editor.rename(&Rename, window, cx).unwrap()
1060 });
1061
1062 fake_language_server
1063 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(|params, _| async move {
1064 assert_eq!(
1065 params.text_document.uri.as_str(),
1066 uri!("file:///dir/one.rs")
1067 );
1068 assert_eq!(params.position, lsp::Position::new(0, 8));
1069 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
1070 lsp::Position::new(0, 6),
1071 lsp::Position::new(0, 9),
1072 ))))
1073 })
1074 .next()
1075 .await
1076 .unwrap();
1077 prepare_rename.await.unwrap();
1078 editor_b.update(cx_b, |editor, cx| {
1079 use editor::ToOffset;
1080 let rename = editor.pending_rename().unwrap();
1081 let buffer = editor.buffer().read(cx).snapshot(cx);
1082 let lsp_rename_start = rename.range.start.to_offset(&buffer);
1083 let lsp_rename_end = rename.range.end.to_offset(&buffer);
1084 assert_eq!(lsp_rename_start..lsp_rename_end, MultiBufferOffset(6)..MultiBufferOffset(9));
1085 rename.editor.update(cx, |rename_editor, cx| {
1086 let rename_selection = rename_editor.selections.newest::<MultiBufferOffset>(&rename_editor.display_snapshot(cx));
1087 assert_eq!(
1088 rename_selection.range(),
1089 MultiBufferOffset(1)..MultiBufferOffset(2),
1090 "Rename that was triggered from a selection, should have the same selection range in the rename proposal"
1091 );
1092 rename_editor.buffer().update(cx, |rename_buffer, cx| {
1093 rename_buffer.edit([(MultiBufferOffset(0)..MultiBufferOffset(lsp_rename_end - lsp_rename_start), "THREE")], None, cx);
1094 });
1095 });
1096 });
1097
1098 let confirm_rename = editor_b.update_in(cx_b, |editor, window, cx| {
1099 Editor::confirm_rename(editor, &ConfirmRename, window, cx).unwrap()
1100 });
1101 fake_language_server
1102 .set_request_handler::<lsp::request::Rename, _, _>(|params, _| async move {
1103 assert_eq!(
1104 params.text_document_position.text_document.uri.as_str(),
1105 uri!("file:///dir/one.rs")
1106 );
1107 assert_eq!(
1108 params.text_document_position.position,
1109 lsp::Position::new(0, 6)
1110 );
1111 assert_eq!(params.new_name, "THREE");
1112 Ok(Some(lsp::WorkspaceEdit {
1113 changes: Some(
1114 [
1115 (
1116 lsp::Uri::from_file_path(path!("/dir/one.rs")).unwrap(),
1117 vec![lsp::TextEdit::new(
1118 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
1119 "THREE".to_string(),
1120 )],
1121 ),
1122 (
1123 lsp::Uri::from_file_path(path!("/dir/two.rs")).unwrap(),
1124 vec![
1125 lsp::TextEdit::new(
1126 lsp::Range::new(
1127 lsp::Position::new(0, 24),
1128 lsp::Position::new(0, 27),
1129 ),
1130 "THREE".to_string(),
1131 ),
1132 lsp::TextEdit::new(
1133 lsp::Range::new(
1134 lsp::Position::new(0, 35),
1135 lsp::Position::new(0, 38),
1136 ),
1137 "THREE".to_string(),
1138 ),
1139 ],
1140 ),
1141 ]
1142 .into_iter()
1143 .collect(),
1144 ),
1145 ..Default::default()
1146 }))
1147 })
1148 .next()
1149 .await
1150 .unwrap();
1151 confirm_rename.await.unwrap();
1152
1153 let rename_editor = workspace_b.update(cx_b, |workspace, cx| {
1154 workspace.active_item_as::<Editor>(cx).unwrap()
1155 });
1156
1157 rename_editor.update_in(cx_b, |editor, window, cx| {
1158 assert_eq!(
1159 editor.text(cx),
1160 "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
1161 );
1162 editor.undo(&Undo, window, cx);
1163 assert_eq!(
1164 editor.text(cx),
1165 "const ONE: usize = 1;\nconst TWO: usize = one::ONE + one::ONE;"
1166 );
1167 editor.redo(&Redo, window, cx);
1168 assert_eq!(
1169 editor.text(cx),
1170 "const THREE: usize = 1;\nconst TWO: usize = one::THREE + one::THREE;"
1171 );
1172 });
1173
1174 // Ensure temporary rename edits cannot be undone/redone.
1175 editor_b.update_in(cx_b, |editor, window, cx| {
1176 editor.undo(&Undo, window, cx);
1177 assert_eq!(editor.text(cx), "const ONE: usize = 1;");
1178 editor.undo(&Undo, window, cx);
1179 assert_eq!(editor.text(cx), "const ONE: usize = 1;");
1180 editor.redo(&Redo, window, cx);
1181 assert_eq!(editor.text(cx), "const THREE: usize = 1;");
1182 })
1183}
1184
1185#[gpui::test]
1186async fn test_slow_lsp_server(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
1187 let mut server = TestServer::start(cx_a.executor()).await;
1188 let client_a = server.create_client(cx_a, "user_a").await;
1189 let client_b = server.create_client(cx_b, "user_b").await;
1190 server
1191 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1192 .await;
1193 let active_call_a = cx_a.read(ActiveCall::global);
1194 cx_b.update(editor::init);
1195
1196 let command_name = "test_command";
1197 let capabilities = lsp::ServerCapabilities {
1198 code_lens_provider: Some(lsp::CodeLensOptions {
1199 resolve_provider: None,
1200 }),
1201 execute_command_provider: Some(lsp::ExecuteCommandOptions {
1202 commands: vec![command_name.to_string()],
1203 ..lsp::ExecuteCommandOptions::default()
1204 }),
1205 ..lsp::ServerCapabilities::default()
1206 };
1207 client_a.language_registry().add(rust_lang());
1208 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
1209 "Rust",
1210 FakeLspAdapter {
1211 capabilities: capabilities.clone(),
1212 ..FakeLspAdapter::default()
1213 },
1214 );
1215 client_b.language_registry().add(rust_lang());
1216 client_b.language_registry().register_fake_lsp_adapter(
1217 "Rust",
1218 FakeLspAdapter {
1219 capabilities,
1220 ..FakeLspAdapter::default()
1221 },
1222 );
1223
1224 client_a
1225 .fs()
1226 .insert_tree(
1227 path!("/dir"),
1228 json!({
1229 "one.rs": "const ONE: usize = 1;"
1230 }),
1231 )
1232 .await;
1233 let (project_a, worktree_id) = client_a.build_local_project(path!("/dir"), cx_a).await;
1234 let project_id = active_call_a
1235 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1236 .await
1237 .unwrap();
1238 let project_b = client_b.join_remote_project(project_id, cx_b).await;
1239
1240 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
1241 let editor_b = workspace_b
1242 .update_in(cx_b, |workspace, window, cx| {
1243 workspace.open_path((worktree_id, rel_path("one.rs")), None, true, window, cx)
1244 })
1245 .await
1246 .unwrap()
1247 .downcast::<Editor>()
1248 .unwrap();
1249 let (lsp_store_b, buffer_b) = editor_b.update(cx_b, |editor, cx| {
1250 let lsp_store = editor.project().unwrap().read(cx).lsp_store();
1251 let buffer = editor.buffer().read(cx).as_singleton().unwrap();
1252 (lsp_store, buffer)
1253 });
1254 let fake_language_server = fake_language_servers.next().await.unwrap();
1255 cx_a.run_until_parked();
1256 cx_b.run_until_parked();
1257
1258 let long_request_time = DEFAULT_LSP_REQUEST_TIMEOUT / 2;
1259 let (request_started_tx, mut request_started_rx) = mpsc::unbounded();
1260 let requests_started = Arc::new(AtomicUsize::new(0));
1261 let requests_completed = Arc::new(AtomicUsize::new(0));
1262 let _lens_requests = fake_language_server
1263 .set_request_handler::<lsp::request::CodeLensRequest, _, _>({
1264 let request_started_tx = request_started_tx.clone();
1265 let requests_started = requests_started.clone();
1266 let requests_completed = requests_completed.clone();
1267 move |params, cx| {
1268 let mut request_started_tx = request_started_tx.clone();
1269 let requests_started = requests_started.clone();
1270 let requests_completed = requests_completed.clone();
1271 async move {
1272 assert_eq!(
1273 params.text_document.uri.as_str(),
1274 uri!("file:///dir/one.rs")
1275 );
1276 requests_started.fetch_add(1, atomic::Ordering::Release);
1277 request_started_tx.send(()).await.unwrap();
1278 cx.background_executor().timer(long_request_time).await;
1279 let i = requests_completed.fetch_add(1, atomic::Ordering::Release) + 1;
1280 Ok(Some(vec![lsp::CodeLens {
1281 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 9)),
1282 command: Some(lsp::Command {
1283 title: format!("LSP Command {i}"),
1284 command: command_name.to_string(),
1285 arguments: None,
1286 }),
1287 data: None,
1288 }]))
1289 }
1290 }
1291 });
1292
1293 // Move cursor to a location, this should trigger the code lens call.
1294 editor_b.update_in(cx_b, |editor, window, cx| {
1295 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1296 s.select_ranges([MultiBufferOffset(7)..MultiBufferOffset(7)])
1297 });
1298 });
1299 let () = request_started_rx.next().await.unwrap();
1300 assert_eq!(
1301 requests_started.load(atomic::Ordering::Acquire),
1302 1,
1303 "Selection change should have initiated the first request"
1304 );
1305 assert_eq!(
1306 requests_completed.load(atomic::Ordering::Acquire),
1307 0,
1308 "Slow requests should be running still"
1309 );
1310 let _first_task = lsp_store_b.update(cx_b, |lsp_store, cx| {
1311 lsp_store
1312 .forget_code_lens_task(buffer_b.read(cx).remote_id())
1313 .expect("Should have the fetch task started")
1314 });
1315
1316 editor_b.update_in(cx_b, |editor, window, cx| {
1317 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1318 s.select_ranges([MultiBufferOffset(1)..MultiBufferOffset(1)])
1319 });
1320 });
1321 let () = request_started_rx.next().await.unwrap();
1322 assert_eq!(
1323 requests_started.load(atomic::Ordering::Acquire),
1324 2,
1325 "Selection change should have initiated the second request"
1326 );
1327 assert_eq!(
1328 requests_completed.load(atomic::Ordering::Acquire),
1329 0,
1330 "Slow requests should be running still"
1331 );
1332 let _second_task = lsp_store_b.update(cx_b, |lsp_store, cx| {
1333 lsp_store
1334 .forget_code_lens_task(buffer_b.read(cx).remote_id())
1335 .expect("Should have the fetch task started for the 2nd time")
1336 });
1337
1338 editor_b.update_in(cx_b, |editor, window, cx| {
1339 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1340 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(2)])
1341 });
1342 });
1343 let () = request_started_rx.next().await.unwrap();
1344 assert_eq!(
1345 requests_started.load(atomic::Ordering::Acquire),
1346 3,
1347 "Selection change should have initiated the third request"
1348 );
1349 assert_eq!(
1350 requests_completed.load(atomic::Ordering::Acquire),
1351 0,
1352 "Slow requests should be running still"
1353 );
1354
1355 _first_task.await.unwrap();
1356 _second_task.await.unwrap();
1357 cx_b.run_until_parked();
1358 assert_eq!(
1359 requests_started.load(atomic::Ordering::Acquire),
1360 3,
1361 "No selection changes should trigger no more code lens requests"
1362 );
1363 assert_eq!(
1364 requests_completed.load(atomic::Ordering::Acquire),
1365 1,
1366 "After enough time, a single, deduplicated, LSP request should have been served by the language server"
1367 );
1368 let resulting_lens_actions = editor_b
1369 .update(cx_b, |editor, cx| {
1370 let lsp_store = editor.project().unwrap().read(cx).lsp_store();
1371 lsp_store.update(cx, |lsp_store, cx| {
1372 lsp_store.code_lens_actions(&buffer_b, cx)
1373 })
1374 })
1375 .await
1376 .unwrap()
1377 .unwrap();
1378 assert_eq!(
1379 resulting_lens_actions.len(),
1380 1,
1381 "Should have fetched one code lens action, but got: {resulting_lens_actions:?}"
1382 );
1383 assert_eq!(
1384 resulting_lens_actions.first().unwrap().lsp_action.title(),
1385 "LSP Command 1",
1386 "Only the final code lens action should be in the data"
1387 )
1388}
1389
1390#[gpui::test(iterations = 10)]
1391async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
1392 let mut server = TestServer::start(cx_a.executor()).await;
1393 let executor = cx_a.executor();
1394 let client_a = server.create_client(cx_a, "user_a").await;
1395 let client_b = server.create_client(cx_b, "user_b").await;
1396 server
1397 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1398 .await;
1399 let active_call_a = cx_a.read(ActiveCall::global);
1400
1401 cx_b.update(editor::init);
1402
1403 client_a.language_registry().add(rust_lang());
1404 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
1405 "Rust",
1406 FakeLspAdapter {
1407 name: "the-language-server",
1408 ..Default::default()
1409 },
1410 );
1411
1412 client_a
1413 .fs()
1414 .insert_tree(
1415 path!("/dir"),
1416 json!({
1417 "main.rs": "const ONE: usize = 1;",
1418 }),
1419 )
1420 .await;
1421 let (project_a, _) = client_a.build_local_project(path!("/dir"), cx_a).await;
1422
1423 let _buffer_a = project_a
1424 .update(cx_a, |p, cx| {
1425 p.open_local_buffer_with_lsp(path!("/dir/main.rs"), cx)
1426 })
1427 .await
1428 .unwrap();
1429
1430 let fake_language_server = fake_language_servers.next().await.unwrap();
1431 executor.run_until_parked();
1432 fake_language_server.start_progress("the-token").await;
1433
1434 executor.advance_clock(SERVER_PROGRESS_THROTTLE_TIMEOUT);
1435 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
1436 token: lsp::NumberOrString::String("the-token".to_string()),
1437 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
1438 lsp::WorkDoneProgressReport {
1439 message: Some("the-message".to_string()),
1440 ..Default::default()
1441 },
1442 )),
1443 });
1444 executor.run_until_parked();
1445
1446 let token = ProgressToken::String(SharedString::from("the-token"));
1447
1448 project_a.read_with(cx_a, |project, cx| {
1449 let status = project.language_server_statuses(cx).next().unwrap().1;
1450 assert_eq!(status.name.0, "the-language-server");
1451 assert_eq!(status.pending_work.len(), 1);
1452 assert_eq!(
1453 status.pending_work[&token].message.as_ref().unwrap(),
1454 "the-message"
1455 );
1456 });
1457
1458 let project_id = active_call_a
1459 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1460 .await
1461 .unwrap();
1462 executor.run_until_parked();
1463 let project_b = client_b.join_remote_project(project_id, cx_b).await;
1464
1465 project_b.read_with(cx_b, |project, cx| {
1466 let status = project.language_server_statuses(cx).next().unwrap().1;
1467 assert_eq!(status.name.0, "the-language-server");
1468 });
1469
1470 executor.advance_clock(SERVER_PROGRESS_THROTTLE_TIMEOUT);
1471 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
1472 token: lsp::NumberOrString::String("the-token".to_string()),
1473 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Report(
1474 lsp::WorkDoneProgressReport {
1475 message: Some("the-message-2".to_string()),
1476 ..Default::default()
1477 },
1478 )),
1479 });
1480 executor.run_until_parked();
1481
1482 project_a.read_with(cx_a, |project, cx| {
1483 let status = project.language_server_statuses(cx).next().unwrap().1;
1484 assert_eq!(status.name.0, "the-language-server");
1485 assert_eq!(status.pending_work.len(), 1);
1486 assert_eq!(
1487 status.pending_work[&token].message.as_ref().unwrap(),
1488 "the-message-2"
1489 );
1490 });
1491
1492 project_b.read_with(cx_b, |project, cx| {
1493 let status = project.language_server_statuses(cx).next().unwrap().1;
1494 assert_eq!(status.name.0, "the-language-server");
1495 assert_eq!(status.pending_work.len(), 1);
1496 assert_eq!(
1497 status.pending_work[&token].message.as_ref().unwrap(),
1498 "the-message-2"
1499 );
1500 });
1501}
1502
1503#[gpui::test(iterations = 10)]
1504async fn test_share_project(
1505 cx_a: &mut TestAppContext,
1506 cx_b: &mut TestAppContext,
1507 cx_c: &mut TestAppContext,
1508) {
1509 let executor = cx_a.executor();
1510 let cx_b = cx_b.add_empty_window();
1511 let mut server = TestServer::start(executor.clone()).await;
1512 let client_a = server.create_client(cx_a, "user_a").await;
1513 let client_b = server.create_client(cx_b, "user_b").await;
1514 let client_c = server.create_client(cx_c, "user_c").await;
1515 server
1516 .make_contacts(&mut [(&client_a, cx_a), (&client_b, cx_b), (&client_c, cx_c)])
1517 .await;
1518 let active_call_a = cx_a.read(ActiveCall::global);
1519 let active_call_b = cx_b.read(ActiveCall::global);
1520 let active_call_c = cx_c.read(ActiveCall::global);
1521
1522 client_a
1523 .fs()
1524 .insert_tree(
1525 path!("/a"),
1526 json!({
1527 ".gitignore": "ignored-dir",
1528 "a.txt": "a-contents",
1529 "b.txt": "b-contents",
1530 "ignored-dir": {
1531 "c.txt": "",
1532 "d.txt": "",
1533 }
1534 }),
1535 )
1536 .await;
1537
1538 // Invite client B to collaborate on a project
1539 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
1540 active_call_a
1541 .update(cx_a, |call, cx| {
1542 call.invite(client_b.user_id().unwrap(), Some(project_a.clone()), cx)
1543 })
1544 .await
1545 .unwrap();
1546
1547 // Join that project as client B
1548
1549 let incoming_call_b = active_call_b.read_with(cx_b, |call, _| call.incoming());
1550 executor.run_until_parked();
1551 let call = incoming_call_b.borrow().clone().unwrap();
1552 assert_eq!(call.calling_user.github_login, "user_a");
1553 let initial_project = call.initial_project.unwrap();
1554 active_call_b
1555 .update(cx_b, |call, cx| call.accept_incoming(cx))
1556 .await
1557 .unwrap();
1558 let client_b_peer_id = client_b.peer_id().unwrap();
1559 let project_b = client_b.join_remote_project(initial_project.id, cx_b).await;
1560
1561 let replica_id_b = project_b.read_with(cx_b, |project, _| project.replica_id());
1562
1563 executor.run_until_parked();
1564
1565 project_a.read_with(cx_a, |project, _| {
1566 let client_b_collaborator = project.collaborators().get(&client_b_peer_id).unwrap();
1567 assert_eq!(client_b_collaborator.replica_id, replica_id_b);
1568 });
1569
1570 project_b.read_with(cx_b, |project, cx| {
1571 let worktree = project.worktrees(cx).next().unwrap().read(cx);
1572 assert_eq!(
1573 worktree.paths().collect::<Vec<_>>(),
1574 [
1575 rel_path(".gitignore"),
1576 rel_path("a.txt"),
1577 rel_path("b.txt"),
1578 rel_path("ignored-dir"),
1579 ]
1580 );
1581 });
1582
1583 project_b
1584 .update(cx_b, |project, cx| {
1585 let worktree = project.worktrees(cx).next().unwrap();
1586 let entry = worktree
1587 .read(cx)
1588 .entry_for_path(rel_path("ignored-dir"))
1589 .unwrap();
1590 project.expand_entry(worktree_id, entry.id, cx).unwrap()
1591 })
1592 .await
1593 .unwrap();
1594
1595 project_b.read_with(cx_b, |project, cx| {
1596 let worktree = project.worktrees(cx).next().unwrap().read(cx);
1597 assert_eq!(
1598 worktree.paths().collect::<Vec<_>>(),
1599 [
1600 rel_path(".gitignore"),
1601 rel_path("a.txt"),
1602 rel_path("b.txt"),
1603 rel_path("ignored-dir"),
1604 rel_path("ignored-dir/c.txt"),
1605 rel_path("ignored-dir/d.txt"),
1606 ]
1607 );
1608 });
1609
1610 // Open the same file as client B and client A.
1611 let buffer_b = project_b
1612 .update(cx_b, |p, cx| {
1613 p.open_buffer((worktree_id, rel_path("b.txt")), cx)
1614 })
1615 .await
1616 .unwrap();
1617
1618 buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "b-contents"));
1619
1620 project_a.read_with(cx_a, |project, cx| {
1621 assert!(project.has_open_buffer((worktree_id, rel_path("b.txt")), cx))
1622 });
1623 let buffer_a = project_a
1624 .update(cx_a, |p, cx| {
1625 p.open_buffer((worktree_id, rel_path("b.txt")), cx)
1626 })
1627 .await
1628 .unwrap();
1629
1630 let editor_b =
1631 cx_b.new_window_entity(|window, cx| Editor::for_buffer(buffer_b, None, window, cx));
1632
1633 // Client A sees client B's selection
1634 executor.run_until_parked();
1635
1636 buffer_a.read_with(cx_a, |buffer, _| {
1637 buffer
1638 .snapshot()
1639 .selections_in_range(
1640 text::Anchor::min_max_range_for_buffer(buffer.remote_id()),
1641 false,
1642 )
1643 .count()
1644 == 1
1645 });
1646
1647 // Edit the buffer as client B and see that edit as client A.
1648 editor_b.update_in(cx_b, |editor, window, cx| {
1649 editor.handle_input("ok, ", window, cx)
1650 });
1651 executor.run_until_parked();
1652
1653 buffer_a.read_with(cx_a, |buffer, _| {
1654 assert_eq!(buffer.text(), "ok, b-contents")
1655 });
1656
1657 // Client B can invite client C on a project shared by client A.
1658 active_call_b
1659 .update(cx_b, |call, cx| {
1660 call.invite(client_c.user_id().unwrap(), Some(project_b.clone()), cx)
1661 })
1662 .await
1663 .unwrap();
1664
1665 let incoming_call_c = active_call_c.read_with(cx_c, |call, _| call.incoming());
1666 executor.run_until_parked();
1667 let call = incoming_call_c.borrow().clone().unwrap();
1668 assert_eq!(call.calling_user.github_login, "user_b");
1669 let initial_project = call.initial_project.unwrap();
1670 active_call_c
1671 .update(cx_c, |call, cx| call.accept_incoming(cx))
1672 .await
1673 .unwrap();
1674 let _project_c = client_c.join_remote_project(initial_project.id, cx_c).await;
1675
1676 // Client B closes the editor, and client A sees client B's selections removed.
1677 cx_b.update(move |_, _| drop(editor_b));
1678 executor.run_until_parked();
1679
1680 buffer_a.read_with(cx_a, |buffer, _| {
1681 buffer
1682 .snapshot()
1683 .selections_in_range(
1684 text::Anchor::min_max_range_for_buffer(buffer.remote_id()),
1685 false,
1686 )
1687 .count()
1688 == 0
1689 });
1690}
1691
1692#[gpui::test(iterations = 10)]
1693async fn test_on_input_format_from_host_to_guest(
1694 cx_a: &mut TestAppContext,
1695 cx_b: &mut TestAppContext,
1696) {
1697 let mut server = TestServer::start(cx_a.executor()).await;
1698 let executor = cx_a.executor();
1699 let client_a = server.create_client(cx_a, "user_a").await;
1700 let client_b = server.create_client(cx_b, "user_b").await;
1701 server
1702 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1703 .await;
1704 let active_call_a = cx_a.read(ActiveCall::global);
1705
1706 client_a.language_registry().add(rust_lang());
1707 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
1708 "Rust",
1709 FakeLspAdapter {
1710 capabilities: lsp::ServerCapabilities {
1711 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
1712 first_trigger_character: ":".to_string(),
1713 more_trigger_character: Some(vec![">".to_string()]),
1714 }),
1715 ..Default::default()
1716 },
1717 ..Default::default()
1718 },
1719 );
1720
1721 client_a
1722 .fs()
1723 .insert_tree(
1724 path!("/a"),
1725 json!({
1726 "main.rs": "fn main() { a }",
1727 "other.rs": "// Test file",
1728 }),
1729 )
1730 .await;
1731 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
1732 let project_id = active_call_a
1733 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1734 .await
1735 .unwrap();
1736 let project_b = client_b.join_remote_project(project_id, cx_b).await;
1737
1738 // Open a file in an editor as the host.
1739 let buffer_a = project_a
1740 .update(cx_a, |p, cx| {
1741 p.open_buffer((worktree_id, rel_path("main.rs")), cx)
1742 })
1743 .await
1744 .unwrap();
1745 let cx_a = cx_a.add_empty_window();
1746 let editor_a = cx_a.new_window_entity(|window, cx| {
1747 Editor::for_buffer(buffer_a, Some(project_a.clone()), window, cx)
1748 });
1749
1750 let fake_language_server = fake_language_servers.next().await.unwrap();
1751 executor.run_until_parked();
1752
1753 // Receive an OnTypeFormatting request as the host's language server.
1754 // Return some formatting from the host's language server.
1755 fake_language_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
1756 |params, _| async move {
1757 assert_eq!(
1758 params.text_document_position.text_document.uri,
1759 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
1760 );
1761 assert_eq!(
1762 params.text_document_position.position,
1763 lsp::Position::new(0, 14),
1764 );
1765
1766 Ok(Some(vec![lsp::TextEdit {
1767 new_text: "~<".to_string(),
1768 range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
1769 }]))
1770 },
1771 );
1772
1773 // Open the buffer on the guest and see that the formatting worked
1774 let buffer_b = project_b
1775 .update(cx_b, |p, cx| {
1776 p.open_buffer((worktree_id, rel_path("main.rs")), cx)
1777 })
1778 .await
1779 .unwrap();
1780
1781 // Type a on type formatting trigger character as the guest.
1782 cx_a.focus(&editor_a);
1783 editor_a.update_in(cx_a, |editor, window, cx| {
1784 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1785 s.select_ranges([MultiBufferOffset(13)..MultiBufferOffset(13)])
1786 });
1787 editor.handle_input(">", window, cx);
1788 });
1789
1790 executor.run_until_parked();
1791
1792 buffer_b.read_with(cx_b, |buffer, _| {
1793 assert_eq!(buffer.text(), "fn main() { a>~< }")
1794 });
1795
1796 // Undo should remove LSP edits first
1797 editor_a.update_in(cx_a, |editor, window, cx| {
1798 assert_eq!(editor.text(cx), "fn main() { a>~< }");
1799 editor.undo(&Undo, window, cx);
1800 assert_eq!(editor.text(cx), "fn main() { a> }");
1801 });
1802 executor.run_until_parked();
1803
1804 buffer_b.read_with(cx_b, |buffer, _| {
1805 assert_eq!(buffer.text(), "fn main() { a> }")
1806 });
1807
1808 editor_a.update_in(cx_a, |editor, window, cx| {
1809 assert_eq!(editor.text(cx), "fn main() { a> }");
1810 editor.undo(&Undo, window, cx);
1811 assert_eq!(editor.text(cx), "fn main() { a }");
1812 });
1813 executor.run_until_parked();
1814
1815 buffer_b.read_with(cx_b, |buffer, _| {
1816 assert_eq!(buffer.text(), "fn main() { a }")
1817 });
1818}
1819
1820#[gpui::test(iterations = 10)]
1821async fn test_on_input_format_from_guest_to_host(
1822 cx_a: &mut TestAppContext,
1823 cx_b: &mut TestAppContext,
1824) {
1825 let mut server = TestServer::start(cx_a.executor()).await;
1826 let executor = cx_a.executor();
1827 let client_a = server.create_client(cx_a, "user_a").await;
1828 let client_b = server.create_client(cx_b, "user_b").await;
1829 server
1830 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1831 .await;
1832 let active_call_a = cx_a.read(ActiveCall::global);
1833
1834 let capabilities = lsp::ServerCapabilities {
1835 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
1836 first_trigger_character: ":".to_string(),
1837 more_trigger_character: Some(vec![">".to_string()]),
1838 }),
1839 ..lsp::ServerCapabilities::default()
1840 };
1841 client_a.language_registry().add(rust_lang());
1842 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
1843 "Rust",
1844 FakeLspAdapter {
1845 capabilities: capabilities.clone(),
1846 ..FakeLspAdapter::default()
1847 },
1848 );
1849 client_b.language_registry().add(rust_lang());
1850 client_b.language_registry().register_fake_lsp_adapter(
1851 "Rust",
1852 FakeLspAdapter {
1853 capabilities,
1854 ..FakeLspAdapter::default()
1855 },
1856 );
1857
1858 client_a
1859 .fs()
1860 .insert_tree(
1861 path!("/a"),
1862 json!({
1863 "main.rs": "fn main() { a }",
1864 "other.rs": "// Test file",
1865 }),
1866 )
1867 .await;
1868 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
1869 let project_id = active_call_a
1870 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
1871 .await
1872 .unwrap();
1873 let project_b = client_b.join_remote_project(project_id, cx_b).await;
1874
1875 // Open a file in an editor as the guest.
1876 let buffer_b = project_b
1877 .update(cx_b, |p, cx| {
1878 p.open_buffer((worktree_id, rel_path("main.rs")), cx)
1879 })
1880 .await
1881 .unwrap();
1882 let cx_b = cx_b.add_empty_window();
1883 let editor_b = cx_b.new_window_entity(|window, cx| {
1884 Editor::for_buffer(buffer_b, Some(project_b.clone()), window, cx)
1885 });
1886
1887 let fake_language_server = fake_language_servers.next().await.unwrap();
1888 executor.run_until_parked();
1889
1890 // Type a on type formatting trigger character as the guest.
1891 cx_b.focus(&editor_b);
1892 editor_b.update_in(cx_b, |editor, window, cx| {
1893 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1894 s.select_ranges([MultiBufferOffset(13)..MultiBufferOffset(13)])
1895 });
1896 editor.handle_input(":", window, cx);
1897 });
1898
1899 // Receive an OnTypeFormatting request as the host's language server.
1900 // Return some formatting from the host's language server.
1901 fake_language_server
1902 .set_request_handler::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
1903 assert_eq!(
1904 params.text_document_position.text_document.uri,
1905 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
1906 );
1907 assert_eq!(
1908 params.text_document_position.position,
1909 lsp::Position::new(0, 14),
1910 );
1911
1912 Ok(Some(vec![lsp::TextEdit {
1913 new_text: "~:".to_string(),
1914 range: lsp::Range::new(lsp::Position::new(0, 14), lsp::Position::new(0, 14)),
1915 }]))
1916 })
1917 .next()
1918 .await
1919 .unwrap();
1920
1921 // Open the buffer on the host and see that the formatting worked
1922 let buffer_a = project_a
1923 .update(cx_a, |p, cx| {
1924 p.open_buffer((worktree_id, rel_path("main.rs")), cx)
1925 })
1926 .await
1927 .unwrap();
1928 executor.run_until_parked();
1929
1930 buffer_a.read_with(cx_a, |buffer, _| {
1931 assert_eq!(buffer.text(), "fn main() { a:~: }")
1932 });
1933
1934 // Undo should remove LSP edits first
1935 editor_b.update_in(cx_b, |editor, window, cx| {
1936 assert_eq!(editor.text(cx), "fn main() { a:~: }");
1937 editor.undo(&Undo, window, cx);
1938 assert_eq!(editor.text(cx), "fn main() { a: }");
1939 });
1940 executor.run_until_parked();
1941
1942 buffer_a.read_with(cx_a, |buffer, _| {
1943 assert_eq!(buffer.text(), "fn main() { a: }")
1944 });
1945
1946 editor_b.update_in(cx_b, |editor, window, cx| {
1947 assert_eq!(editor.text(cx), "fn main() { a: }");
1948 editor.undo(&Undo, window, cx);
1949 assert_eq!(editor.text(cx), "fn main() { a }");
1950 });
1951 executor.run_until_parked();
1952
1953 buffer_a.read_with(cx_a, |buffer, _| {
1954 assert_eq!(buffer.text(), "fn main() { a }")
1955 });
1956}
1957
1958#[gpui::test(iterations = 10)]
1959async fn test_mutual_editor_inlay_hint_cache_update(
1960 cx_a: &mut TestAppContext,
1961 cx_b: &mut TestAppContext,
1962) {
1963 let mut server = TestServer::start(cx_a.executor()).await;
1964 let executor = cx_a.executor();
1965 let client_a = server.create_client(cx_a, "user_a").await;
1966 let client_b = server.create_client(cx_b, "user_b").await;
1967 server
1968 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
1969 .await;
1970 let active_call_a = cx_a.read(ActiveCall::global);
1971 let active_call_b = cx_b.read(ActiveCall::global);
1972
1973 cx_a.update(editor::init);
1974 cx_b.update(editor::init);
1975
1976 cx_a.update(|cx| {
1977 SettingsStore::update_global(cx, |store, cx| {
1978 store.update_user_settings(cx, |settings| {
1979 settings.project.all_languages.defaults.inlay_hints =
1980 Some(InlayHintSettingsContent {
1981 enabled: Some(true),
1982 ..InlayHintSettingsContent::default()
1983 })
1984 });
1985 });
1986 });
1987 cx_b.update(|cx| {
1988 SettingsStore::update_global(cx, |store, cx| {
1989 store.update_user_settings(cx, |settings| {
1990 settings.project.all_languages.defaults.inlay_hints =
1991 Some(InlayHintSettingsContent {
1992 enabled: Some(true),
1993 ..InlayHintSettingsContent::default()
1994 })
1995 });
1996 });
1997 });
1998
1999 let capabilities = lsp::ServerCapabilities {
2000 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
2001 ..lsp::ServerCapabilities::default()
2002 };
2003 client_a.language_registry().add(rust_lang());
2004
2005 // Set up the language server to return an additional inlay hint on each request.
2006 let edits_made = Arc::new(AtomicUsize::new(0));
2007 let closure_edits_made = Arc::clone(&edits_made);
2008 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
2009 "Rust",
2010 FakeLspAdapter {
2011 capabilities: capabilities.clone(),
2012 initializer: Some(Box::new(move |fake_language_server| {
2013 let closure_edits_made = closure_edits_made.clone();
2014 fake_language_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
2015 move |params, _| {
2016 let edits_made_2 = Arc::clone(&closure_edits_made);
2017 async move {
2018 assert_eq!(
2019 params.text_document.uri,
2020 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
2021 );
2022 let edits_made =
2023 AtomicUsize::load(&edits_made_2, atomic::Ordering::Acquire);
2024 Ok(Some(vec![lsp::InlayHint {
2025 position: lsp::Position::new(0, edits_made as u32),
2026 label: lsp::InlayHintLabel::String(edits_made.to_string()),
2027 kind: None,
2028 text_edits: None,
2029 tooltip: None,
2030 padding_left: None,
2031 padding_right: None,
2032 data: None,
2033 }]))
2034 }
2035 },
2036 );
2037 })),
2038 ..FakeLspAdapter::default()
2039 },
2040 );
2041 client_b.language_registry().add(rust_lang());
2042 client_b.language_registry().register_fake_lsp_adapter(
2043 "Rust",
2044 FakeLspAdapter {
2045 capabilities,
2046 ..FakeLspAdapter::default()
2047 },
2048 );
2049
2050 // Client A opens a project.
2051 client_a
2052 .fs()
2053 .insert_tree(
2054 path!("/a"),
2055 json!({
2056 "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
2057 "other.rs": "// Test file",
2058 }),
2059 )
2060 .await;
2061 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
2062 active_call_a
2063 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
2064 .await
2065 .unwrap();
2066 let project_id = active_call_a
2067 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2068 .await
2069 .unwrap();
2070
2071 // Client B joins the project
2072 let project_b = client_b.join_remote_project(project_id, cx_b).await;
2073 active_call_b
2074 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
2075 .await
2076 .unwrap();
2077
2078 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
2079
2080 // The host opens a rust file.
2081 let file_a = workspace_a.update_in(cx_a, |workspace, window, cx| {
2082 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
2083 });
2084 let fake_language_server = fake_language_servers.next().await.unwrap();
2085 let editor_a = file_a.await.unwrap().downcast::<Editor>().unwrap();
2086 executor.advance_clock(Duration::from_millis(100));
2087 executor.run_until_parked();
2088
2089 let initial_edit = edits_made.load(atomic::Ordering::Acquire);
2090 editor_a.update(cx_a, |editor, cx| {
2091 assert_eq!(
2092 vec![initial_edit.to_string()],
2093 extract_hint_labels(editor, cx),
2094 "Host should get its first hints when opens an editor"
2095 );
2096 });
2097 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
2098 let editor_b = workspace_b
2099 .update_in(cx_b, |workspace, window, cx| {
2100 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
2101 })
2102 .await
2103 .unwrap()
2104 .downcast::<Editor>()
2105 .unwrap();
2106
2107 executor.advance_clock(Duration::from_millis(100));
2108 executor.run_until_parked();
2109 editor_b.update(cx_b, |editor, cx| {
2110 assert_eq!(
2111 vec![initial_edit.to_string()],
2112 extract_hint_labels(editor, cx),
2113 "Client should get its first hints when opens an editor"
2114 );
2115 });
2116
2117 let after_client_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
2118 editor_b.update_in(cx_b, |editor, window, cx| {
2119 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2120 s.select_ranges([MultiBufferOffset(13)..MultiBufferOffset(13)].clone())
2121 });
2122 editor.handle_input(":", window, cx);
2123 });
2124 cx_b.focus(&editor_b);
2125
2126 executor.advance_clock(Duration::from_secs(1));
2127 executor.run_until_parked();
2128 editor_a.update(cx_a, |editor, cx| {
2129 assert_eq!(
2130 vec![after_client_edit.to_string()],
2131 extract_hint_labels(editor, cx),
2132 );
2133 });
2134 editor_b.update(cx_b, |editor, cx| {
2135 assert_eq!(
2136 vec![after_client_edit.to_string()],
2137 extract_hint_labels(editor, cx),
2138 );
2139 });
2140
2141 let after_host_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
2142 editor_a.update_in(cx_a, |editor, window, cx| {
2143 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2144 s.select_ranges([MultiBufferOffset(13)..MultiBufferOffset(13)])
2145 });
2146 editor.handle_input("a change to increment both buffers' versions", window, cx);
2147 });
2148 cx_a.focus(&editor_a);
2149
2150 executor.advance_clock(Duration::from_secs(1));
2151 executor.run_until_parked();
2152 editor_a.update(cx_a, |editor, cx| {
2153 assert_eq!(
2154 vec![after_host_edit.to_string()],
2155 extract_hint_labels(editor, cx),
2156 );
2157 });
2158 editor_b.update(cx_b, |editor, cx| {
2159 assert_eq!(
2160 vec![after_host_edit.to_string()],
2161 extract_hint_labels(editor, cx),
2162 );
2163 });
2164
2165 let after_special_edit_for_refresh = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
2166 fake_language_server
2167 .request::<lsp::request::InlayHintRefreshRequest>((), DEFAULT_LSP_REQUEST_TIMEOUT)
2168 .await
2169 .into_response()
2170 .expect("inlay refresh request failed");
2171
2172 executor.advance_clock(Duration::from_secs(1));
2173 executor.run_until_parked();
2174 editor_a.update(cx_a, |editor, cx| {
2175 assert_eq!(
2176 vec![after_special_edit_for_refresh.to_string()],
2177 extract_hint_labels(editor, cx),
2178 "Host should react to /refresh LSP request"
2179 );
2180 });
2181 editor_b.update(cx_b, |editor, cx| {
2182 assert_eq!(
2183 vec![after_special_edit_for_refresh.to_string()],
2184 extract_hint_labels(editor, cx),
2185 "Guest should get a /refresh LSP request propagated by host"
2186 );
2187 });
2188}
2189
2190#[gpui::test(iterations = 10)]
2191async fn test_inlay_hint_refresh_is_forwarded(
2192 cx_a: &mut TestAppContext,
2193 cx_b: &mut TestAppContext,
2194) {
2195 let mut server = TestServer::start(cx_a.executor()).await;
2196 let executor = cx_a.executor();
2197 let client_a = server.create_client(cx_a, "user_a").await;
2198 let client_b = server.create_client(cx_b, "user_b").await;
2199 server
2200 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2201 .await;
2202 let active_call_a = cx_a.read(ActiveCall::global);
2203 let active_call_b = cx_b.read(ActiveCall::global);
2204
2205 cx_a.update(editor::init);
2206 cx_b.update(editor::init);
2207
2208 cx_a.update(|cx| {
2209 SettingsStore::update_global(cx, |store, cx| {
2210 store.update_user_settings(cx, |settings| {
2211 settings.project.all_languages.defaults.inlay_hints =
2212 Some(InlayHintSettingsContent {
2213 show_value_hints: Some(true),
2214 enabled: Some(false),
2215 edit_debounce_ms: Some(0),
2216 scroll_debounce_ms: Some(0),
2217 show_type_hints: Some(false),
2218 show_parameter_hints: Some(false),
2219 show_other_hints: Some(false),
2220 show_background: Some(false),
2221 toggle_on_modifiers_press: None,
2222 })
2223 });
2224 });
2225 });
2226 cx_b.update(|cx| {
2227 SettingsStore::update_global(cx, |store, cx| {
2228 store.update_user_settings(cx, |settings| {
2229 settings.project.all_languages.defaults.inlay_hints =
2230 Some(InlayHintSettingsContent {
2231 show_value_hints: Some(true),
2232 enabled: Some(true),
2233 edit_debounce_ms: Some(0),
2234 scroll_debounce_ms: Some(0),
2235 show_type_hints: Some(true),
2236 show_parameter_hints: Some(true),
2237 show_other_hints: Some(true),
2238 show_background: Some(false),
2239 toggle_on_modifiers_press: None,
2240 })
2241 });
2242 });
2243 });
2244
2245 let capabilities = lsp::ServerCapabilities {
2246 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
2247 ..lsp::ServerCapabilities::default()
2248 };
2249 client_a.language_registry().add(rust_lang());
2250 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
2251 "Rust",
2252 FakeLspAdapter {
2253 capabilities: capabilities.clone(),
2254 ..FakeLspAdapter::default()
2255 },
2256 );
2257 client_b.language_registry().add(rust_lang());
2258 client_b.language_registry().register_fake_lsp_adapter(
2259 "Rust",
2260 FakeLspAdapter {
2261 capabilities,
2262 ..FakeLspAdapter::default()
2263 },
2264 );
2265
2266 client_a
2267 .fs()
2268 .insert_tree(
2269 path!("/a"),
2270 json!({
2271 "main.rs": "fn main() { a } // and some long comment to ensure inlay hints are not trimmed out",
2272 "other.rs": "// Test file",
2273 }),
2274 )
2275 .await;
2276 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
2277 active_call_a
2278 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
2279 .await
2280 .unwrap();
2281 let project_id = active_call_a
2282 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2283 .await
2284 .unwrap();
2285
2286 let project_b = client_b.join_remote_project(project_id, cx_b).await;
2287 active_call_b
2288 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
2289 .await
2290 .unwrap();
2291
2292 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
2293 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
2294
2295 let editor_a = workspace_a
2296 .update_in(cx_a, |workspace, window, cx| {
2297 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
2298 })
2299 .await
2300 .unwrap()
2301 .downcast::<Editor>()
2302 .unwrap();
2303
2304 let editor_b = workspace_b
2305 .update_in(cx_b, |workspace, window, cx| {
2306 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
2307 })
2308 .await
2309 .unwrap()
2310 .downcast::<Editor>()
2311 .unwrap();
2312
2313 let other_hints = Arc::new(AtomicBool::new(false));
2314 let fake_language_server = fake_language_servers.next().await.unwrap();
2315 let closure_other_hints = Arc::clone(&other_hints);
2316 fake_language_server
2317 .set_request_handler::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
2318 let task_other_hints = Arc::clone(&closure_other_hints);
2319 async move {
2320 assert_eq!(
2321 params.text_document.uri,
2322 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
2323 );
2324 let other_hints = task_other_hints.load(atomic::Ordering::Acquire);
2325 let character = if other_hints { 0 } else { 2 };
2326 let label = if other_hints {
2327 "other hint"
2328 } else {
2329 "initial hint"
2330 };
2331 Ok(Some(vec![
2332 lsp::InlayHint {
2333 position: lsp::Position::new(0, character),
2334 label: lsp::InlayHintLabel::String(label.to_string()),
2335 kind: None,
2336 text_edits: None,
2337 tooltip: None,
2338 padding_left: None,
2339 padding_right: None,
2340 data: None,
2341 },
2342 lsp::InlayHint {
2343 position: lsp::Position::new(1090, 1090),
2344 label: lsp::InlayHintLabel::String("out-of-bounds hint".to_string()),
2345 kind: None,
2346 text_edits: None,
2347 tooltip: None,
2348 padding_left: None,
2349 padding_right: None,
2350 data: None,
2351 },
2352 ]))
2353 }
2354 })
2355 .next()
2356 .await
2357 .unwrap();
2358
2359 executor.run_until_parked();
2360 editor_a.update(cx_a, |editor, cx| {
2361 assert!(
2362 extract_hint_labels(editor, cx).is_empty(),
2363 "Host should get no hints due to them turned off"
2364 );
2365 });
2366
2367 executor.run_until_parked();
2368 editor_b.update(cx_b, |editor, cx| {
2369 assert_eq!(
2370 vec!["initial hint".to_string()],
2371 extract_hint_labels(editor, cx),
2372 "Client should get its first hints when opens an editor"
2373 );
2374 });
2375
2376 other_hints.fetch_or(true, atomic::Ordering::Release);
2377 fake_language_server
2378 .request::<lsp::request::InlayHintRefreshRequest>((), DEFAULT_LSP_REQUEST_TIMEOUT)
2379 .await
2380 .into_response()
2381 .expect("inlay refresh request failed");
2382 executor.run_until_parked();
2383 editor_a.update(cx_a, |editor, cx| {
2384 assert!(
2385 extract_hint_labels(editor, cx).is_empty(),
2386 "Host should get no hints due to them turned off, even after the /refresh"
2387 );
2388 });
2389
2390 executor.run_until_parked();
2391 editor_b.update(cx_b, |editor, cx| {
2392 assert_eq!(
2393 vec!["other hint".to_string()],
2394 extract_hint_labels(editor, cx),
2395 "Guest should get a /refresh LSP request propagated by host despite host hints are off"
2396 );
2397 });
2398}
2399
2400#[gpui::test(iterations = 10)]
2401async fn test_lsp_document_color(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
2402 let expected_color = Rgba {
2403 r: 0.33,
2404 g: 0.33,
2405 b: 0.33,
2406 a: 0.33,
2407 };
2408 let mut server = TestServer::start(cx_a.executor()).await;
2409 let executor = cx_a.executor();
2410 let client_a = server.create_client(cx_a, "user_a").await;
2411 let client_b = server.create_client(cx_b, "user_b").await;
2412 server
2413 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2414 .await;
2415 let active_call_a = cx_a.read(ActiveCall::global);
2416 let active_call_b = cx_b.read(ActiveCall::global);
2417
2418 cx_a.update(editor::init);
2419 cx_b.update(editor::init);
2420
2421 cx_a.update(|cx| {
2422 SettingsStore::update_global(cx, |store, cx| {
2423 store.update_user_settings(cx, |settings| {
2424 settings.editor.lsp_document_colors = Some(DocumentColorsRenderMode::None);
2425 });
2426 });
2427 });
2428 cx_b.update(|cx| {
2429 SettingsStore::update_global(cx, |store, cx| {
2430 store.update_user_settings(cx, |settings| {
2431 settings.editor.lsp_document_colors = Some(DocumentColorsRenderMode::Inlay);
2432 });
2433 });
2434 });
2435
2436 let capabilities = lsp::ServerCapabilities {
2437 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
2438 ..lsp::ServerCapabilities::default()
2439 };
2440 client_a.language_registry().add(rust_lang());
2441 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
2442 "Rust",
2443 FakeLspAdapter {
2444 capabilities: capabilities.clone(),
2445 ..FakeLspAdapter::default()
2446 },
2447 );
2448 client_b.language_registry().add(rust_lang());
2449 client_b.language_registry().register_fake_lsp_adapter(
2450 "Rust",
2451 FakeLspAdapter {
2452 capabilities,
2453 ..FakeLspAdapter::default()
2454 },
2455 );
2456
2457 // Client A opens a project.
2458 client_a
2459 .fs()
2460 .insert_tree(
2461 path!("/a"),
2462 json!({
2463 "main.rs": "fn main() { a }",
2464 }),
2465 )
2466 .await;
2467 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
2468 active_call_a
2469 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
2470 .await
2471 .unwrap();
2472 let project_id = active_call_a
2473 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2474 .await
2475 .unwrap();
2476
2477 // Client B joins the project
2478 let project_b = client_b.join_remote_project(project_id, cx_b).await;
2479 active_call_b
2480 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
2481 .await
2482 .unwrap();
2483
2484 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
2485
2486 // The host opens a rust file.
2487 let _buffer_a = project_a
2488 .update(cx_a, |project, cx| {
2489 project.open_local_buffer(path!("/a/main.rs"), cx)
2490 })
2491 .await
2492 .unwrap();
2493 let editor_a = workspace_a
2494 .update_in(cx_a, |workspace, window, cx| {
2495 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
2496 })
2497 .await
2498 .unwrap()
2499 .downcast::<Editor>()
2500 .unwrap();
2501
2502 let fake_language_server = fake_language_servers.next().await.unwrap();
2503 cx_a.run_until_parked();
2504 cx_b.run_until_parked();
2505
2506 let requests_made = Arc::new(AtomicUsize::new(0));
2507 let closure_requests_made = Arc::clone(&requests_made);
2508 let mut color_request_handle = fake_language_server
2509 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
2510 let requests_made = Arc::clone(&closure_requests_made);
2511 async move {
2512 assert_eq!(
2513 params.text_document.uri,
2514 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
2515 );
2516 requests_made.fetch_add(1, atomic::Ordering::Release);
2517 Ok(vec![lsp::ColorInformation {
2518 range: lsp::Range {
2519 start: lsp::Position {
2520 line: 0,
2521 character: 0,
2522 },
2523 end: lsp::Position {
2524 line: 0,
2525 character: 1,
2526 },
2527 },
2528 color: lsp::Color {
2529 red: 0.33,
2530 green: 0.33,
2531 blue: 0.33,
2532 alpha: 0.33,
2533 },
2534 }])
2535 }
2536 });
2537 executor.run_until_parked();
2538
2539 assert_eq!(
2540 0,
2541 requests_made.load(atomic::Ordering::Acquire),
2542 "Host did not enable document colors, hence should query for none"
2543 );
2544 editor_a.update(cx_a, |editor, cx| {
2545 assert_eq!(
2546 Vec::<Rgba>::new(),
2547 extract_color_inlays(editor, cx),
2548 "No query colors should result in no hints"
2549 );
2550 });
2551
2552 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
2553 let editor_b = workspace_b
2554 .update_in(cx_b, |workspace, window, cx| {
2555 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
2556 })
2557 .await
2558 .unwrap()
2559 .downcast::<Editor>()
2560 .unwrap();
2561
2562 color_request_handle.next().await.unwrap();
2563 executor.advance_clock(LSP_REQUEST_DEBOUNCE_TIMEOUT);
2564 executor.run_until_parked();
2565
2566 assert_eq!(
2567 1,
2568 requests_made.load(atomic::Ordering::Acquire),
2569 "The client opened the file and got its first colors back"
2570 );
2571 editor_b.update(cx_b, |editor, cx| {
2572 assert_eq!(
2573 vec![expected_color],
2574 extract_color_inlays(editor, cx),
2575 "With document colors as inlays, color inlays should be pushed"
2576 );
2577 });
2578
2579 editor_a.update_in(cx_a, |editor, window, cx| {
2580 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2581 s.select_ranges([MultiBufferOffset(13)..MultiBufferOffset(13)].clone())
2582 });
2583 editor.handle_input(":", window, cx);
2584 });
2585 color_request_handle.next().await.unwrap();
2586 executor.run_until_parked();
2587 assert_eq!(
2588 2,
2589 requests_made.load(atomic::Ordering::Acquire),
2590 "After the host edits his file, the client should request the colors again"
2591 );
2592 editor_a.update(cx_a, |editor, cx| {
2593 assert_eq!(
2594 Vec::<Rgba>::new(),
2595 extract_color_inlays(editor, cx),
2596 "Host has no colors still"
2597 );
2598 });
2599 editor_b.update(cx_b, |editor, cx| {
2600 assert_eq!(vec![expected_color], extract_color_inlays(editor, cx),);
2601 });
2602
2603 cx_b.update(|_, cx| {
2604 SettingsStore::update_global(cx, |store, cx| {
2605 store.update_user_settings(cx, |settings| {
2606 settings.editor.lsp_document_colors = Some(DocumentColorsRenderMode::Background);
2607 });
2608 });
2609 });
2610 executor.run_until_parked();
2611 assert_eq!(
2612 2,
2613 requests_made.load(atomic::Ordering::Acquire),
2614 "After the client have changed the colors settings, no extra queries should happen"
2615 );
2616 editor_a.update(cx_a, |editor, cx| {
2617 assert_eq!(
2618 Vec::<Rgba>::new(),
2619 extract_color_inlays(editor, cx),
2620 "Host is unaffected by the client's settings changes"
2621 );
2622 });
2623 editor_b.update(cx_b, |editor, cx| {
2624 assert_eq!(
2625 Vec::<Rgba>::new(),
2626 extract_color_inlays(editor, cx),
2627 "Client should have no colors hints, as in the settings"
2628 );
2629 });
2630
2631 cx_b.update(|_, cx| {
2632 SettingsStore::update_global(cx, |store, cx| {
2633 store.update_user_settings(cx, |settings| {
2634 settings.editor.lsp_document_colors = Some(DocumentColorsRenderMode::Inlay);
2635 });
2636 });
2637 });
2638 executor.run_until_parked();
2639 assert_eq!(
2640 2,
2641 requests_made.load(atomic::Ordering::Acquire),
2642 "After falling back to colors as inlays, no extra LSP queries are made"
2643 );
2644 editor_a.update(cx_a, |editor, cx| {
2645 assert_eq!(
2646 Vec::<Rgba>::new(),
2647 extract_color_inlays(editor, cx),
2648 "Host is unaffected by the client's settings changes, again"
2649 );
2650 });
2651 editor_b.update(cx_b, |editor, cx| {
2652 assert_eq!(
2653 vec![expected_color],
2654 extract_color_inlays(editor, cx),
2655 "Client should have its color hints back"
2656 );
2657 });
2658
2659 cx_a.update(|_, cx| {
2660 SettingsStore::update_global(cx, |store, cx| {
2661 store.update_user_settings(cx, |settings| {
2662 settings.editor.lsp_document_colors = Some(DocumentColorsRenderMode::Border);
2663 });
2664 });
2665 });
2666 color_request_handle.next().await.unwrap();
2667 executor.run_until_parked();
2668 assert_eq!(
2669 3,
2670 requests_made.load(atomic::Ordering::Acquire),
2671 "After the host enables document colors, another LSP query should be made"
2672 );
2673 editor_a.update(cx_a, |editor, cx| {
2674 assert_eq!(
2675 Vec::<Rgba>::new(),
2676 extract_color_inlays(editor, cx),
2677 "Host did not configure document colors as hints hence gets nothing"
2678 );
2679 });
2680 editor_b.update(cx_b, |editor, cx| {
2681 assert_eq!(
2682 vec![expected_color],
2683 extract_color_inlays(editor, cx),
2684 "Client should be unaffected by the host's settings changes"
2685 );
2686 });
2687}
2688
2689async fn test_lsp_pull_diagnostics(
2690 should_stream_workspace_diagnostic: bool,
2691 cx_a: &mut TestAppContext,
2692 cx_b: &mut TestAppContext,
2693) {
2694 let mut server = TestServer::start(cx_a.executor()).await;
2695 let executor = cx_a.executor();
2696 let client_a = server.create_client(cx_a, "user_a").await;
2697 let client_b = server.create_client(cx_b, "user_b").await;
2698 server
2699 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
2700 .await;
2701 let active_call_a = cx_a.read(ActiveCall::global);
2702 let active_call_b = cx_b.read(ActiveCall::global);
2703
2704 cx_a.update(editor::init);
2705 cx_b.update(editor::init);
2706
2707 let expected_push_diagnostic_main_message = "pushed main diagnostic";
2708 let expected_push_diagnostic_lib_message = "pushed lib diagnostic";
2709 let expected_pull_diagnostic_main_message = "pulled main diagnostic";
2710 let expected_pull_diagnostic_lib_message = "pulled lib diagnostic";
2711 let expected_workspace_pull_diagnostics_main_message = "pulled workspace main diagnostic";
2712 let expected_workspace_pull_diagnostics_lib_message = "pulled workspace lib diagnostic";
2713
2714 let diagnostics_pulls_result_ids = Arc::new(Mutex::new(BTreeSet::<Option<String>>::new()));
2715 let workspace_diagnostics_pulls_result_ids = Arc::new(Mutex::new(BTreeSet::<String>::new()));
2716 let diagnostics_pulls_made = Arc::new(AtomicUsize::new(0));
2717 let closure_diagnostics_pulls_made = diagnostics_pulls_made.clone();
2718 let closure_diagnostics_pulls_result_ids = diagnostics_pulls_result_ids.clone();
2719 let workspace_diagnostics_pulls_made = Arc::new(AtomicUsize::new(0));
2720 let closure_workspace_diagnostics_pulls_made = workspace_diagnostics_pulls_made.clone();
2721 let closure_workspace_diagnostics_pulls_result_ids =
2722 workspace_diagnostics_pulls_result_ids.clone();
2723 let (workspace_diagnostic_cancel_tx, closure_workspace_diagnostic_cancel_rx) =
2724 smol::channel::bounded::<()>(1);
2725 let (closure_workspace_diagnostic_received_tx, workspace_diagnostic_received_rx) =
2726 smol::channel::bounded::<()>(1);
2727
2728 let capabilities = lsp::ServerCapabilities {
2729 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
2730 lsp::DiagnosticOptions {
2731 identifier: Some("test-pulls".to_string()),
2732 inter_file_dependencies: true,
2733 workspace_diagnostics: true,
2734 work_done_progress_options: lsp::WorkDoneProgressOptions {
2735 work_done_progress: None,
2736 },
2737 },
2738 )),
2739 ..lsp::ServerCapabilities::default()
2740 };
2741 client_a.language_registry().add(rust_lang());
2742
2743 let pull_diagnostics_handle = Arc::new(parking_lot::Mutex::new(None));
2744 let workspace_diagnostics_pulls_handle = Arc::new(parking_lot::Mutex::new(None));
2745
2746 let closure_pull_diagnostics_handle = pull_diagnostics_handle.clone();
2747 let closure_workspace_diagnostics_pulls_handle = workspace_diagnostics_pulls_handle.clone();
2748 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
2749 "Rust",
2750 FakeLspAdapter {
2751 capabilities: capabilities.clone(),
2752 initializer: Some(Box::new(move |fake_language_server| {
2753 let expected_workspace_diagnostic_token = lsp::ProgressToken::String(format!(
2754 "workspace/diagnostic/{}/1",
2755 fake_language_server.server.server_id()
2756 ));
2757 let closure_workspace_diagnostics_pulls_result_ids = closure_workspace_diagnostics_pulls_result_ids.clone();
2758 let diagnostics_pulls_made = closure_diagnostics_pulls_made.clone();
2759 let diagnostics_pulls_result_ids = closure_diagnostics_pulls_result_ids.clone();
2760 let closure_pull_diagnostics_handle = closure_pull_diagnostics_handle.clone();
2761 let closure_workspace_diagnostics_pulls_handle = closure_workspace_diagnostics_pulls_handle.clone();
2762 let closure_workspace_diagnostic_cancel_rx = closure_workspace_diagnostic_cancel_rx.clone();
2763 let closure_workspace_diagnostic_received_tx = closure_workspace_diagnostic_received_tx.clone();
2764 let pull_diagnostics_handle = fake_language_server
2765 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(
2766 move |params, _| {
2767 let requests_made = diagnostics_pulls_made.clone();
2768 let diagnostics_pulls_result_ids =
2769 diagnostics_pulls_result_ids.clone();
2770 async move {
2771 let message = if lsp::Uri::from_file_path(path!("/a/main.rs"))
2772 .unwrap()
2773 == params.text_document.uri
2774 {
2775 expected_pull_diagnostic_main_message.to_string()
2776 } else if lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap()
2777 == params.text_document.uri
2778 {
2779 expected_pull_diagnostic_lib_message.to_string()
2780 } else {
2781 panic!("Unexpected document: {}", params.text_document.uri)
2782 };
2783 {
2784 diagnostics_pulls_result_ids
2785 .lock()
2786 .await
2787 .insert(params.previous_result_id);
2788 }
2789 let new_requests_count =
2790 requests_made.fetch_add(1, atomic::Ordering::Release) + 1;
2791 Ok(lsp::DocumentDiagnosticReportResult::Report(
2792 lsp::DocumentDiagnosticReport::Full(
2793 lsp::RelatedFullDocumentDiagnosticReport {
2794 related_documents: None,
2795 full_document_diagnostic_report:
2796 lsp::FullDocumentDiagnosticReport {
2797 result_id: Some(format!(
2798 "pull-{new_requests_count}"
2799 )),
2800 items: vec![lsp::Diagnostic {
2801 range: lsp::Range {
2802 start: lsp::Position {
2803 line: 0,
2804 character: 0,
2805 },
2806 end: lsp::Position {
2807 line: 0,
2808 character: 2,
2809 },
2810 },
2811 severity: Some(
2812 lsp::DiagnosticSeverity::ERROR,
2813 ),
2814 message,
2815 ..lsp::Diagnostic::default()
2816 }],
2817 },
2818 },
2819 ),
2820 ))
2821 }
2822 },
2823 );
2824 let _ = closure_pull_diagnostics_handle.lock().insert(pull_diagnostics_handle);
2825
2826 let closure_workspace_diagnostics_pulls_made = closure_workspace_diagnostics_pulls_made.clone();
2827 let workspace_diagnostics_pulls_handle = fake_language_server.set_request_handler::<lsp::request::WorkspaceDiagnosticRequest, _, _>(
2828 move |params, _| {
2829 let workspace_requests_made = closure_workspace_diagnostics_pulls_made.clone();
2830 let workspace_diagnostics_pulls_result_ids =
2831 closure_workspace_diagnostics_pulls_result_ids.clone();
2832 let workspace_diagnostic_cancel_rx = closure_workspace_diagnostic_cancel_rx.clone();
2833 let workspace_diagnostic_received_tx = closure_workspace_diagnostic_received_tx.clone();
2834 let expected_workspace_diagnostic_token = expected_workspace_diagnostic_token.clone();
2835 async move {
2836 let workspace_request_count =
2837 workspace_requests_made.fetch_add(1, atomic::Ordering::Release) + 1;
2838 {
2839 workspace_diagnostics_pulls_result_ids
2840 .lock()
2841 .await
2842 .extend(params.previous_result_ids.into_iter().map(|id| id.value));
2843 }
2844 if should_stream_workspace_diagnostic && !workspace_diagnostic_cancel_rx.is_closed()
2845 {
2846 assert_eq!(
2847 params.partial_result_params.partial_result_token,
2848 Some(expected_workspace_diagnostic_token)
2849 );
2850 workspace_diagnostic_received_tx.send(()).await.unwrap();
2851 workspace_diagnostic_cancel_rx.recv().await.unwrap();
2852 workspace_diagnostic_cancel_rx.close();
2853 // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#partialResults
2854 // > The final response has to be empty in terms of result values.
2855 return Ok(lsp::WorkspaceDiagnosticReportResult::Report(
2856 lsp::WorkspaceDiagnosticReport { items: Vec::new() },
2857 ));
2858 }
2859 Ok(lsp::WorkspaceDiagnosticReportResult::Report(
2860 lsp::WorkspaceDiagnosticReport {
2861 items: vec![
2862 lsp::WorkspaceDocumentDiagnosticReport::Full(
2863 lsp::WorkspaceFullDocumentDiagnosticReport {
2864 uri: lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
2865 version: None,
2866 full_document_diagnostic_report:
2867 lsp::FullDocumentDiagnosticReport {
2868 result_id: Some(format!(
2869 "workspace_{workspace_request_count}"
2870 )),
2871 items: vec![lsp::Diagnostic {
2872 range: lsp::Range {
2873 start: lsp::Position {
2874 line: 0,
2875 character: 1,
2876 },
2877 end: lsp::Position {
2878 line: 0,
2879 character: 3,
2880 },
2881 },
2882 severity: Some(lsp::DiagnosticSeverity::WARNING),
2883 message:
2884 expected_workspace_pull_diagnostics_main_message
2885 .to_string(),
2886 ..lsp::Diagnostic::default()
2887 }],
2888 },
2889 },
2890 ),
2891 lsp::WorkspaceDocumentDiagnosticReport::Full(
2892 lsp::WorkspaceFullDocumentDiagnosticReport {
2893 uri: lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap(),
2894 version: None,
2895 full_document_diagnostic_report:
2896 lsp::FullDocumentDiagnosticReport {
2897 result_id: Some(format!(
2898 "workspace_{workspace_request_count}"
2899 )),
2900 items: vec![lsp::Diagnostic {
2901 range: lsp::Range {
2902 start: lsp::Position {
2903 line: 0,
2904 character: 1,
2905 },
2906 end: lsp::Position {
2907 line: 0,
2908 character: 3,
2909 },
2910 },
2911 severity: Some(lsp::DiagnosticSeverity::WARNING),
2912 message:
2913 expected_workspace_pull_diagnostics_lib_message
2914 .to_string(),
2915 ..lsp::Diagnostic::default()
2916 }],
2917 },
2918 },
2919 ),
2920 ],
2921 },
2922 ))
2923 }
2924 });
2925 let _ = closure_workspace_diagnostics_pulls_handle.lock().insert(workspace_diagnostics_pulls_handle);
2926 })),
2927 ..FakeLspAdapter::default()
2928 },
2929 );
2930
2931 client_b.language_registry().add(rust_lang());
2932 client_b.language_registry().register_fake_lsp_adapter(
2933 "Rust",
2934 FakeLspAdapter {
2935 capabilities,
2936 ..FakeLspAdapter::default()
2937 },
2938 );
2939
2940 // Client A opens a project.
2941 client_a
2942 .fs()
2943 .insert_tree(
2944 path!("/a"),
2945 json!({
2946 "main.rs": "fn main() { a }",
2947 "lib.rs": "fn other() {}",
2948 }),
2949 )
2950 .await;
2951 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
2952 active_call_a
2953 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
2954 .await
2955 .unwrap();
2956 let project_id = active_call_a
2957 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
2958 .await
2959 .unwrap();
2960
2961 // Client B joins the project
2962 let project_b = client_b.join_remote_project(project_id, cx_b).await;
2963 active_call_b
2964 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
2965 .await
2966 .unwrap();
2967
2968 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
2969
2970 // The host opens a rust file.
2971 let _buffer_a = project_a
2972 .update(cx_a, |project, cx| {
2973 project.open_local_buffer(path!("/a/main.rs"), cx)
2974 })
2975 .await
2976 .unwrap();
2977 let editor_a_main = workspace_a
2978 .update_in(cx_a, |workspace, window, cx| {
2979 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
2980 })
2981 .await
2982 .unwrap()
2983 .downcast::<Editor>()
2984 .unwrap();
2985
2986 let fake_language_server = fake_language_servers.next().await.unwrap();
2987 let expected_workspace_diagnostic_token = lsp::ProgressToken::String(format!(
2988 "workspace/diagnostic-{}-1",
2989 fake_language_server.server.server_id()
2990 ));
2991 cx_a.run_until_parked();
2992 cx_b.run_until_parked();
2993 let mut pull_diagnostics_handle = pull_diagnostics_handle.lock().take().unwrap();
2994 let mut workspace_diagnostics_pulls_handle =
2995 workspace_diagnostics_pulls_handle.lock().take().unwrap();
2996
2997 if should_stream_workspace_diagnostic {
2998 workspace_diagnostic_received_rx.recv().await.unwrap();
2999 } else {
3000 workspace_diagnostics_pulls_handle.next().await.unwrap();
3001 }
3002 assert_eq!(
3003 1,
3004 workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3005 "Workspace diagnostics should be pulled initially on a server startup"
3006 );
3007 pull_diagnostics_handle.next().await.unwrap();
3008 assert_eq!(
3009 1,
3010 diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3011 "Host should query pull diagnostics when the editor is opened"
3012 );
3013 executor.run_until_parked();
3014 editor_a_main.update(cx_a, |editor, cx| {
3015 let snapshot = editor.buffer().read(cx).snapshot(cx);
3016 let all_diagnostics = snapshot
3017 .diagnostics_in_range(MultiBufferOffset(0)..snapshot.len())
3018 .collect::<Vec<_>>();
3019 assert_eq!(
3020 all_diagnostics.len(),
3021 1,
3022 "Expected single diagnostic, but got: {all_diagnostics:?}"
3023 );
3024 let diagnostic = &all_diagnostics[0];
3025 let mut expected_messages = vec![expected_pull_diagnostic_main_message];
3026 if !should_stream_workspace_diagnostic {
3027 expected_messages.push(expected_workspace_pull_diagnostics_main_message);
3028 }
3029 assert!(
3030 expected_messages.contains(&diagnostic.diagnostic.message.as_str()),
3031 "Expected {expected_messages:?} on the host, but got: {}",
3032 diagnostic.diagnostic.message
3033 );
3034 });
3035
3036 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3037 lsp::PublishDiagnosticsParams {
3038 uri: lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
3039 diagnostics: vec![lsp::Diagnostic {
3040 range: lsp::Range {
3041 start: lsp::Position {
3042 line: 0,
3043 character: 3,
3044 },
3045 end: lsp::Position {
3046 line: 0,
3047 character: 4,
3048 },
3049 },
3050 severity: Some(lsp::DiagnosticSeverity::INFORMATION),
3051 message: expected_push_diagnostic_main_message.to_string(),
3052 ..lsp::Diagnostic::default()
3053 }],
3054 version: None,
3055 },
3056 );
3057 fake_language_server.notify::<lsp::notification::PublishDiagnostics>(
3058 lsp::PublishDiagnosticsParams {
3059 uri: lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap(),
3060 diagnostics: vec![lsp::Diagnostic {
3061 range: lsp::Range {
3062 start: lsp::Position {
3063 line: 0,
3064 character: 3,
3065 },
3066 end: lsp::Position {
3067 line: 0,
3068 character: 4,
3069 },
3070 },
3071 severity: Some(lsp::DiagnosticSeverity::INFORMATION),
3072 message: expected_push_diagnostic_lib_message.to_string(),
3073 ..lsp::Diagnostic::default()
3074 }],
3075 version: None,
3076 },
3077 );
3078
3079 if should_stream_workspace_diagnostic {
3080 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
3081 token: expected_workspace_diagnostic_token.clone(),
3082 value: lsp::ProgressParamsValue::WorkspaceDiagnostic(
3083 lsp::WorkspaceDiagnosticReportResult::Report(lsp::WorkspaceDiagnosticReport {
3084 items: vec![
3085 lsp::WorkspaceDocumentDiagnosticReport::Full(
3086 lsp::WorkspaceFullDocumentDiagnosticReport {
3087 uri: lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
3088 version: None,
3089 full_document_diagnostic_report:
3090 lsp::FullDocumentDiagnosticReport {
3091 result_id: Some(format!(
3092 "workspace_{}",
3093 workspace_diagnostics_pulls_made
3094 .fetch_add(1, atomic::Ordering::Release)
3095 + 1
3096 )),
3097 items: vec![lsp::Diagnostic {
3098 range: lsp::Range {
3099 start: lsp::Position {
3100 line: 0,
3101 character: 1,
3102 },
3103 end: lsp::Position {
3104 line: 0,
3105 character: 2,
3106 },
3107 },
3108 severity: Some(lsp::DiagnosticSeverity::ERROR),
3109 message:
3110 expected_workspace_pull_diagnostics_main_message
3111 .to_string(),
3112 ..lsp::Diagnostic::default()
3113 }],
3114 },
3115 },
3116 ),
3117 lsp::WorkspaceDocumentDiagnosticReport::Full(
3118 lsp::WorkspaceFullDocumentDiagnosticReport {
3119 uri: lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap(),
3120 version: None,
3121 full_document_diagnostic_report:
3122 lsp::FullDocumentDiagnosticReport {
3123 result_id: Some(format!(
3124 "workspace_{}",
3125 workspace_diagnostics_pulls_made
3126 .fetch_add(1, atomic::Ordering::Release)
3127 + 1
3128 )),
3129 items: Vec::new(),
3130 },
3131 },
3132 ),
3133 ],
3134 }),
3135 ),
3136 });
3137 };
3138
3139 let mut workspace_diagnostic_start_count =
3140 workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire);
3141
3142 executor.run_until_parked();
3143 editor_a_main.update(cx_a, |editor, cx| {
3144 let snapshot = editor.buffer().read(cx).snapshot(cx);
3145 let all_diagnostics = snapshot
3146 .diagnostics_in_range(MultiBufferOffset(0)..snapshot.len())
3147 .collect::<Vec<_>>();
3148 assert_eq!(
3149 all_diagnostics.len(),
3150 2,
3151 "Expected pull and push diagnostics, but got: {all_diagnostics:?}"
3152 );
3153 let expected_messages = [
3154 expected_workspace_pull_diagnostics_main_message,
3155 expected_pull_diagnostic_main_message,
3156 expected_push_diagnostic_main_message,
3157 ];
3158 for diagnostic in all_diagnostics {
3159 assert!(
3160 expected_messages.contains(&diagnostic.diagnostic.message.as_str()),
3161 "Expected push and pull messages on the host: {expected_messages:?}, but got: {}",
3162 diagnostic.diagnostic.message
3163 );
3164 }
3165 });
3166
3167 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
3168 let editor_b_main = workspace_b
3169 .update_in(cx_b, |workspace, window, cx| {
3170 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
3171 })
3172 .await
3173 .unwrap()
3174 .downcast::<Editor>()
3175 .unwrap();
3176 cx_b.run_until_parked();
3177
3178 pull_diagnostics_handle.next().await.unwrap();
3179 assert_eq!(
3180 2,
3181 diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3182 "Client should query pull diagnostics when its editor is opened"
3183 );
3184 executor.run_until_parked();
3185 assert_eq!(
3186 workspace_diagnostic_start_count,
3187 workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3188 "Workspace diagnostics should not be changed as the remote client does not initialize the workspace diagnostics pull"
3189 );
3190 editor_b_main.update(cx_b, |editor, cx| {
3191 let snapshot = editor.buffer().read(cx).snapshot(cx);
3192 let all_diagnostics = snapshot
3193 .diagnostics_in_range(MultiBufferOffset(0)..snapshot.len())
3194 .collect::<Vec<_>>();
3195 assert_eq!(
3196 all_diagnostics.len(),
3197 2,
3198 "Expected pull and push diagnostics, but got: {all_diagnostics:?}"
3199 );
3200
3201 // Despite the workspace diagnostics not re-initialized for the remote client, we can still expect its message synced from the host.
3202 let expected_messages = [
3203 expected_workspace_pull_diagnostics_main_message,
3204 expected_pull_diagnostic_main_message,
3205 expected_push_diagnostic_main_message,
3206 ];
3207 for diagnostic in all_diagnostics {
3208 assert!(
3209 expected_messages.contains(&diagnostic.diagnostic.message.as_str()),
3210 "The client should get both push and pull messages: {expected_messages:?}, but got: {}",
3211 diagnostic.diagnostic.message
3212 );
3213 }
3214 });
3215
3216 let editor_b_lib = workspace_b
3217 .update_in(cx_b, |workspace, window, cx| {
3218 workspace.open_path((worktree_id, rel_path("lib.rs")), None, true, window, cx)
3219 })
3220 .await
3221 .unwrap()
3222 .downcast::<Editor>()
3223 .unwrap();
3224
3225 pull_diagnostics_handle.next().await.unwrap();
3226 assert_eq!(
3227 3,
3228 diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3229 "Client should query pull diagnostics when its another editor is opened"
3230 );
3231 executor.run_until_parked();
3232 assert_eq!(
3233 workspace_diagnostic_start_count,
3234 workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3235 "The remote client still did not anything to trigger the workspace diagnostics pull"
3236 );
3237 editor_b_lib.update(cx_b, |editor, cx| {
3238 let snapshot = editor.buffer().read(cx).snapshot(cx);
3239 let all_diagnostics = snapshot
3240 .diagnostics_in_range(MultiBufferOffset(0)..snapshot.len())
3241 .collect::<Vec<_>>();
3242 let expected_messages = [
3243 expected_pull_diagnostic_lib_message,
3244 expected_push_diagnostic_lib_message,
3245 ];
3246 assert_eq!(
3247 all_diagnostics.len(),
3248 2,
3249 "Expected pull and push diagnostics, but got: {all_diagnostics:?}"
3250 );
3251 for diagnostic in all_diagnostics {
3252 assert!(
3253 expected_messages.contains(&diagnostic.diagnostic.message.as_str()),
3254 "The client should get both push and pull messages: {expected_messages:?}, but got: {}",
3255 diagnostic.diagnostic.message
3256 );
3257 }
3258 });
3259
3260 if should_stream_workspace_diagnostic {
3261 fake_language_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
3262 token: expected_workspace_diagnostic_token.clone(),
3263 value: lsp::ProgressParamsValue::WorkspaceDiagnostic(
3264 lsp::WorkspaceDiagnosticReportResult::Report(lsp::WorkspaceDiagnosticReport {
3265 items: vec![lsp::WorkspaceDocumentDiagnosticReport::Full(
3266 lsp::WorkspaceFullDocumentDiagnosticReport {
3267 uri: lsp::Uri::from_file_path(path!("/a/lib.rs")).unwrap(),
3268 version: None,
3269 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
3270 result_id: Some(format!(
3271 "workspace_{}",
3272 workspace_diagnostics_pulls_made
3273 .fetch_add(1, atomic::Ordering::Release)
3274 + 1
3275 )),
3276 items: vec![lsp::Diagnostic {
3277 range: lsp::Range {
3278 start: lsp::Position {
3279 line: 0,
3280 character: 1,
3281 },
3282 end: lsp::Position {
3283 line: 0,
3284 character: 2,
3285 },
3286 },
3287 severity: Some(lsp::DiagnosticSeverity::ERROR),
3288 message: expected_workspace_pull_diagnostics_lib_message
3289 .to_string(),
3290 ..lsp::Diagnostic::default()
3291 }],
3292 },
3293 },
3294 )],
3295 }),
3296 ),
3297 });
3298 workspace_diagnostic_start_count =
3299 workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire);
3300 workspace_diagnostic_cancel_tx.send(()).await.unwrap();
3301 workspace_diagnostics_pulls_handle.next().await.unwrap();
3302 executor.run_until_parked();
3303 editor_b_lib.update(cx_b, |editor, cx| {
3304 let snapshot = editor.buffer().read(cx).snapshot(cx);
3305 let all_diagnostics = snapshot
3306 .diagnostics_in_range(MultiBufferOffset(0)..snapshot.len())
3307 .collect::<Vec<_>>();
3308 let expected_messages = [
3309 // Despite workspace diagnostics provided,
3310 // the currently open file's diagnostics should be preferred, as LSP suggests.
3311 expected_pull_diagnostic_lib_message,
3312 expected_push_diagnostic_lib_message,
3313 ];
3314 assert_eq!(
3315 all_diagnostics.len(),
3316 2,
3317 "Expected pull and push diagnostics, but got: {all_diagnostics:?}"
3318 );
3319 for diagnostic in all_diagnostics {
3320 assert!(
3321 expected_messages.contains(&diagnostic.diagnostic.message.as_str()),
3322 "The client should get both push and pull messages: {expected_messages:?}, but got: {}",
3323 diagnostic.diagnostic.message
3324 );
3325 }
3326 });
3327 };
3328
3329 {
3330 assert!(
3331 !diagnostics_pulls_result_ids.lock().await.is_empty(),
3332 "Initial diagnostics pulls should report None at least"
3333 );
3334 assert_eq!(
3335 0,
3336 workspace_diagnostics_pulls_result_ids
3337 .lock()
3338 .await
3339 .deref()
3340 .len(),
3341 "After the initial workspace request, opening files should not reuse any result ids"
3342 );
3343 }
3344
3345 editor_b_lib.update_in(cx_b, |editor, window, cx| {
3346 editor.move_to_end(&MoveToEnd, window, cx);
3347 editor.handle_input(":", window, cx);
3348 });
3349 pull_diagnostics_handle.next().await.unwrap();
3350 // pull_diagnostics_handle.next().await.unwrap();
3351 assert_eq!(
3352 4,
3353 diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3354 "Client lib.rs edits should trigger another diagnostics pull for open buffers"
3355 );
3356 workspace_diagnostics_pulls_handle.next().await.unwrap();
3357 assert_eq!(
3358 workspace_diagnostic_start_count + 1,
3359 workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3360 "After client lib.rs edits, the workspace diagnostics request should follow"
3361 );
3362 executor.run_until_parked();
3363
3364 editor_b_main.update_in(cx_b, |editor, window, cx| {
3365 editor.move_to_end(&MoveToEnd, window, cx);
3366 editor.handle_input(":", window, cx);
3367 });
3368 pull_diagnostics_handle.next().await.unwrap();
3369 pull_diagnostics_handle.next().await.unwrap();
3370 pull_diagnostics_handle.next().await.unwrap();
3371 assert_eq!(
3372 7,
3373 diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3374 "Client main.rs edits should trigger diagnostics pull by both client and host and an extra pull for the client's lib.rs"
3375 );
3376 workspace_diagnostics_pulls_handle.next().await.unwrap();
3377 assert_eq!(
3378 workspace_diagnostic_start_count + 2,
3379 workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3380 "After client main.rs edits, the workspace diagnostics pull should follow"
3381 );
3382 executor.run_until_parked();
3383
3384 editor_a_main.update_in(cx_a, |editor, window, cx| {
3385 editor.move_to_end(&MoveToEnd, window, cx);
3386 editor.handle_input(":", window, cx);
3387 });
3388 pull_diagnostics_handle.next().await.unwrap();
3389 pull_diagnostics_handle.next().await.unwrap();
3390 pull_diagnostics_handle.next().await.unwrap();
3391 assert_eq!(
3392 10,
3393 diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3394 "Host main.rs edits should trigger another diagnostics pull by both client and host and another pull for the client's lib.rs"
3395 );
3396 workspace_diagnostics_pulls_handle.next().await.unwrap();
3397 assert_eq!(
3398 workspace_diagnostic_start_count + 3,
3399 workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3400 "After host main.rs edits, the workspace diagnostics pull should follow"
3401 );
3402 executor.run_until_parked();
3403 let diagnostic_pulls_result_ids = diagnostics_pulls_result_ids.lock().await.len();
3404 let workspace_pulls_result_ids = workspace_diagnostics_pulls_result_ids.lock().await.len();
3405 {
3406 assert!(
3407 diagnostic_pulls_result_ids > 1,
3408 "Should have sent result ids when pulling diagnostics"
3409 );
3410 assert!(
3411 workspace_pulls_result_ids > 1,
3412 "Should have sent result ids when pulling workspace diagnostics"
3413 );
3414 }
3415
3416 fake_language_server
3417 .request::<lsp::request::WorkspaceDiagnosticRefresh>((), DEFAULT_LSP_REQUEST_TIMEOUT)
3418 .await
3419 .into_response()
3420 .expect("workspace diagnostics refresh request failed");
3421 // Workspace refresh now also triggers document diagnostic pulls for all open buffers
3422 pull_diagnostics_handle.next().await.unwrap();
3423 pull_diagnostics_handle.next().await.unwrap();
3424 assert_eq!(
3425 12,
3426 diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3427 "Workspace refresh should trigger document pulls for all open buffers (main.rs and lib.rs)"
3428 );
3429 workspace_diagnostics_pulls_handle.next().await.unwrap();
3430 assert_eq!(
3431 workspace_diagnostic_start_count + 4,
3432 workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire),
3433 "Another workspace diagnostics pull should happen after the diagnostics refresh server request"
3434 );
3435 {
3436 assert!(
3437 diagnostics_pulls_result_ids.lock().await.len() > diagnostic_pulls_result_ids,
3438 "Document diagnostic pulls should happen after workspace refresh"
3439 );
3440 assert!(
3441 workspace_diagnostics_pulls_result_ids.lock().await.len() > workspace_pulls_result_ids,
3442 "More workspace diagnostics should be pulled"
3443 );
3444 }
3445 editor_b_lib.update(cx_b, |editor, cx| {
3446 let snapshot = editor.buffer().read(cx).snapshot(cx);
3447 let all_diagnostics = snapshot
3448 .diagnostics_in_range(MultiBufferOffset(0)..snapshot.len())
3449 .collect::<Vec<_>>();
3450 let expected_messages = [
3451 expected_workspace_pull_diagnostics_lib_message,
3452 expected_pull_diagnostic_lib_message,
3453 expected_push_diagnostic_lib_message,
3454 ];
3455 assert_eq!(all_diagnostics.len(), 2);
3456 for diagnostic in &all_diagnostics {
3457 assert!(
3458 expected_messages.contains(&diagnostic.diagnostic.message.as_str()),
3459 "Unexpected diagnostics: {all_diagnostics:?}"
3460 );
3461 }
3462 });
3463 editor_b_main.update(cx_b, |editor, cx| {
3464 let snapshot = editor.buffer().read(cx).snapshot(cx);
3465 let all_diagnostics = snapshot
3466 .diagnostics_in_range(MultiBufferOffset(0)..snapshot.len())
3467 .collect::<Vec<_>>();
3468 assert_eq!(all_diagnostics.len(), 2);
3469
3470 let expected_messages = [
3471 expected_workspace_pull_diagnostics_main_message,
3472 expected_pull_diagnostic_main_message,
3473 expected_push_diagnostic_main_message,
3474 ];
3475 for diagnostic in &all_diagnostics {
3476 assert!(
3477 expected_messages.contains(&diagnostic.diagnostic.message.as_str()),
3478 "Unexpected diagnostics: {all_diagnostics:?}"
3479 );
3480 }
3481 });
3482 editor_a_main.update(cx_a, |editor, cx| {
3483 let snapshot = editor.buffer().read(cx).snapshot(cx);
3484 let all_diagnostics = snapshot
3485 .diagnostics_in_range(MultiBufferOffset(0)..snapshot.len())
3486 .collect::<Vec<_>>();
3487 assert_eq!(all_diagnostics.len(), 2);
3488 let expected_messages = [
3489 expected_workspace_pull_diagnostics_main_message,
3490 expected_pull_diagnostic_main_message,
3491 expected_push_diagnostic_main_message,
3492 ];
3493 for diagnostic in &all_diagnostics {
3494 assert!(
3495 expected_messages.contains(&diagnostic.diagnostic.message.as_str()),
3496 "Unexpected diagnostics: {all_diagnostics:?}"
3497 );
3498 }
3499 });
3500}
3501
3502#[gpui::test(iterations = 10)]
3503async fn test_non_streamed_lsp_pull_diagnostics(
3504 cx_a: &mut TestAppContext,
3505 cx_b: &mut TestAppContext,
3506) {
3507 test_lsp_pull_diagnostics(false, cx_a, cx_b).await;
3508}
3509
3510#[gpui::test(iterations = 10)]
3511async fn test_streamed_lsp_pull_diagnostics(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
3512 test_lsp_pull_diagnostics(true, cx_a, cx_b).await;
3513}
3514
3515#[gpui::test(iterations = 10)]
3516async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
3517 let mut server = TestServer::start(cx_a.executor()).await;
3518 let client_a = server.create_client(cx_a, "user_a").await;
3519 let client_b = server.create_client(cx_b, "user_b").await;
3520 server
3521 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3522 .await;
3523 let active_call_a = cx_a.read(ActiveCall::global);
3524
3525 cx_a.update(editor::init);
3526 cx_b.update(editor::init);
3527 // Turn inline-blame-off by default so no state is transferred without us explicitly doing so
3528 let inline_blame_off_settings = Some(InlineBlameSettings {
3529 enabled: Some(false),
3530 ..Default::default()
3531 });
3532 cx_a.update(|cx| {
3533 SettingsStore::update_global(cx, |store, cx| {
3534 store.update_user_settings(cx, |settings| {
3535 settings.git.get_or_insert_default().inline_blame = inline_blame_off_settings;
3536 });
3537 });
3538 });
3539 cx_b.update(|cx| {
3540 SettingsStore::update_global(cx, |store, cx| {
3541 store.update_user_settings(cx, |settings| {
3542 settings.git.get_or_insert_default().inline_blame = inline_blame_off_settings;
3543 });
3544 });
3545 });
3546
3547 client_a
3548 .fs()
3549 .insert_tree(
3550 path!("/my-repo"),
3551 json!({
3552 ".git": {},
3553 "file.txt": "line1\nline2\nline3\nline\n",
3554 }),
3555 )
3556 .await;
3557
3558 let blame = git::blame::Blame {
3559 entries: vec![
3560 blame_entry("1b1b1b", 0..1),
3561 blame_entry("0d0d0d", 1..2),
3562 blame_entry("3a3a3a", 2..3),
3563 blame_entry("4c4c4c", 3..4),
3564 ],
3565 messages: [
3566 ("1b1b1b", "message for idx-0"),
3567 ("0d0d0d", "message for idx-1"),
3568 ("3a3a3a", "message for idx-2"),
3569 ("4c4c4c", "message for idx-3"),
3570 ]
3571 .into_iter()
3572 .map(|(sha, message)| (sha.parse().unwrap(), message.into()))
3573 .collect(),
3574 };
3575 client_a.fs().set_blame_for_repo(
3576 Path::new(path!("/my-repo/.git")),
3577 vec![(repo_path("file.txt"), blame)],
3578 );
3579
3580 let (project_a, worktree_id) = client_a.build_local_project(path!("/my-repo"), cx_a).await;
3581 let project_id = active_call_a
3582 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3583 .await
3584 .unwrap();
3585
3586 // Create editor_a
3587 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
3588 let editor_a = workspace_a
3589 .update_in(cx_a, |workspace, window, cx| {
3590 workspace.open_path((worktree_id, rel_path("file.txt")), None, true, window, cx)
3591 })
3592 .await
3593 .unwrap()
3594 .downcast::<Editor>()
3595 .unwrap();
3596
3597 // Join the project as client B.
3598 let project_b = client_b.join_remote_project(project_id, cx_b).await;
3599 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
3600 let editor_b = workspace_b
3601 .update_in(cx_b, |workspace, window, cx| {
3602 workspace.open_path((worktree_id, rel_path("file.txt")), None, true, window, cx)
3603 })
3604 .await
3605 .unwrap()
3606 .downcast::<Editor>()
3607 .unwrap();
3608 let buffer_id_b = editor_b.update(cx_b, |editor_b, cx| {
3609 editor_b
3610 .buffer()
3611 .read(cx)
3612 .as_singleton()
3613 .unwrap()
3614 .read(cx)
3615 .remote_id()
3616 });
3617
3618 // client_b now requests git blame for the open buffer
3619 editor_b.update_in(cx_b, |editor_b, window, cx| {
3620 assert!(editor_b.blame().is_none());
3621 editor_b.toggle_git_blame(&git::Blame {}, window, cx);
3622 });
3623
3624 cx_a.executor().run_until_parked();
3625 cx_b.executor().run_until_parked();
3626
3627 editor_b.update(cx_b, |editor_b, cx| {
3628 let blame = editor_b.blame().expect("editor_b should have blame now");
3629 let entries = blame.update(cx, |blame, cx| {
3630 blame
3631 .blame_for_rows(
3632 &(0..4)
3633 .map(|row| RowInfo {
3634 buffer_row: Some(row),
3635 buffer_id: Some(buffer_id_b),
3636 ..Default::default()
3637 })
3638 .collect::<Vec<_>>(),
3639 cx,
3640 )
3641 .collect::<Vec<_>>()
3642 });
3643
3644 assert_eq!(
3645 entries,
3646 vec![
3647 Some((buffer_id_b, blame_entry("1b1b1b", 0..1))),
3648 Some((buffer_id_b, blame_entry("0d0d0d", 1..2))),
3649 Some((buffer_id_b, blame_entry("3a3a3a", 2..3))),
3650 Some((buffer_id_b, blame_entry("4c4c4c", 3..4))),
3651 ]
3652 );
3653
3654 blame.update(cx, |blame, _| {
3655 for (idx, (buffer, entry)) in entries.iter().flatten().enumerate() {
3656 let details = blame.details_for_entry(*buffer, entry).unwrap();
3657 assert_eq!(details.message, format!("message for idx-{}", idx));
3658 }
3659 });
3660 });
3661
3662 // editor_b updates the file, which gets sent to client_a, which updates git blame,
3663 // which gets back to client_b.
3664 editor_b.update_in(cx_b, |editor_b, _, cx| {
3665 editor_b.edit([(Point::new(0, 3)..Point::new(0, 3), "FOO")], cx);
3666 });
3667
3668 cx_a.executor().run_until_parked();
3669 cx_b.executor().run_until_parked();
3670
3671 editor_b.update(cx_b, |editor_b, cx| {
3672 let blame = editor_b.blame().expect("editor_b should have blame now");
3673 let entries = blame.update(cx, |blame, cx| {
3674 blame
3675 .blame_for_rows(
3676 &(0..4)
3677 .map(|row| RowInfo {
3678 buffer_row: Some(row),
3679 buffer_id: Some(buffer_id_b),
3680 ..Default::default()
3681 })
3682 .collect::<Vec<_>>(),
3683 cx,
3684 )
3685 .collect::<Vec<_>>()
3686 });
3687
3688 assert_eq!(
3689 entries,
3690 vec![
3691 None,
3692 Some((buffer_id_b, blame_entry("0d0d0d", 1..2))),
3693 Some((buffer_id_b, blame_entry("3a3a3a", 2..3))),
3694 Some((buffer_id_b, blame_entry("4c4c4c", 3..4))),
3695 ]
3696 );
3697 });
3698
3699 // Now editor_a also updates the file
3700 editor_a.update_in(cx_a, |editor_a, _, cx| {
3701 editor_a.edit([(Point::new(1, 3)..Point::new(1, 3), "FOO")], cx);
3702 });
3703
3704 cx_a.executor().run_until_parked();
3705 cx_b.executor().run_until_parked();
3706
3707 editor_b.update(cx_b, |editor_b, cx| {
3708 let blame = editor_b.blame().expect("editor_b should have blame now");
3709 let entries = blame.update(cx, |blame, cx| {
3710 blame
3711 .blame_for_rows(
3712 &(0..4)
3713 .map(|row| RowInfo {
3714 buffer_row: Some(row),
3715 buffer_id: Some(buffer_id_b),
3716 ..Default::default()
3717 })
3718 .collect::<Vec<_>>(),
3719 cx,
3720 )
3721 .collect::<Vec<_>>()
3722 });
3723
3724 assert_eq!(
3725 entries,
3726 vec![
3727 None,
3728 None,
3729 Some((buffer_id_b, blame_entry("3a3a3a", 2..3))),
3730 Some((buffer_id_b, blame_entry("4c4c4c", 3..4))),
3731 ]
3732 );
3733 });
3734}
3735
3736#[gpui::test(iterations = 30)]
3737async fn test_collaborating_with_editorconfig(
3738 cx_a: &mut TestAppContext,
3739 cx_b: &mut TestAppContext,
3740) {
3741 let mut server = TestServer::start(cx_a.executor()).await;
3742 let client_a = server.create_client(cx_a, "user_a").await;
3743 let client_b = server.create_client(cx_b, "user_b").await;
3744 server
3745 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3746 .await;
3747 let active_call_a = cx_a.read(ActiveCall::global);
3748
3749 cx_b.update(editor::init);
3750
3751 // Set up a fake language server.
3752 client_a.language_registry().add(rust_lang());
3753 client_a
3754 .fs()
3755 .insert_tree(
3756 path!("/a"),
3757 json!({
3758 "src": {
3759 "main.rs": "mod other;\nfn main() { let foo = other::foo(); }",
3760 "other_mod": {
3761 "other.rs": "pub fn foo() -> usize {\n 4\n}",
3762 ".editorconfig": "",
3763 },
3764 },
3765 ".editorconfig": "[*]\ntab_width = 2\n",
3766 }),
3767 )
3768 .await;
3769 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
3770 let project_id = active_call_a
3771 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
3772 .await
3773 .unwrap();
3774 let main_buffer_a = project_a
3775 .update(cx_a, |p, cx| {
3776 p.open_buffer((worktree_id, rel_path("src/main.rs")), cx)
3777 })
3778 .await
3779 .unwrap();
3780 let other_buffer_a = project_a
3781 .update(cx_a, |p, cx| {
3782 p.open_buffer((worktree_id, rel_path("src/other_mod/other.rs")), cx)
3783 })
3784 .await
3785 .unwrap();
3786 let cx_a = cx_a.add_empty_window();
3787 let main_editor_a = cx_a.new_window_entity(|window, cx| {
3788 Editor::for_buffer(main_buffer_a, Some(project_a.clone()), window, cx)
3789 });
3790 let other_editor_a = cx_a.new_window_entity(|window, cx| {
3791 Editor::for_buffer(other_buffer_a, Some(project_a), window, cx)
3792 });
3793 let mut main_editor_cx_a = EditorTestContext {
3794 cx: cx_a.clone(),
3795 window: cx_a.window_handle(),
3796 editor: main_editor_a,
3797 assertion_cx: AssertionContextManager::new(),
3798 };
3799 let mut other_editor_cx_a = EditorTestContext {
3800 cx: cx_a.clone(),
3801 window: cx_a.window_handle(),
3802 editor: other_editor_a,
3803 assertion_cx: AssertionContextManager::new(),
3804 };
3805
3806 // Join the project as client B.
3807 let project_b = client_b.join_remote_project(project_id, cx_b).await;
3808 let main_buffer_b = project_b
3809 .update(cx_b, |p, cx| {
3810 p.open_buffer((worktree_id, rel_path("src/main.rs")), cx)
3811 })
3812 .await
3813 .unwrap();
3814 let other_buffer_b = project_b
3815 .update(cx_b, |p, cx| {
3816 p.open_buffer((worktree_id, rel_path("src/other_mod/other.rs")), cx)
3817 })
3818 .await
3819 .unwrap();
3820 let cx_b = cx_b.add_empty_window();
3821 let main_editor_b = cx_b.new_window_entity(|window, cx| {
3822 Editor::for_buffer(main_buffer_b, Some(project_b.clone()), window, cx)
3823 });
3824 let other_editor_b = cx_b.new_window_entity(|window, cx| {
3825 Editor::for_buffer(other_buffer_b, Some(project_b.clone()), window, cx)
3826 });
3827 let mut main_editor_cx_b = EditorTestContext {
3828 cx: cx_b.clone(),
3829 window: cx_b.window_handle(),
3830 editor: main_editor_b,
3831 assertion_cx: AssertionContextManager::new(),
3832 };
3833 let mut other_editor_cx_b = EditorTestContext {
3834 cx: cx_b.clone(),
3835 window: cx_b.window_handle(),
3836 editor: other_editor_b,
3837 assertion_cx: AssertionContextManager::new(),
3838 };
3839
3840 let initial_main = indoc! {"
3841ˇmod other;
3842fn main() { let foo = other::foo(); }"};
3843 let initial_other = indoc! {"
3844ˇpub fn foo() -> usize {
3845 4
3846}"};
3847
3848 let first_tabbed_main = indoc! {"
3849 ˇmod other;
3850fn main() { let foo = other::foo(); }"};
3851 tab_undo_assert(
3852 &mut main_editor_cx_a,
3853 &mut main_editor_cx_b,
3854 initial_main,
3855 first_tabbed_main,
3856 true,
3857 );
3858 tab_undo_assert(
3859 &mut main_editor_cx_a,
3860 &mut main_editor_cx_b,
3861 initial_main,
3862 first_tabbed_main,
3863 false,
3864 );
3865
3866 let first_tabbed_other = indoc! {"
3867 ˇpub fn foo() -> usize {
3868 4
3869}"};
3870 tab_undo_assert(
3871 &mut other_editor_cx_a,
3872 &mut other_editor_cx_b,
3873 initial_other,
3874 first_tabbed_other,
3875 true,
3876 );
3877 tab_undo_assert(
3878 &mut other_editor_cx_a,
3879 &mut other_editor_cx_b,
3880 initial_other,
3881 first_tabbed_other,
3882 false,
3883 );
3884
3885 client_a
3886 .fs()
3887 .atomic_write(
3888 PathBuf::from(path!("/a/src/.editorconfig")),
3889 "[*]\ntab_width = 3\n".to_owned(),
3890 )
3891 .await
3892 .unwrap();
3893 cx_a.run_until_parked();
3894 cx_b.run_until_parked();
3895
3896 let second_tabbed_main = indoc! {"
3897 ˇmod other;
3898fn main() { let foo = other::foo(); }"};
3899 tab_undo_assert(
3900 &mut main_editor_cx_a,
3901 &mut main_editor_cx_b,
3902 initial_main,
3903 second_tabbed_main,
3904 true,
3905 );
3906 tab_undo_assert(
3907 &mut main_editor_cx_a,
3908 &mut main_editor_cx_b,
3909 initial_main,
3910 second_tabbed_main,
3911 false,
3912 );
3913
3914 let second_tabbed_other = indoc! {"
3915 ˇpub fn foo() -> usize {
3916 4
3917}"};
3918 tab_undo_assert(
3919 &mut other_editor_cx_a,
3920 &mut other_editor_cx_b,
3921 initial_other,
3922 second_tabbed_other,
3923 true,
3924 );
3925 tab_undo_assert(
3926 &mut other_editor_cx_a,
3927 &mut other_editor_cx_b,
3928 initial_other,
3929 second_tabbed_other,
3930 false,
3931 );
3932
3933 let editorconfig_buffer_b = project_b
3934 .update(cx_b, |p, cx| {
3935 p.open_buffer((worktree_id, rel_path("src/other_mod/.editorconfig")), cx)
3936 })
3937 .await
3938 .unwrap();
3939 editorconfig_buffer_b.update(cx_b, |buffer, cx| {
3940 buffer.set_text("[*.rs]\ntab_width = 6\n", cx);
3941 });
3942 project_b
3943 .update(cx_b, |project, cx| {
3944 project.save_buffer(editorconfig_buffer_b.clone(), cx)
3945 })
3946 .await
3947 .unwrap();
3948 cx_a.run_until_parked();
3949 cx_b.run_until_parked();
3950
3951 tab_undo_assert(
3952 &mut main_editor_cx_a,
3953 &mut main_editor_cx_b,
3954 initial_main,
3955 second_tabbed_main,
3956 true,
3957 );
3958 tab_undo_assert(
3959 &mut main_editor_cx_a,
3960 &mut main_editor_cx_b,
3961 initial_main,
3962 second_tabbed_main,
3963 false,
3964 );
3965
3966 let third_tabbed_other = indoc! {"
3967 ˇpub fn foo() -> usize {
3968 4
3969}"};
3970 tab_undo_assert(
3971 &mut other_editor_cx_a,
3972 &mut other_editor_cx_b,
3973 initial_other,
3974 third_tabbed_other,
3975 true,
3976 );
3977
3978 tab_undo_assert(
3979 &mut other_editor_cx_a,
3980 &mut other_editor_cx_b,
3981 initial_other,
3982 third_tabbed_other,
3983 false,
3984 );
3985}
3986
3987#[gpui::test(iterations = 10)]
3988async fn test_collaborating_with_external_editorconfig(
3989 cx_a: &mut TestAppContext,
3990 cx_b: &mut TestAppContext,
3991) {
3992 let mut server = TestServer::start(cx_a.executor()).await;
3993 let client_a = server.create_client(cx_a, "user_a").await;
3994 let client_b = server.create_client(cx_b, "user_b").await;
3995 server
3996 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
3997 .await;
3998 let active_call_a = cx_a.read(ActiveCall::global);
3999
4000 client_a.language_registry().add(rust_lang());
4001 client_b.language_registry().add(rust_lang());
4002
4003 // Set up external .editorconfig in parent directory
4004 client_a
4005 .fs()
4006 .insert_tree(
4007 path!("/parent"),
4008 json!({
4009 ".editorconfig": "[*]\nindent_size = 5\n",
4010 "worktree": {
4011 ".editorconfig": "[*]\n",
4012 "src": {
4013 "main.rs": "fn main() {}",
4014 },
4015 },
4016 }),
4017 )
4018 .await;
4019
4020 let (project_a, worktree_id) = client_a
4021 .build_local_project(path!("/parent/worktree"), cx_a)
4022 .await;
4023 let project_id = active_call_a
4024 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4025 .await
4026 .unwrap();
4027
4028 // Open buffer on client A
4029 let buffer_a = project_a
4030 .update(cx_a, |p, cx| {
4031 p.open_buffer((worktree_id, rel_path("src/main.rs")), cx)
4032 })
4033 .await
4034 .unwrap();
4035
4036 cx_a.run_until_parked();
4037
4038 // Verify client A sees external editorconfig settings
4039 cx_a.read(|cx| {
4040 let file = buffer_a.read(cx).file();
4041 let settings = language_settings(Some("Rust".into()), file, cx);
4042 assert_eq!(Some(settings.tab_size), NonZeroU32::new(5));
4043 });
4044
4045 // Client B joins the project
4046 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4047 let buffer_b = project_b
4048 .update(cx_b, |p, cx| {
4049 p.open_buffer((worktree_id, rel_path("src/main.rs")), cx)
4050 })
4051 .await
4052 .unwrap();
4053
4054 cx_b.run_until_parked();
4055
4056 // Verify client B also sees external editorconfig settings
4057 cx_b.read(|cx| {
4058 let file = buffer_b.read(cx).file();
4059 let settings = language_settings(Some("Rust".into()), file, cx);
4060 assert_eq!(Some(settings.tab_size), NonZeroU32::new(5));
4061 });
4062
4063 // Client A modifies the external .editorconfig
4064 client_a
4065 .fs()
4066 .atomic_write(
4067 PathBuf::from(path!("/parent/.editorconfig")),
4068 "[*]\nindent_size = 9\n".to_owned(),
4069 )
4070 .await
4071 .unwrap();
4072
4073 cx_a.run_until_parked();
4074 cx_b.run_until_parked();
4075
4076 // Verify client A sees updated settings
4077 cx_a.read(|cx| {
4078 let file = buffer_a.read(cx).file();
4079 let settings = language_settings(Some("Rust".into()), file, cx);
4080 assert_eq!(Some(settings.tab_size), NonZeroU32::new(9));
4081 });
4082
4083 // Verify client B also sees updated settings
4084 cx_b.read(|cx| {
4085 let file = buffer_b.read(cx).file();
4086 let settings = language_settings(Some("Rust".into()), file, cx);
4087 assert_eq!(Some(settings.tab_size), NonZeroU32::new(9));
4088 });
4089}
4090
4091#[gpui::test]
4092async fn test_add_breakpoints(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
4093 let executor = cx_a.executor();
4094 let mut server = TestServer::start(executor.clone()).await;
4095 let client_a = server.create_client(cx_a, "user_a").await;
4096 let client_b = server.create_client(cx_b, "user_b").await;
4097 server
4098 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4099 .await;
4100 let active_call_a = cx_a.read(ActiveCall::global);
4101 let active_call_b = cx_b.read(ActiveCall::global);
4102 cx_a.update(editor::init);
4103 cx_b.update(editor::init);
4104 client_a
4105 .fs()
4106 .insert_tree(
4107 "/a",
4108 json!({
4109 "test.txt": "one\ntwo\nthree\nfour\nfive",
4110 }),
4111 )
4112 .await;
4113 let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
4114 let project_path = ProjectPath {
4115 worktree_id,
4116 path: rel_path(&"test.txt").into(),
4117 };
4118 let abs_path = project_a.read_with(cx_a, |project, cx| {
4119 project
4120 .absolute_path(&project_path, cx)
4121 .map(Arc::from)
4122 .unwrap()
4123 });
4124
4125 active_call_a
4126 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
4127 .await
4128 .unwrap();
4129 let project_id = active_call_a
4130 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4131 .await
4132 .unwrap();
4133 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4134 active_call_b
4135 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
4136 .await
4137 .unwrap();
4138 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
4139 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
4140
4141 // Client A opens an editor.
4142 let editor_a = workspace_a
4143 .update_in(cx_a, |workspace, window, cx| {
4144 workspace.open_path(project_path.clone(), None, true, window, cx)
4145 })
4146 .await
4147 .unwrap()
4148 .downcast::<Editor>()
4149 .unwrap();
4150
4151 // Client B opens same editor as A.
4152 let editor_b = workspace_b
4153 .update_in(cx_b, |workspace, window, cx| {
4154 workspace.open_path(project_path.clone(), None, true, window, cx)
4155 })
4156 .await
4157 .unwrap()
4158 .downcast::<Editor>()
4159 .unwrap();
4160
4161 cx_a.run_until_parked();
4162 cx_b.run_until_parked();
4163
4164 // Client A adds breakpoint on line (1)
4165 editor_a.update_in(cx_a, |editor, window, cx| {
4166 editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
4167 });
4168
4169 cx_a.run_until_parked();
4170 cx_b.run_until_parked();
4171
4172 let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
4173 editor
4174 .breakpoint_store()
4175 .unwrap()
4176 .read(cx)
4177 .all_source_breakpoints(cx)
4178 });
4179 let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
4180 editor
4181 .breakpoint_store()
4182 .unwrap()
4183 .read(cx)
4184 .all_source_breakpoints(cx)
4185 });
4186
4187 assert_eq!(1, breakpoints_a.len());
4188 assert_eq!(1, breakpoints_a.get(&abs_path).unwrap().len());
4189 assert_eq!(breakpoints_a, breakpoints_b);
4190
4191 // Client B adds breakpoint on line(2)
4192 editor_b.update_in(cx_b, |editor, window, cx| {
4193 editor.move_down(&zed_actions::editor::MoveDown, window, cx);
4194 editor.move_down(&zed_actions::editor::MoveDown, window, cx);
4195 editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
4196 });
4197
4198 cx_a.run_until_parked();
4199 cx_b.run_until_parked();
4200
4201 let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
4202 editor
4203 .breakpoint_store()
4204 .unwrap()
4205 .read(cx)
4206 .all_source_breakpoints(cx)
4207 });
4208 let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
4209 editor
4210 .breakpoint_store()
4211 .unwrap()
4212 .read(cx)
4213 .all_source_breakpoints(cx)
4214 });
4215
4216 assert_eq!(1, breakpoints_a.len());
4217 assert_eq!(breakpoints_a, breakpoints_b);
4218 assert_eq!(2, breakpoints_a.get(&abs_path).unwrap().len());
4219
4220 // Client A removes last added breakpoint from client B
4221 editor_a.update_in(cx_a, |editor, window, cx| {
4222 editor.move_down(&zed_actions::editor::MoveDown, window, cx);
4223 editor.move_down(&zed_actions::editor::MoveDown, window, cx);
4224 editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
4225 });
4226
4227 cx_a.run_until_parked();
4228 cx_b.run_until_parked();
4229
4230 let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
4231 editor
4232 .breakpoint_store()
4233 .unwrap()
4234 .read(cx)
4235 .all_source_breakpoints(cx)
4236 });
4237 let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
4238 editor
4239 .breakpoint_store()
4240 .unwrap()
4241 .read(cx)
4242 .all_source_breakpoints(cx)
4243 });
4244
4245 assert_eq!(1, breakpoints_a.len());
4246 assert_eq!(breakpoints_a, breakpoints_b);
4247 assert_eq!(1, breakpoints_a.get(&abs_path).unwrap().len());
4248
4249 // Client B removes first added breakpoint by client A
4250 editor_b.update_in(cx_b, |editor, window, cx| {
4251 editor.move_up(&zed_actions::editor::MoveUp, window, cx);
4252 editor.move_up(&zed_actions::editor::MoveUp, window, cx);
4253 editor.toggle_breakpoint(&editor::actions::ToggleBreakpoint, window, cx);
4254 });
4255
4256 cx_a.run_until_parked();
4257 cx_b.run_until_parked();
4258
4259 let breakpoints_a = editor_a.update(cx_a, |editor, cx| {
4260 editor
4261 .breakpoint_store()
4262 .unwrap()
4263 .read(cx)
4264 .all_source_breakpoints(cx)
4265 });
4266 let breakpoints_b = editor_b.update(cx_b, |editor, cx| {
4267 editor
4268 .breakpoint_store()
4269 .unwrap()
4270 .read(cx)
4271 .all_source_breakpoints(cx)
4272 });
4273
4274 assert_eq!(0, breakpoints_a.len());
4275 assert_eq!(breakpoints_a, breakpoints_b);
4276}
4277
4278#[gpui::test]
4279async fn test_client_can_query_lsp_ext(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
4280 let mut server = TestServer::start(cx_a.executor()).await;
4281 let client_a = server.create_client(cx_a, "user_a").await;
4282 let client_b = server.create_client(cx_b, "user_b").await;
4283 server
4284 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4285 .await;
4286 let active_call_a = cx_a.read(ActiveCall::global);
4287 let active_call_b = cx_b.read(ActiveCall::global);
4288
4289 cx_a.update(editor::init);
4290 cx_b.update(editor::init);
4291
4292 client_a.language_registry().add(rust_lang());
4293 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
4294 "Rust",
4295 FakeLspAdapter {
4296 name: "rust-analyzer",
4297 ..FakeLspAdapter::default()
4298 },
4299 );
4300 client_b.language_registry().add(rust_lang());
4301 client_b.language_registry().register_fake_lsp_adapter(
4302 "Rust",
4303 FakeLspAdapter {
4304 name: "rust-analyzer",
4305 ..FakeLspAdapter::default()
4306 },
4307 );
4308
4309 client_a
4310 .fs()
4311 .insert_tree(
4312 path!("/a"),
4313 json!({
4314 "main.rs": "fn main() {}",
4315 }),
4316 )
4317 .await;
4318 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
4319 active_call_a
4320 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
4321 .await
4322 .unwrap();
4323 let project_id = active_call_a
4324 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4325 .await
4326 .unwrap();
4327
4328 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4329 active_call_b
4330 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
4331 .await
4332 .unwrap();
4333
4334 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
4335 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
4336
4337 let editor_a = workspace_a
4338 .update_in(cx_a, |workspace, window, cx| {
4339 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
4340 })
4341 .await
4342 .unwrap()
4343 .downcast::<Editor>()
4344 .unwrap();
4345
4346 let editor_b = workspace_b
4347 .update_in(cx_b, |workspace, window, cx| {
4348 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
4349 })
4350 .await
4351 .unwrap()
4352 .downcast::<Editor>()
4353 .unwrap();
4354
4355 let fake_language_server = fake_language_servers.next().await.unwrap();
4356
4357 // host
4358 let mut expand_request_a = fake_language_server.set_request_handler::<LspExtExpandMacro, _, _>(
4359 |params, _| async move {
4360 assert_eq!(
4361 params.text_document.uri,
4362 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
4363 );
4364 assert_eq!(params.position, lsp::Position::new(0, 0));
4365 Ok(Some(ExpandedMacro {
4366 name: "test_macro_name".to_string(),
4367 expansion: "test_macro_expansion on the host".to_string(),
4368 }))
4369 },
4370 );
4371
4372 editor_a.update_in(cx_a, |editor, window, cx| {
4373 expand_macro_recursively(editor, &ExpandMacroRecursively, window, cx)
4374 });
4375 expand_request_a.next().await.unwrap();
4376 cx_a.run_until_parked();
4377
4378 workspace_a.update(cx_a, |workspace, cx| {
4379 workspace.active_pane().update(cx, |pane, cx| {
4380 assert_eq!(
4381 pane.items_len(),
4382 2,
4383 "Should have added a macro expansion to the host's pane"
4384 );
4385 let new_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
4386 new_editor.update(cx, |editor, cx| {
4387 assert_eq!(editor.text(cx), "test_macro_expansion on the host");
4388 });
4389 })
4390 });
4391
4392 // client
4393 let mut expand_request_b = fake_language_server.set_request_handler::<LspExtExpandMacro, _, _>(
4394 |params, _| async move {
4395 assert_eq!(
4396 params.text_document.uri,
4397 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
4398 );
4399 assert_eq!(
4400 params.position,
4401 lsp::Position::new(0, 12),
4402 "editor_b has selected the entire text and should query for a different position"
4403 );
4404 Ok(Some(ExpandedMacro {
4405 name: "test_macro_name".to_string(),
4406 expansion: "test_macro_expansion on the client".to_string(),
4407 }))
4408 },
4409 );
4410
4411 editor_b.update_in(cx_b, |editor, window, cx| {
4412 editor.select_all(&SelectAll, window, cx);
4413 expand_macro_recursively(editor, &ExpandMacroRecursively, window, cx)
4414 });
4415 expand_request_b.next().await.unwrap();
4416 cx_b.run_until_parked();
4417
4418 workspace_b.update(cx_b, |workspace, cx| {
4419 workspace.active_pane().update(cx, |pane, cx| {
4420 assert_eq!(
4421 pane.items_len(),
4422 2,
4423 "Should have added a macro expansion to the client's pane"
4424 );
4425 let new_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
4426 new_editor.update(cx, |editor, cx| {
4427 assert_eq!(editor.text(cx), "test_macro_expansion on the client");
4428 });
4429 })
4430 });
4431}
4432
4433#[gpui::test]
4434async fn test_copy_file_name_without_extension(
4435 cx_a: &mut TestAppContext,
4436 cx_b: &mut TestAppContext,
4437) {
4438 let mut server = TestServer::start(cx_a.executor()).await;
4439 let client_a = server.create_client(cx_a, "user_a").await;
4440 let client_b = server.create_client(cx_b, "user_b").await;
4441 server
4442 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4443 .await;
4444
4445 cx_b.update(editor::init);
4446
4447 client_a
4448 .fs()
4449 .insert_tree(
4450 path!("/root"),
4451 json!({
4452 "src": {
4453 "main.rs": indoc! {"
4454 fn main() {
4455 println!(\"Hello, world!\");
4456 }
4457 "},
4458 }
4459 }),
4460 )
4461 .await;
4462
4463 let (project_a, worktree_id) = client_a.build_local_project(path!("/root"), cx_a).await;
4464 let active_call_a = cx_a.read(ActiveCall::global);
4465 let project_id = active_call_a
4466 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4467 .await
4468 .unwrap();
4469
4470 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4471
4472 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
4473 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
4474
4475 let editor_a = workspace_a
4476 .update_in(cx_a, |workspace, window, cx| {
4477 workspace.open_path(
4478 (worktree_id, rel_path("src/main.rs")),
4479 None,
4480 true,
4481 window,
4482 cx,
4483 )
4484 })
4485 .await
4486 .unwrap()
4487 .downcast::<Editor>()
4488 .unwrap();
4489
4490 let editor_b = workspace_b
4491 .update_in(cx_b, |workspace, window, cx| {
4492 workspace.open_path(
4493 (worktree_id, rel_path("src/main.rs")),
4494 None,
4495 true,
4496 window,
4497 cx,
4498 )
4499 })
4500 .await
4501 .unwrap()
4502 .downcast::<Editor>()
4503 .unwrap();
4504
4505 cx_a.run_until_parked();
4506 cx_b.run_until_parked();
4507
4508 editor_a.update_in(cx_a, |editor, window, cx| {
4509 editor.copy_file_name_without_extension(&CopyFileNameWithoutExtension, window, cx);
4510 });
4511
4512 assert_eq!(
4513 cx_a.read_from_clipboard().and_then(|item| item.text()),
4514 Some("main".to_string())
4515 );
4516
4517 editor_b.update_in(cx_b, |editor, window, cx| {
4518 editor.copy_file_name_without_extension(&CopyFileNameWithoutExtension, window, cx);
4519 });
4520
4521 assert_eq!(
4522 cx_b.read_from_clipboard().and_then(|item| item.text()),
4523 Some("main".to_string())
4524 );
4525}
4526
4527#[gpui::test]
4528async fn test_copy_file_name(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
4529 let mut server = TestServer::start(cx_a.executor()).await;
4530 let client_a = server.create_client(cx_a, "user_a").await;
4531 let client_b = server.create_client(cx_b, "user_b").await;
4532 server
4533 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4534 .await;
4535
4536 cx_b.update(editor::init);
4537
4538 client_a
4539 .fs()
4540 .insert_tree(
4541 path!("/root"),
4542 json!({
4543 "src": {
4544 "main.rs": indoc! {"
4545 fn main() {
4546 println!(\"Hello, world!\");
4547 }
4548 "},
4549 }
4550 }),
4551 )
4552 .await;
4553
4554 let (project_a, worktree_id) = client_a.build_local_project(path!("/root"), cx_a).await;
4555 let active_call_a = cx_a.read(ActiveCall::global);
4556 let project_id = active_call_a
4557 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4558 .await
4559 .unwrap();
4560
4561 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4562
4563 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
4564 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
4565
4566 let editor_a = workspace_a
4567 .update_in(cx_a, |workspace, window, cx| {
4568 workspace.open_path(
4569 (worktree_id, rel_path("src/main.rs")),
4570 None,
4571 true,
4572 window,
4573 cx,
4574 )
4575 })
4576 .await
4577 .unwrap()
4578 .downcast::<Editor>()
4579 .unwrap();
4580
4581 let editor_b = workspace_b
4582 .update_in(cx_b, |workspace, window, cx| {
4583 workspace.open_path(
4584 (worktree_id, rel_path("src/main.rs")),
4585 None,
4586 true,
4587 window,
4588 cx,
4589 )
4590 })
4591 .await
4592 .unwrap()
4593 .downcast::<Editor>()
4594 .unwrap();
4595
4596 cx_a.run_until_parked();
4597 cx_b.run_until_parked();
4598
4599 editor_a.update_in(cx_a, |editor, window, cx| {
4600 editor.copy_file_name(&CopyFileName, window, cx);
4601 });
4602
4603 assert_eq!(
4604 cx_a.read_from_clipboard().and_then(|item| item.text()),
4605 Some("main.rs".to_string())
4606 );
4607
4608 editor_b.update_in(cx_b, |editor, window, cx| {
4609 editor.copy_file_name(&CopyFileName, window, cx);
4610 });
4611
4612 assert_eq!(
4613 cx_b.read_from_clipboard().and_then(|item| item.text()),
4614 Some("main.rs".to_string())
4615 );
4616}
4617
4618#[gpui::test]
4619async fn test_copy_file_location(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
4620 let mut server = TestServer::start(cx_a.executor()).await;
4621 let client_a = server.create_client(cx_a, "user_a").await;
4622 let client_b = server.create_client(cx_b, "user_b").await;
4623 server
4624 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4625 .await;
4626
4627 cx_b.update(editor::init);
4628
4629 client_a
4630 .fs()
4631 .insert_tree(
4632 path!("/root"),
4633 json!({
4634 "src": {
4635 "main.rs": indoc! {"
4636 fn main() {
4637 println!(\"Hello, world!\");
4638 }
4639 "},
4640 }
4641 }),
4642 )
4643 .await;
4644
4645 let (project_a, worktree_id) = client_a.build_local_project(path!("/root"), cx_a).await;
4646 let active_call_a = cx_a.read(ActiveCall::global);
4647 let project_id = active_call_a
4648 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4649 .await
4650 .unwrap();
4651
4652 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4653
4654 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
4655 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
4656
4657 let editor_a = workspace_a
4658 .update_in(cx_a, |workspace, window, cx| {
4659 workspace.open_path(
4660 (worktree_id, rel_path("src/main.rs")),
4661 None,
4662 true,
4663 window,
4664 cx,
4665 )
4666 })
4667 .await
4668 .unwrap()
4669 .downcast::<Editor>()
4670 .unwrap();
4671
4672 let editor_b = workspace_b
4673 .update_in(cx_b, |workspace, window, cx| {
4674 workspace.open_path(
4675 (worktree_id, rel_path("src/main.rs")),
4676 None,
4677 true,
4678 window,
4679 cx,
4680 )
4681 })
4682 .await
4683 .unwrap()
4684 .downcast::<Editor>()
4685 .unwrap();
4686
4687 cx_a.run_until_parked();
4688 cx_b.run_until_parked();
4689
4690 editor_a.update_in(cx_a, |editor, window, cx| {
4691 editor.change_selections(Default::default(), window, cx, |s| {
4692 s.select_ranges([MultiBufferOffset(16)..MultiBufferOffset(16)]);
4693 });
4694 editor.copy_file_location(&CopyFileLocation, window, cx);
4695 });
4696
4697 assert_eq!(
4698 cx_a.read_from_clipboard().and_then(|item| item.text()),
4699 Some(format!("{}:2", path!("src/main.rs")))
4700 );
4701
4702 editor_b.update_in(cx_b, |editor, window, cx| {
4703 editor.change_selections(Default::default(), window, cx, |s| {
4704 s.select_ranges([MultiBufferOffset(16)..MultiBufferOffset(16)]);
4705 });
4706 editor.copy_file_location(&CopyFileLocation, window, cx);
4707 });
4708
4709 assert_eq!(
4710 cx_b.read_from_clipboard().and_then(|item| item.text()),
4711 Some(format!("{}:2", path!("src/main.rs")))
4712 );
4713}
4714
4715#[track_caller]
4716fn tab_undo_assert(
4717 cx_a: &mut EditorTestContext,
4718 cx_b: &mut EditorTestContext,
4719 expected_initial: &str,
4720 expected_tabbed: &str,
4721 a_tabs: bool,
4722) {
4723 cx_a.assert_editor_state(expected_initial);
4724 cx_b.assert_editor_state(expected_initial);
4725
4726 if a_tabs {
4727 cx_a.update_editor(|editor, window, cx| {
4728 editor.tab(&editor::actions::Tab, window, cx);
4729 });
4730 } else {
4731 cx_b.update_editor(|editor, window, cx| {
4732 editor.tab(&editor::actions::Tab, window, cx);
4733 });
4734 }
4735
4736 cx_a.run_until_parked();
4737 cx_b.run_until_parked();
4738
4739 cx_a.assert_editor_state(expected_tabbed);
4740 cx_b.assert_editor_state(expected_tabbed);
4741
4742 if a_tabs {
4743 cx_a.update_editor(|editor, window, cx| {
4744 editor.undo(&editor::actions::Undo, window, cx);
4745 });
4746 } else {
4747 cx_b.update_editor(|editor, window, cx| {
4748 editor.undo(&editor::actions::Undo, window, cx);
4749 });
4750 }
4751 cx_a.run_until_parked();
4752 cx_b.run_until_parked();
4753 cx_a.assert_editor_state(expected_initial);
4754 cx_b.assert_editor_state(expected_initial);
4755}
4756
4757fn extract_hint_labels(editor: &Editor, cx: &mut App) -> Vec<String> {
4758 let lsp_store = editor.project().unwrap().read(cx).lsp_store();
4759
4760 let mut all_cached_labels = Vec::new();
4761 let mut all_fetched_hints = Vec::new();
4762 for buffer in editor.buffer().read(cx).all_buffers() {
4763 lsp_store.update(cx, |lsp_store, cx| {
4764 let hints = &lsp_store.latest_lsp_data(&buffer, cx).inlay_hints();
4765 all_cached_labels.extend(hints.all_cached_hints().into_iter().map(|hint| {
4766 let mut label = hint.text().to_string();
4767 if hint.padding_left {
4768 label.insert(0, ' ');
4769 }
4770 if hint.padding_right {
4771 label.push_str(" ");
4772 }
4773 label
4774 }));
4775 all_fetched_hints.extend(hints.all_fetched_hints());
4776 });
4777 }
4778
4779 assert!(
4780 all_fetched_hints.is_empty(),
4781 "Did not expect background hints fetch tasks, but got {} of them",
4782 all_fetched_hints.len()
4783 );
4784
4785 all_cached_labels
4786}
4787
4788#[track_caller]
4789fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
4790 editor
4791 .all_inlays(cx)
4792 .into_iter()
4793 .filter_map(|inlay| inlay.get_color())
4794 .map(Rgba::from)
4795 .collect()
4796}
4797
4798fn extract_semantic_token_ranges(editor: &Editor, cx: &App) -> Vec<Range<MultiBufferOffset>> {
4799 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
4800 editor
4801 .display_map
4802 .read(cx)
4803 .semantic_token_highlights
4804 .iter()
4805 .flat_map(|(_, (v, _))| v.iter())
4806 .map(|highlights| highlights.range.to_offset(&multi_buffer_snapshot))
4807 .collect()
4808}
4809
4810#[gpui::test(iterations = 10)]
4811async fn test_mutual_editor_semantic_token_cache_update(
4812 cx_a: &mut TestAppContext,
4813 cx_b: &mut TestAppContext,
4814) {
4815 let mut server = TestServer::start(cx_a.executor()).await;
4816 let executor = cx_a.executor();
4817 let client_a = server.create_client(cx_a, "user_a").await;
4818 let client_b = server.create_client(cx_b, "user_b").await;
4819 server
4820 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
4821 .await;
4822 let active_call_a = cx_a.read(ActiveCall::global);
4823 let active_call_b = cx_b.read(ActiveCall::global);
4824
4825 cx_a.update(editor::init);
4826 cx_b.update(editor::init);
4827
4828 cx_a.update(|cx| {
4829 SettingsStore::update_global(cx, |store, cx| {
4830 store.update_user_settings(cx, |settings| {
4831 settings.project.all_languages.defaults.semantic_tokens =
4832 Some(SemanticTokens::Full);
4833 });
4834 });
4835 });
4836 cx_b.update(|cx| {
4837 SettingsStore::update_global(cx, |store, cx| {
4838 store.update_user_settings(cx, |settings| {
4839 settings.project.all_languages.defaults.semantic_tokens =
4840 Some(SemanticTokens::Full);
4841 });
4842 });
4843 });
4844
4845 let capabilities = lsp::ServerCapabilities {
4846 semantic_tokens_provider: Some(
4847 lsp::SemanticTokensServerCapabilities::SemanticTokensOptions(
4848 lsp::SemanticTokensOptions {
4849 legend: lsp::SemanticTokensLegend {
4850 token_types: vec!["function".into()],
4851 token_modifiers: vec![],
4852 },
4853 full: Some(lsp::SemanticTokensFullOptions::Delta { delta: None }),
4854 ..Default::default()
4855 },
4856 ),
4857 ),
4858 ..lsp::ServerCapabilities::default()
4859 };
4860 client_a.language_registry().add(rust_lang());
4861
4862 let edits_made = Arc::new(AtomicUsize::new(0));
4863 let closure_edits_made = Arc::clone(&edits_made);
4864 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
4865 "Rust",
4866 FakeLspAdapter {
4867 capabilities: capabilities.clone(),
4868 initializer: Some(Box::new(move |fake_language_server| {
4869 let closure_edits_made = closure_edits_made.clone();
4870 fake_language_server
4871 .set_request_handler::<lsp::request::SemanticTokensFullRequest, _, _>(
4872 move |_, _| {
4873 let edits_made_2 = Arc::clone(&closure_edits_made);
4874 async move {
4875 let edits_made =
4876 AtomicUsize::load(&edits_made_2, atomic::Ordering::Acquire);
4877 Ok(Some(lsp::SemanticTokensResult::Tokens(
4878 lsp::SemanticTokens {
4879 data: vec![
4880 0, // delta_line
4881 3, // delta_start
4882 edits_made as u32 + 4, // length
4883 0, // token_type
4884 0, // token_modifiers_bitset
4885 ],
4886 result_id: None,
4887 },
4888 )))
4889 }
4890 },
4891 );
4892 })),
4893 ..FakeLspAdapter::default()
4894 },
4895 );
4896 client_b.language_registry().add(rust_lang());
4897 client_b.language_registry().register_fake_lsp_adapter(
4898 "Rust",
4899 FakeLspAdapter {
4900 capabilities,
4901 ..FakeLspAdapter::default()
4902 },
4903 );
4904
4905 client_a
4906 .fs()
4907 .insert_tree(
4908 path!("/a"),
4909 json!({
4910 "main.rs": "fn main() { a }",
4911 "other.rs": "// Test file",
4912 }),
4913 )
4914 .await;
4915 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
4916 active_call_a
4917 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
4918 .await
4919 .unwrap();
4920 let project_id = active_call_a
4921 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
4922 .await
4923 .unwrap();
4924
4925 let project_b = client_b.join_remote_project(project_id, cx_b).await;
4926 active_call_b
4927 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
4928 .await
4929 .unwrap();
4930
4931 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
4932
4933 let file_a = workspace_a.update_in(cx_a, |workspace, window, cx| {
4934 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
4935 });
4936 let _fake_language_server = fake_language_servers.next().await.unwrap();
4937 let editor_a = file_a.await.unwrap().downcast::<Editor>().unwrap();
4938 executor.advance_clock(Duration::from_millis(100));
4939 executor.run_until_parked();
4940
4941 let initial_edit = edits_made.load(atomic::Ordering::Acquire);
4942 editor_a.update(cx_a, |editor, cx| {
4943 let ranges = extract_semantic_token_ranges(editor, cx);
4944 assert_eq!(
4945 ranges,
4946 vec![MultiBufferOffset(3)..MultiBufferOffset(3 + initial_edit + 4)],
4947 "Host should get its first semantic tokens when opening an editor"
4948 );
4949 });
4950
4951 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
4952 let editor_b = workspace_b
4953 .update_in(cx_b, |workspace, window, cx| {
4954 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
4955 })
4956 .await
4957 .unwrap()
4958 .downcast::<Editor>()
4959 .unwrap();
4960
4961 executor.advance_clock(Duration::from_millis(100));
4962 executor.run_until_parked();
4963 editor_b.update(cx_b, |editor, cx| {
4964 let ranges = extract_semantic_token_ranges(editor, cx);
4965 assert_eq!(
4966 ranges,
4967 vec![MultiBufferOffset(3)..MultiBufferOffset(3 + initial_edit + 4)],
4968 "Client should get its first semantic tokens when opening an editor"
4969 );
4970 });
4971
4972 let after_client_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
4973 editor_b.update_in(cx_b, |editor, window, cx| {
4974 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4975 s.select_ranges([MultiBufferOffset(13)..MultiBufferOffset(13)].clone())
4976 });
4977 editor.handle_input(":", window, cx);
4978 });
4979 cx_b.focus(&editor_b);
4980
4981 executor.advance_clock(Duration::from_secs(1));
4982 executor.run_until_parked();
4983 editor_a.update(cx_a, |editor, cx| {
4984 let ranges = extract_semantic_token_ranges(editor, cx);
4985 assert_eq!(
4986 ranges,
4987 vec![MultiBufferOffset(3)..MultiBufferOffset(3 + after_client_edit + 4)],
4988 );
4989 });
4990 editor_b.update(cx_b, |editor, cx| {
4991 let ranges = extract_semantic_token_ranges(editor, cx);
4992 assert_eq!(
4993 ranges,
4994 vec![MultiBufferOffset(3)..MultiBufferOffset(3 + after_client_edit + 4)],
4995 );
4996 });
4997
4998 let after_host_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
4999 editor_a.update_in(cx_a, |editor, window, cx| {
5000 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5001 s.select_ranges([MultiBufferOffset(14)..MultiBufferOffset(14)])
5002 });
5003 editor.handle_input("a change", window, cx);
5004 });
5005 cx_a.focus(&editor_a);
5006
5007 executor.advance_clock(Duration::from_secs(1));
5008 executor.run_until_parked();
5009 editor_a.update(cx_a, |editor, cx| {
5010 let ranges = extract_semantic_token_ranges(editor, cx);
5011 assert_eq!(
5012 ranges,
5013 vec![MultiBufferOffset(3)..MultiBufferOffset(3 + after_host_edit + 4)],
5014 );
5015 });
5016 editor_b.update(cx_b, |editor, cx| {
5017 let ranges = extract_semantic_token_ranges(editor, cx);
5018 assert_eq!(
5019 ranges,
5020 vec![MultiBufferOffset(3)..MultiBufferOffset(3 + after_host_edit + 4)],
5021 );
5022 });
5023}
5024
5025#[gpui::test(iterations = 10)]
5026async fn test_semantic_token_refresh_is_forwarded(
5027 cx_a: &mut TestAppContext,
5028 cx_b: &mut TestAppContext,
5029) {
5030 let mut server = TestServer::start(cx_a.executor()).await;
5031 let executor = cx_a.executor();
5032 let client_a = server.create_client(cx_a, "user_a").await;
5033 let client_b = server.create_client(cx_b, "user_b").await;
5034 server
5035 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5036 .await;
5037 let active_call_a = cx_a.read(ActiveCall::global);
5038 let active_call_b = cx_b.read(ActiveCall::global);
5039
5040 cx_a.update(editor::init);
5041 cx_b.update(editor::init);
5042
5043 cx_a.update(|cx| {
5044 SettingsStore::update_global(cx, |store, cx| {
5045 store.update_user_settings(cx, |settings| {
5046 settings.project.all_languages.defaults.semantic_tokens = Some(SemanticTokens::Off);
5047 });
5048 });
5049 });
5050 cx_b.update(|cx| {
5051 SettingsStore::update_global(cx, |store, cx| {
5052 store.update_user_settings(cx, |settings| {
5053 settings.project.all_languages.defaults.semantic_tokens =
5054 Some(SemanticTokens::Full);
5055 });
5056 });
5057 });
5058
5059 let capabilities = lsp::ServerCapabilities {
5060 semantic_tokens_provider: Some(
5061 lsp::SemanticTokensServerCapabilities::SemanticTokensOptions(
5062 lsp::SemanticTokensOptions {
5063 legend: lsp::SemanticTokensLegend {
5064 token_types: vec!["function".into()],
5065 token_modifiers: vec![],
5066 },
5067 full: Some(lsp::SemanticTokensFullOptions::Delta { delta: None }),
5068 ..Default::default()
5069 },
5070 ),
5071 ),
5072 ..lsp::ServerCapabilities::default()
5073 };
5074 client_a.language_registry().add(rust_lang());
5075 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
5076 "Rust",
5077 FakeLspAdapter {
5078 capabilities: capabilities.clone(),
5079 ..FakeLspAdapter::default()
5080 },
5081 );
5082 client_b.language_registry().add(rust_lang());
5083 client_b.language_registry().register_fake_lsp_adapter(
5084 "Rust",
5085 FakeLspAdapter {
5086 capabilities,
5087 ..FakeLspAdapter::default()
5088 },
5089 );
5090
5091 client_a
5092 .fs()
5093 .insert_tree(
5094 path!("/a"),
5095 json!({
5096 "main.rs": "fn main() { a }",
5097 "other.rs": "// Test file",
5098 }),
5099 )
5100 .await;
5101 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
5102 active_call_a
5103 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
5104 .await
5105 .unwrap();
5106 let project_id = active_call_a
5107 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5108 .await
5109 .unwrap();
5110
5111 let project_b = client_b.join_remote_project(project_id, cx_b).await;
5112 active_call_b
5113 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
5114 .await
5115 .unwrap();
5116
5117 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
5118 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
5119
5120 let editor_a = workspace_a
5121 .update_in(cx_a, |workspace, window, cx| {
5122 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
5123 })
5124 .await
5125 .unwrap()
5126 .downcast::<Editor>()
5127 .unwrap();
5128
5129 let editor_b = workspace_b
5130 .update_in(cx_b, |workspace, window, cx| {
5131 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
5132 })
5133 .await
5134 .unwrap()
5135 .downcast::<Editor>()
5136 .unwrap();
5137
5138 let other_tokens = Arc::new(AtomicBool::new(false));
5139 let fake_language_server = fake_language_servers.next().await.unwrap();
5140 let closure_other_tokens = Arc::clone(&other_tokens);
5141 fake_language_server
5142 .set_request_handler::<lsp::request::SemanticTokensFullRequest, _, _>(move |params, _| {
5143 let task_other_tokens = Arc::clone(&closure_other_tokens);
5144 async move {
5145 assert_eq!(
5146 params.text_document.uri,
5147 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
5148 );
5149 let other_tokens = task_other_tokens.load(atomic::Ordering::Acquire);
5150 let (delta_start, length) = if other_tokens { (0, 2) } else { (3, 4) };
5151 Ok(Some(lsp::SemanticTokensResult::Tokens(
5152 lsp::SemanticTokens {
5153 data: vec![
5154 0, // delta_line
5155 delta_start,
5156 length,
5157 0, // token_type
5158 0, // token_modifiers_bitset
5159 ],
5160 result_id: None,
5161 },
5162 )))
5163 }
5164 })
5165 .next()
5166 .await
5167 .unwrap();
5168
5169 executor.run_until_parked();
5170 editor_a.update(cx_a, |editor, cx| {
5171 assert!(
5172 extract_semantic_token_ranges(editor, cx).is_empty(),
5173 "Host should get no semantic tokens due to them turned off"
5174 );
5175 });
5176
5177 executor.run_until_parked();
5178 editor_b.update(cx_b, |editor, cx| {
5179 assert_eq!(
5180 vec![MultiBufferOffset(3)..MultiBufferOffset(7)],
5181 extract_semantic_token_ranges(editor, cx),
5182 "Client should get its first semantic tokens when opening an editor"
5183 );
5184 });
5185
5186 other_tokens.fetch_or(true, atomic::Ordering::Release);
5187 fake_language_server
5188 .request::<lsp::request::SemanticTokensRefresh>((), DEFAULT_LSP_REQUEST_TIMEOUT)
5189 .await
5190 .into_response()
5191 .expect("semantic tokens refresh request failed");
5192 // wait out the debounce timeout
5193 executor.advance_clock(LSP_REQUEST_DEBOUNCE_TIMEOUT);
5194 executor.run_until_parked();
5195 editor_a.update(cx_a, |editor, cx| {
5196 assert!(
5197 extract_semantic_token_ranges(editor, cx).is_empty(),
5198 "Host should get no semantic tokens due to them turned off, even after the /refresh"
5199 );
5200 });
5201
5202 executor.run_until_parked();
5203 editor_b.update(cx_b, |editor, cx| {
5204 assert_eq!(
5205 vec![MultiBufferOffset(0)..MultiBufferOffset(2)],
5206 extract_semantic_token_ranges(editor, cx),
5207 "Guest should get a /refresh LSP request propagated by host despite host tokens are off"
5208 );
5209 });
5210}
5211
5212#[gpui::test]
5213async fn test_document_folding_ranges(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
5214 let mut server = TestServer::start(cx_a.executor()).await;
5215 let executor = cx_a.executor();
5216 let client_a = server.create_client(cx_a, "user_a").await;
5217 let client_b = server.create_client(cx_b, "user_b").await;
5218 server
5219 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5220 .await;
5221 let active_call_a = cx_a.read(ActiveCall::global);
5222 let active_call_b = cx_b.read(ActiveCall::global);
5223
5224 cx_a.update(editor::init);
5225 cx_b.update(editor::init);
5226
5227 let capabilities = lsp::ServerCapabilities {
5228 folding_range_provider: Some(lsp::FoldingRangeProviderCapability::Simple(true)),
5229 ..lsp::ServerCapabilities::default()
5230 };
5231 client_a.language_registry().add(rust_lang());
5232 let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
5233 "Rust",
5234 FakeLspAdapter {
5235 capabilities: capabilities.clone(),
5236 ..FakeLspAdapter::default()
5237 },
5238 );
5239 client_b.language_registry().add(rust_lang());
5240 client_b.language_registry().register_fake_lsp_adapter(
5241 "Rust",
5242 FakeLspAdapter {
5243 capabilities,
5244 ..FakeLspAdapter::default()
5245 },
5246 );
5247
5248 client_a
5249 .fs()
5250 .insert_tree(
5251 path!("/a"),
5252 json!({
5253 "main.rs": "fn main() {\n if true {\n println!(\"hello\");\n }\n}\n",
5254 }),
5255 )
5256 .await;
5257 let (project_a, worktree_id) = client_a.build_local_project(path!("/a"), cx_a).await;
5258 active_call_a
5259 .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
5260 .await
5261 .unwrap();
5262 let project_id = active_call_a
5263 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5264 .await
5265 .unwrap();
5266
5267 let project_b = client_b.join_remote_project(project_id, cx_b).await;
5268 active_call_b
5269 .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
5270 .await
5271 .unwrap();
5272
5273 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
5274
5275 let _buffer_a = project_a
5276 .update(cx_a, |project, cx| {
5277 project.open_local_buffer(path!("/a/main.rs"), cx)
5278 })
5279 .await
5280 .unwrap();
5281 let editor_a = workspace_a
5282 .update_in(cx_a, |workspace, window, cx| {
5283 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
5284 })
5285 .await
5286 .unwrap()
5287 .downcast::<Editor>()
5288 .unwrap();
5289
5290 let fake_language_server = fake_language_servers.next().await.unwrap();
5291
5292 let folding_request_count = Arc::new(AtomicUsize::new(0));
5293 let closure_count = Arc::clone(&folding_request_count);
5294 let mut folding_request_handle = fake_language_server
5295 .set_request_handler::<lsp::request::FoldingRangeRequest, _, _>(move |_, _| {
5296 let count = Arc::clone(&closure_count);
5297 async move {
5298 count.fetch_add(1, atomic::Ordering::Release);
5299 Ok(Some(vec![lsp::FoldingRange {
5300 start_line: 0,
5301 start_character: Some(10),
5302 end_line: 4,
5303 end_character: Some(1),
5304 kind: None,
5305 collapsed_text: None,
5306 }]))
5307 }
5308 });
5309
5310 executor.run_until_parked();
5311
5312 assert_eq!(
5313 0,
5314 folding_request_count.load(atomic::Ordering::Acquire),
5315 "LSP folding ranges are off by default, no request should have been made"
5316 );
5317 editor_a.update(cx_a, |editor, cx| {
5318 assert!(
5319 !editor.document_folding_ranges_enabled(cx),
5320 "Host should not have LSP folding ranges enabled"
5321 );
5322 });
5323
5324 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
5325 let editor_b = workspace_b
5326 .update_in(cx_b, |workspace, window, cx| {
5327 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
5328 })
5329 .await
5330 .unwrap()
5331 .downcast::<Editor>()
5332 .unwrap();
5333 executor.run_until_parked();
5334
5335 editor_b.update(cx_b, |editor, cx| {
5336 assert!(
5337 !editor.document_folding_ranges_enabled(cx),
5338 "Client should not have LSP folding ranges enabled by default"
5339 );
5340 });
5341
5342 cx_b.update(|_, cx| {
5343 SettingsStore::update_global(cx, |store, cx| {
5344 store.update_user_settings(cx, |settings| {
5345 settings
5346 .project
5347 .all_languages
5348 .defaults
5349 .document_folding_ranges = Some(DocumentFoldingRanges::On);
5350 });
5351 });
5352 });
5353 executor.advance_clock(LSP_REQUEST_DEBOUNCE_TIMEOUT);
5354 folding_request_handle.next().await.unwrap();
5355 executor.run_until_parked();
5356
5357 assert!(
5358 folding_request_count.load(atomic::Ordering::Acquire) > 0,
5359 "After the client enables LSP folding ranges, a request should be made"
5360 );
5361 editor_b.update(cx_b, |editor, cx| {
5362 assert!(
5363 editor.document_folding_ranges_enabled(cx),
5364 "Client should have LSP folding ranges enabled after toggling the setting on"
5365 );
5366 });
5367 editor_a.update(cx_a, |editor, cx| {
5368 assert!(
5369 !editor.document_folding_ranges_enabled(cx),
5370 "Host should remain unaffected by the client's setting change"
5371 );
5372 });
5373
5374 editor_b.update_in(cx_b, |editor, window, cx| {
5375 let snapshot = editor.display_snapshot(cx);
5376 assert!(
5377 !snapshot.is_line_folded(MultiBufferRow(0)),
5378 "Line 0 should not be folded before fold_at"
5379 );
5380 editor.fold_at(MultiBufferRow(0), window, cx);
5381 });
5382 executor.run_until_parked();
5383
5384 editor_b.update(cx_b, |editor, cx| {
5385 let snapshot = editor.display_snapshot(cx);
5386 assert!(
5387 snapshot.is_line_folded(MultiBufferRow(0)),
5388 "Line 0 should be folded after fold_at using LSP folding range"
5389 );
5390 });
5391}
5392
5393#[gpui::test]
5394async fn test_remote_project_worktree_trust(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
5395 let has_restricted_worktrees = |project: &gpui::Entity<project::Project>,
5396 cx: &mut VisualTestContext| {
5397 cx.update(|_, cx| {
5398 let worktree_store = project.read(cx).worktree_store();
5399 TrustedWorktrees::try_get_global(cx)
5400 .unwrap()
5401 .read(cx)
5402 .has_restricted_worktrees(&worktree_store, cx)
5403 })
5404 };
5405
5406 cx_a.update(|cx| {
5407 project::trusted_worktrees::init(HashMap::default(), cx);
5408 });
5409 cx_b.update(|cx| {
5410 project::trusted_worktrees::init(HashMap::default(), cx);
5411 });
5412
5413 let mut server = TestServer::start(cx_a.executor()).await;
5414 let client_a = server.create_client(cx_a, "user_a").await;
5415 let client_b = server.create_client(cx_b, "user_b").await;
5416 server
5417 .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
5418 .await;
5419
5420 client_a
5421 .fs()
5422 .insert_tree(
5423 path!("/a"),
5424 json!({
5425 "file.txt": "contents",
5426 }),
5427 )
5428 .await;
5429
5430 let (project_a, worktree_id) = client_a
5431 .build_local_project_with_trust(path!("/a"), cx_a)
5432 .await;
5433 let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a);
5434 let active_call_a = cx_a.read(ActiveCall::global);
5435 let project_id = active_call_a
5436 .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
5437 .await
5438 .unwrap();
5439
5440 let project_b = client_b.join_remote_project(project_id, cx_b).await;
5441 let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b);
5442
5443 let _editor_a = workspace_a
5444 .update_in(cx_a, |workspace, window, cx| {
5445 workspace.open_path(
5446 (worktree_id, rel_path("src/main.rs")),
5447 None,
5448 true,
5449 window,
5450 cx,
5451 )
5452 })
5453 .await
5454 .unwrap()
5455 .downcast::<Editor>()
5456 .unwrap();
5457
5458 let _editor_b = workspace_b
5459 .update_in(cx_b, |workspace, window, cx| {
5460 workspace.open_path(
5461 (worktree_id, rel_path("src/main.rs")),
5462 None,
5463 true,
5464 window,
5465 cx,
5466 )
5467 })
5468 .await
5469 .unwrap()
5470 .downcast::<Editor>()
5471 .unwrap();
5472
5473 cx_a.run_until_parked();
5474 cx_b.run_until_parked();
5475
5476 assert!(
5477 has_restricted_worktrees(&project_a, cx_a),
5478 "local client should have restricted worktrees after opening it"
5479 );
5480 assert!(
5481 !has_restricted_worktrees(&project_b, cx_b),
5482 "remote client joined a project should have no restricted worktrees"
5483 );
5484
5485 cx_a.update(|_, cx| {
5486 if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
5487 trusted_worktrees.update(cx, |trusted_worktrees, cx| {
5488 trusted_worktrees.trust(
5489 &project_a.read(cx).worktree_store(),
5490 HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
5491 cx,
5492 );
5493 });
5494 }
5495 });
5496 assert!(
5497 !has_restricted_worktrees(&project_a, cx_a),
5498 "local client should have no worktrees after trusting those"
5499 );
5500 assert!(
5501 !has_restricted_worktrees(&project_b, cx_b),
5502 "remote client should still be trusted"
5503 );
5504}
5505
5506fn blame_entry(sha: &str, range: Range<u32>) -> git::blame::BlameEntry {
5507 git::blame::BlameEntry {
5508 sha: sha.parse().unwrap(),
5509 range,
5510 ..Default::default()
5511 }
5512}