1use crate::{Event, *};
2use fs::FakeFs;
3use futures::{future, StreamExt};
4use gpui::{AppContext, SemanticVersion, UpdateGlobal};
5use language::{
6 language_settings::{AllLanguageSettings, LanguageSettingsContent},
7 tree_sitter_rust, tree_sitter_typescript, Diagnostic, FakeLspAdapter, LanguageConfig,
8 LanguageMatcher, LineEnding, OffsetRangeExt, Point, ToPoint,
9};
10use parking_lot::Mutex;
11use pretty_assertions::assert_eq;
12use serde_json::json;
13#[cfg(not(windows))]
14use std::os;
15use std::task::Poll;
16use task::{ResolvedTask, TaskContext, TaskTemplate, TaskTemplates};
17use unindent::Unindent as _;
18use util::{assert_set_eq, paths::PathMatcher, test::temp_tree};
19
20#[gpui::test]
21async fn test_block_via_channel(cx: &mut gpui::TestAppContext) {
22 cx.executor().allow_parking();
23
24 let (tx, mut rx) = futures::channel::mpsc::unbounded();
25 let _thread = std::thread::spawn(move || {
26 std::fs::metadata("/tmp").unwrap();
27 std::thread::sleep(Duration::from_millis(1000));
28 tx.unbounded_send(1).unwrap();
29 });
30 rx.next().await.unwrap();
31}
32
33#[gpui::test]
34async fn test_block_via_smol(cx: &mut gpui::TestAppContext) {
35 cx.executor().allow_parking();
36
37 let io_task = smol::unblock(move || {
38 println!("sleeping on thread {:?}", std::thread::current().id());
39 std::thread::sleep(Duration::from_millis(10));
40 1
41 });
42
43 let task = cx.foreground_executor().spawn(async move {
44 io_task.await;
45 });
46
47 task.await;
48}
49
50#[cfg(not(windows))]
51#[gpui::test]
52async fn test_symlinks(cx: &mut gpui::TestAppContext) {
53 init_test(cx);
54 cx.executor().allow_parking();
55
56 let dir = temp_tree(json!({
57 "root": {
58 "apple": "",
59 "banana": {
60 "carrot": {
61 "date": "",
62 "endive": "",
63 }
64 },
65 "fennel": {
66 "grape": "",
67 }
68 }
69 }));
70
71 let root_link_path = dir.path().join("root_link");
72 os::unix::fs::symlink(&dir.path().join("root"), &root_link_path).unwrap();
73 os::unix::fs::symlink(
74 &dir.path().join("root/fennel"),
75 &dir.path().join("root/finnochio"),
76 )
77 .unwrap();
78
79 let project = Project::test(Arc::new(RealFs::default()), [root_link_path.as_ref()], cx).await;
80
81 project.update(cx, |project, cx| {
82 let tree = project.worktrees().next().unwrap().read(cx);
83 assert_eq!(tree.file_count(), 5);
84 assert_eq!(
85 tree.inode_for_path("fennel/grape"),
86 tree.inode_for_path("finnochio/grape")
87 );
88 });
89}
90
91#[gpui::test]
92async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext) {
93 init_test(cx);
94
95 let fs = FakeFs::new(cx.executor());
96 fs.insert_tree(
97 "/the-root",
98 json!({
99 ".zed": {
100 "settings.json": r#"{ "tab_size": 8 }"#,
101 "tasks.json": r#"[{
102 "label": "cargo check",
103 "command": "cargo",
104 "args": ["check", "--all"]
105 },]"#,
106 },
107 "a": {
108 "a.rs": "fn a() {\n A\n}"
109 },
110 "b": {
111 ".zed": {
112 "settings.json": r#"{ "tab_size": 2 }"#,
113 "tasks.json": r#"[{
114 "label": "cargo check",
115 "command": "cargo",
116 "args": ["check"]
117 },]"#,
118 },
119 "b.rs": "fn b() {\n B\n}"
120 }
121 }),
122 )
123 .await;
124
125 let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await;
126 let worktree = project.update(cx, |project, _| project.worktrees().next().unwrap());
127 let task_context = TaskContext::default();
128
129 cx.executor().run_until_parked();
130 let worktree_id = cx.update(|cx| {
131 project.update(cx, |project, cx| {
132 project.worktrees().next().unwrap().read(cx).id()
133 })
134 });
135 let global_task_source_kind = TaskSourceKind::Worktree {
136 id: worktree_id,
137 abs_path: PathBuf::from("/the-root/.zed/tasks.json"),
138 id_base: "local_tasks_for_worktree".into(),
139 };
140
141 let all_tasks = cx
142 .update(|cx| {
143 let tree = worktree.read(cx);
144
145 let settings_a = language_settings(
146 None,
147 Some(
148 &(File::for_entry(
149 tree.entry_for_path("a/a.rs").unwrap().clone(),
150 worktree.clone(),
151 ) as _),
152 ),
153 cx,
154 );
155 let settings_b = language_settings(
156 None,
157 Some(
158 &(File::for_entry(
159 tree.entry_for_path("b/b.rs").unwrap().clone(),
160 worktree.clone(),
161 ) as _),
162 ),
163 cx,
164 );
165
166 assert_eq!(settings_a.tab_size.get(), 8);
167 assert_eq!(settings_b.tab_size.get(), 2);
168
169 get_all_tasks(&project, Some(worktree_id), &task_context, cx)
170 })
171 .await
172 .into_iter()
173 .map(|(source_kind, task)| {
174 let resolved = task.resolved.unwrap();
175 (
176 source_kind,
177 task.resolved_label,
178 resolved.args,
179 resolved.env,
180 )
181 })
182 .collect::<Vec<_>>();
183 assert_eq!(
184 all_tasks,
185 vec![
186 (
187 global_task_source_kind.clone(),
188 "cargo check".to_string(),
189 vec!["check".to_string(), "--all".to_string()],
190 HashMap::default(),
191 ),
192 (
193 TaskSourceKind::Worktree {
194 id: worktree_id,
195 abs_path: PathBuf::from("/the-root/b/.zed/tasks.json"),
196 id_base: "local_tasks_for_worktree".into(),
197 },
198 "cargo check".to_string(),
199 vec!["check".to_string()],
200 HashMap::default(),
201 ),
202 ]
203 );
204
205 let (_, resolved_task) = cx
206 .update(|cx| get_all_tasks(&project, Some(worktree_id), &task_context, cx))
207 .await
208 .into_iter()
209 .find(|(source_kind, _)| source_kind == &global_task_source_kind)
210 .expect("should have one global task");
211 project.update(cx, |project, cx| {
212 project.task_inventory().update(cx, |inventory, _| {
213 inventory.task_scheduled(global_task_source_kind.clone(), resolved_task);
214 });
215 });
216
217 let tasks = serde_json::to_string(&TaskTemplates(vec![TaskTemplate {
218 label: "cargo check".to_string(),
219 command: "cargo".to_string(),
220 args: vec![
221 "check".to_string(),
222 "--all".to_string(),
223 "--all-targets".to_string(),
224 ],
225 env: HashMap::from_iter(Some((
226 "RUSTFLAGS".to_string(),
227 "-Zunstable-options".to_string(),
228 ))),
229 ..TaskTemplate::default()
230 }]))
231 .unwrap();
232 let (tx, rx) = futures::channel::mpsc::unbounded();
233 cx.update(|cx| {
234 project.update(cx, |project, cx| {
235 project.task_inventory().update(cx, |inventory, cx| {
236 inventory.remove_local_static_source(Path::new("/the-root/.zed/tasks.json"));
237 inventory.add_source(
238 global_task_source_kind.clone(),
239 |tx, cx| StaticSource::new(TrackedFile::new(rx, tx, cx)),
240 cx,
241 );
242 });
243 })
244 });
245 tx.unbounded_send(tasks).unwrap();
246
247 cx.run_until_parked();
248 let all_tasks = cx
249 .update(|cx| get_all_tasks(&project, Some(worktree_id), &task_context, cx))
250 .await
251 .into_iter()
252 .map(|(source_kind, task)| {
253 let resolved = task.resolved.unwrap();
254 (
255 source_kind,
256 task.resolved_label,
257 resolved.args,
258 resolved.env,
259 )
260 })
261 .collect::<Vec<_>>();
262 assert_eq!(
263 all_tasks,
264 vec![
265 (
266 TaskSourceKind::Worktree {
267 id: worktree_id,
268 abs_path: PathBuf::from("/the-root/.zed/tasks.json"),
269 id_base: "local_tasks_for_worktree".into(),
270 },
271 "cargo check".to_string(),
272 vec![
273 "check".to_string(),
274 "--all".to_string(),
275 "--all-targets".to_string()
276 ],
277 HashMap::from_iter(Some((
278 "RUSTFLAGS".to_string(),
279 "-Zunstable-options".to_string()
280 ))),
281 ),
282 (
283 TaskSourceKind::Worktree {
284 id: worktree_id,
285 abs_path: PathBuf::from("/the-root/b/.zed/tasks.json"),
286 id_base: "local_tasks_for_worktree".into(),
287 },
288 "cargo check".to_string(),
289 vec!["check".to_string()],
290 HashMap::default(),
291 ),
292 ]
293 );
294}
295
296#[gpui::test]
297async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
298 init_test(cx);
299
300 let fs = FakeFs::new(cx.executor());
301 fs.insert_tree(
302 "/the-root",
303 json!({
304 "test.rs": "const A: i32 = 1;",
305 "test2.rs": "",
306 "Cargo.toml": "a = 1",
307 "package.json": "{\"a\": 1}",
308 }),
309 )
310 .await;
311
312 let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await;
313 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
314
315 let mut fake_rust_servers = language_registry.register_fake_lsp_adapter(
316 "Rust",
317 FakeLspAdapter {
318 name: "the-rust-language-server",
319 capabilities: lsp::ServerCapabilities {
320 completion_provider: Some(lsp::CompletionOptions {
321 trigger_characters: Some(vec![".".to_string(), "::".to_string()]),
322 ..Default::default()
323 }),
324 ..Default::default()
325 },
326 ..Default::default()
327 },
328 );
329 let mut fake_json_servers = language_registry.register_fake_lsp_adapter(
330 "JSON",
331 FakeLspAdapter {
332 name: "the-json-language-server",
333 capabilities: lsp::ServerCapabilities {
334 completion_provider: Some(lsp::CompletionOptions {
335 trigger_characters: Some(vec![":".to_string()]),
336 ..Default::default()
337 }),
338 ..Default::default()
339 },
340 ..Default::default()
341 },
342 );
343
344 // Open a buffer without an associated language server.
345 let toml_buffer = project
346 .update(cx, |project, cx| {
347 project.open_local_buffer("/the-root/Cargo.toml", cx)
348 })
349 .await
350 .unwrap();
351
352 // Open a buffer with an associated language server before the language for it has been loaded.
353 let rust_buffer = project
354 .update(cx, |project, cx| {
355 project.open_local_buffer("/the-root/test.rs", cx)
356 })
357 .await
358 .unwrap();
359 rust_buffer.update(cx, |buffer, _| {
360 assert_eq!(buffer.language().map(|l| l.name()), None);
361 });
362
363 // Now we add the languages to the project, and ensure they get assigned to all
364 // the relevant open buffers.
365 language_registry.add(json_lang());
366 language_registry.add(rust_lang());
367 cx.executor().run_until_parked();
368 rust_buffer.update(cx, |buffer, _| {
369 assert_eq!(buffer.language().map(|l| l.name()), Some("Rust".into()));
370 });
371
372 // A server is started up, and it is notified about Rust files.
373 let mut fake_rust_server = fake_rust_servers.next().await.unwrap();
374 assert_eq!(
375 fake_rust_server
376 .receive_notification::<lsp::notification::DidOpenTextDocument>()
377 .await
378 .text_document,
379 lsp::TextDocumentItem {
380 uri: lsp::Uri::from_file_path("/the-root/test.rs")
381 .unwrap()
382 .into(),
383 version: 0,
384 text: "const A: i32 = 1;".to_string(),
385 language_id: "rust".to_string(),
386 }
387 );
388
389 // The buffer is configured based on the language server's capabilities.
390 rust_buffer.update(cx, |buffer, _| {
391 assert_eq!(
392 buffer.completion_triggers(),
393 &[".".to_string(), "::".to_string()]
394 );
395 });
396 toml_buffer.update(cx, |buffer, _| {
397 assert!(buffer.completion_triggers().is_empty());
398 });
399
400 // Edit a buffer. The changes are reported to the language server.
401 rust_buffer.update(cx, |buffer, cx| buffer.edit([(16..16, "2")], None, cx));
402 assert_eq!(
403 fake_rust_server
404 .receive_notification::<lsp::notification::DidChangeTextDocument>()
405 .await
406 .text_document,
407 lsp::VersionedTextDocumentIdentifier::new(
408 lsp::Uri::from_file_path("/the-root/test.rs")
409 .unwrap()
410 .into(),
411 1
412 )
413 );
414
415 // Open a third buffer with a different associated language server.
416 let json_buffer = project
417 .update(cx, |project, cx| {
418 project.open_local_buffer("/the-root/package.json", cx)
419 })
420 .await
421 .unwrap();
422
423 // A json language server is started up and is only notified about the json buffer.
424 let mut fake_json_server = fake_json_servers.next().await.unwrap();
425 assert_eq!(
426 fake_json_server
427 .receive_notification::<lsp::notification::DidOpenTextDocument>()
428 .await
429 .text_document,
430 lsp::TextDocumentItem {
431 uri: lsp::Uri::from_file_path("/the-root/package.json")
432 .unwrap()
433 .into(),
434 version: 0,
435 text: "{\"a\": 1}".to_string(),
436 language_id: "json".to_string(),
437 }
438 );
439
440 // This buffer is configured based on the second language server's
441 // capabilities.
442 json_buffer.update(cx, |buffer, _| {
443 assert_eq!(buffer.completion_triggers(), &[":".to_string()]);
444 });
445
446 // When opening another buffer whose language server is already running,
447 // it is also configured based on the existing language server's capabilities.
448 let rust_buffer2 = project
449 .update(cx, |project, cx| {
450 project.open_local_buffer("/the-root/test2.rs", cx)
451 })
452 .await
453 .unwrap();
454 rust_buffer2.update(cx, |buffer, _| {
455 assert_eq!(
456 buffer.completion_triggers(),
457 &[".".to_string(), "::".to_string()]
458 );
459 });
460
461 // Changes are reported only to servers matching the buffer's language.
462 toml_buffer.update(cx, |buffer, cx| buffer.edit([(5..5, "23")], None, cx));
463 rust_buffer2.update(cx, |buffer, cx| {
464 buffer.edit([(0..0, "let x = 1;")], None, cx)
465 });
466 assert_eq!(
467 fake_rust_server
468 .receive_notification::<lsp::notification::DidChangeTextDocument>()
469 .await
470 .text_document,
471 lsp::VersionedTextDocumentIdentifier::new(
472 lsp::Uri::from_file_path("/the-root/test2.rs")
473 .unwrap()
474 .into(),
475 1
476 )
477 );
478
479 // Save notifications are reported to all servers.
480 project
481 .update(cx, |project, cx| project.save_buffer(toml_buffer, cx))
482 .await
483 .unwrap();
484 assert_eq!(
485 fake_rust_server
486 .receive_notification::<lsp::notification::DidSaveTextDocument>()
487 .await
488 .text_document,
489 lsp::TextDocumentIdentifier::new(
490 lsp::Uri::from_file_path("/the-root/Cargo.toml")
491 .unwrap()
492 .into()
493 )
494 );
495 assert_eq!(
496 fake_json_server
497 .receive_notification::<lsp::notification::DidSaveTextDocument>()
498 .await
499 .text_document,
500 lsp::TextDocumentIdentifier::new(
501 lsp::Uri::from_file_path("/the-root/Cargo.toml")
502 .unwrap()
503 .into()
504 )
505 );
506
507 // Renames are reported only to servers matching the buffer's language.
508 fs.rename(
509 Path::new("/the-root/test2.rs"),
510 Path::new("/the-root/test3.rs"),
511 Default::default(),
512 )
513 .await
514 .unwrap();
515 assert_eq!(
516 fake_rust_server
517 .receive_notification::<lsp::notification::DidCloseTextDocument>()
518 .await
519 .text_document,
520 lsp::TextDocumentIdentifier::new(
521 lsp::Uri::from_file_path("/the-root/test2.rs")
522 .unwrap()
523 .into()
524 ),
525 );
526 assert_eq!(
527 fake_rust_server
528 .receive_notification::<lsp::notification::DidOpenTextDocument>()
529 .await
530 .text_document,
531 lsp::TextDocumentItem {
532 uri: lsp::Uri::from_file_path("/the-root/test3.rs")
533 .unwrap()
534 .into(),
535 version: 0,
536 text: rust_buffer2.update(cx, |buffer, _| buffer.text()),
537 language_id: "rust".to_string(),
538 },
539 );
540
541 rust_buffer2.update(cx, |buffer, cx| {
542 buffer.update_diagnostics(
543 LanguageServerId(0),
544 DiagnosticSet::from_sorted_entries(
545 vec![DiagnosticEntry {
546 diagnostic: Default::default(),
547 range: Anchor::MIN..Anchor::MAX,
548 }],
549 &buffer.snapshot(),
550 ),
551 cx,
552 );
553 assert_eq!(
554 buffer
555 .snapshot()
556 .diagnostics_in_range::<_, usize>(0..buffer.len(), false)
557 .count(),
558 1
559 );
560 });
561
562 // When the rename changes the extension of the file, the buffer gets closed on the old
563 // language server and gets opened on the new one.
564 fs.rename(
565 Path::new("/the-root/test3.rs"),
566 Path::new("/the-root/test3.json"),
567 Default::default(),
568 )
569 .await
570 .unwrap();
571 assert_eq!(
572 fake_rust_server
573 .receive_notification::<lsp::notification::DidCloseTextDocument>()
574 .await
575 .text_document,
576 lsp::TextDocumentIdentifier::new(
577 lsp::Uri::from_file_path("/the-root/test3.rs")
578 .unwrap()
579 .into(),
580 ),
581 );
582 assert_eq!(
583 fake_json_server
584 .receive_notification::<lsp::notification::DidOpenTextDocument>()
585 .await
586 .text_document,
587 lsp::TextDocumentItem {
588 uri: lsp::Uri::from_file_path("/the-root/test3.json")
589 .unwrap()
590 .into(),
591 version: 0,
592 text: rust_buffer2.update(cx, |buffer, _| buffer.text()),
593 language_id: "json".to_string(),
594 },
595 );
596
597 // We clear the diagnostics, since the language has changed.
598 rust_buffer2.update(cx, |buffer, _| {
599 assert_eq!(
600 buffer
601 .snapshot()
602 .diagnostics_in_range::<_, usize>(0..buffer.len(), false)
603 .count(),
604 0
605 );
606 });
607
608 // The renamed file's version resets after changing language server.
609 rust_buffer2.update(cx, |buffer, cx| buffer.edit([(0..0, "// ")], None, cx));
610 assert_eq!(
611 fake_json_server
612 .receive_notification::<lsp::notification::DidChangeTextDocument>()
613 .await
614 .text_document,
615 lsp::VersionedTextDocumentIdentifier::new(
616 lsp::Uri::from_file_path("/the-root/test3.json")
617 .unwrap()
618 .into(),
619 1
620 )
621 );
622
623 // Restart language servers
624 project.update(cx, |project, cx| {
625 project.restart_language_servers_for_buffers(
626 vec![rust_buffer.clone(), json_buffer.clone()],
627 cx,
628 );
629 });
630
631 let mut rust_shutdown_requests = fake_rust_server
632 .handle_request::<lsp::request::Shutdown, _, _>(|_, _| future::ready(Ok(())));
633 let mut json_shutdown_requests = fake_json_server
634 .handle_request::<lsp::request::Shutdown, _, _>(|_, _| future::ready(Ok(())));
635 futures::join!(rust_shutdown_requests.next(), json_shutdown_requests.next());
636
637 let mut fake_rust_server = fake_rust_servers.next().await.unwrap();
638 let mut fake_json_server = fake_json_servers.next().await.unwrap();
639
640 // Ensure rust document is reopened in new rust language server
641 assert_eq!(
642 fake_rust_server
643 .receive_notification::<lsp::notification::DidOpenTextDocument>()
644 .await
645 .text_document,
646 lsp::TextDocumentItem {
647 uri: lsp::Uri::from_file_path("/the-root/test.rs")
648 .unwrap()
649 .into(),
650 version: 0,
651 text: rust_buffer.update(cx, |buffer, _| buffer.text()),
652 language_id: "rust".to_string(),
653 }
654 );
655
656 // Ensure json documents are reopened in new json language server
657 assert_set_eq!(
658 [
659 fake_json_server
660 .receive_notification::<lsp::notification::DidOpenTextDocument>()
661 .await
662 .text_document,
663 fake_json_server
664 .receive_notification::<lsp::notification::DidOpenTextDocument>()
665 .await
666 .text_document,
667 ],
668 [
669 lsp::TextDocumentItem {
670 uri: lsp::Uri::from_file_path("/the-root/package.json")
671 .unwrap()
672 .into(),
673 version: 0,
674 text: json_buffer.update(cx, |buffer, _| buffer.text()),
675 language_id: "json".to_string(),
676 },
677 lsp::TextDocumentItem {
678 uri: lsp::Uri::from_file_path("/the-root/test3.json")
679 .unwrap()
680 .into(),
681 version: 0,
682 text: rust_buffer2.update(cx, |buffer, _| buffer.text()),
683 language_id: "json".to_string(),
684 }
685 ]
686 );
687
688 // Close notifications are reported only to servers matching the buffer's language.
689 cx.update(|_| drop(json_buffer));
690 let close_message = lsp::DidCloseTextDocumentParams {
691 text_document: lsp::TextDocumentIdentifier::new(
692 lsp::Uri::from_file_path("/the-root/package.json")
693 .unwrap()
694 .into(),
695 ),
696 };
697 assert_eq!(
698 fake_json_server
699 .receive_notification::<lsp::notification::DidCloseTextDocument>()
700 .await,
701 close_message,
702 );
703}
704
705#[gpui::test]
706async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppContext) {
707 init_test(cx);
708
709 let fs = FakeFs::new(cx.executor());
710 fs.insert_tree(
711 "/the-root",
712 json!({
713 ".gitignore": "target\n",
714 "src": {
715 "a.rs": "",
716 "b.rs": "",
717 },
718 "target": {
719 "x": {
720 "out": {
721 "x.rs": ""
722 }
723 },
724 "y": {
725 "out": {
726 "y.rs": "",
727 }
728 },
729 "z": {
730 "out": {
731 "z.rs": ""
732 }
733 }
734 }
735 }),
736 )
737 .await;
738
739 let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await;
740 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
741 language_registry.add(rust_lang());
742 let mut fake_servers = language_registry.register_fake_lsp_adapter(
743 "Rust",
744 FakeLspAdapter {
745 name: "the-language-server",
746 ..Default::default()
747 },
748 );
749
750 cx.executor().run_until_parked();
751
752 // Start the language server by opening a buffer with a compatible file extension.
753 let _buffer = project
754 .update(cx, |project, cx| {
755 project.open_local_buffer("/the-root/src/a.rs", cx)
756 })
757 .await
758 .unwrap();
759
760 // Initially, we don't load ignored files because the language server has not explicitly asked us to watch them.
761 project.update(cx, |project, cx| {
762 let worktree = project.worktrees().next().unwrap();
763 assert_eq!(
764 worktree
765 .read(cx)
766 .snapshot()
767 .entries(true, 0)
768 .map(|entry| (entry.path.as_ref(), entry.is_ignored))
769 .collect::<Vec<_>>(),
770 &[
771 (Path::new(""), false),
772 (Path::new(".gitignore"), false),
773 (Path::new("src"), false),
774 (Path::new("src/a.rs"), false),
775 (Path::new("src/b.rs"), false),
776 (Path::new("target"), true),
777 ]
778 );
779 });
780
781 let prev_read_dir_count = fs.read_dir_call_count();
782
783 // Keep track of the FS events reported to the language server.
784 let fake_server = fake_servers.next().await.unwrap();
785 let file_changes = Arc::new(Mutex::new(Vec::new()));
786 fake_server
787 .request::<lsp::request::RegisterCapability>(lsp::RegistrationParams {
788 registrations: vec![lsp::Registration {
789 id: Default::default(),
790 method: "workspace/didChangeWatchedFiles".to_string(),
791 register_options: serde_json::to_value(
792 lsp::DidChangeWatchedFilesRegistrationOptions {
793 watchers: vec![
794 lsp::FileSystemWatcher {
795 glob_pattern: lsp::GlobPattern::String(
796 "/the-root/Cargo.toml".to_string(),
797 ),
798 kind: None,
799 },
800 lsp::FileSystemWatcher {
801 glob_pattern: lsp::GlobPattern::String(
802 "/the-root/src/*.{rs,c}".to_string(),
803 ),
804 kind: None,
805 },
806 lsp::FileSystemWatcher {
807 glob_pattern: lsp::GlobPattern::String(
808 "/the-root/target/y/**/*.rs".to_string(),
809 ),
810 kind: None,
811 },
812 ],
813 },
814 )
815 .ok(),
816 }],
817 })
818 .await
819 .unwrap();
820 fake_server.handle_notification::<lsp::notification::DidChangeWatchedFiles, _>({
821 let file_changes = file_changes.clone();
822 move |params, _| {
823 let mut file_changes = file_changes.lock();
824 file_changes.extend(params.changes);
825 file_changes.sort_by(|a, b| a.uri.cmp(&b.uri));
826 }
827 });
828
829 cx.executor().run_until_parked();
830 assert_eq!(mem::take(&mut *file_changes.lock()), &[]);
831 assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 4);
832
833 // Now the language server has asked us to watch an ignored directory path,
834 // so we recursively load it.
835 project.update(cx, |project, cx| {
836 let worktree = project.worktrees().next().unwrap();
837 assert_eq!(
838 worktree
839 .read(cx)
840 .snapshot()
841 .entries(true, 0)
842 .map(|entry| (entry.path.as_ref(), entry.is_ignored))
843 .collect::<Vec<_>>(),
844 &[
845 (Path::new(""), false),
846 (Path::new(".gitignore"), false),
847 (Path::new("src"), false),
848 (Path::new("src/a.rs"), false),
849 (Path::new("src/b.rs"), false),
850 (Path::new("target"), true),
851 (Path::new("target/x"), true),
852 (Path::new("target/y"), true),
853 (Path::new("target/y/out"), true),
854 (Path::new("target/y/out/y.rs"), true),
855 (Path::new("target/z"), true),
856 ]
857 );
858 });
859
860 // Perform some file system mutations, two of which match the watched patterns,
861 // and one of which does not.
862 fs.create_file("/the-root/src/c.rs".as_ref(), Default::default())
863 .await
864 .unwrap();
865 fs.create_file("/the-root/src/d.txt".as_ref(), Default::default())
866 .await
867 .unwrap();
868 fs.remove_file("/the-root/src/b.rs".as_ref(), Default::default())
869 .await
870 .unwrap();
871 fs.create_file("/the-root/target/x/out/x2.rs".as_ref(), Default::default())
872 .await
873 .unwrap();
874 fs.create_file("/the-root/target/y/out/y2.rs".as_ref(), Default::default())
875 .await
876 .unwrap();
877
878 // The language server receives events for the FS mutations that match its watch patterns.
879 cx.executor().run_until_parked();
880 assert_eq!(
881 &*file_changes.lock(),
882 &[
883 lsp::FileEvent {
884 uri: lsp::Uri::from_file_path("/the-root/src/b.rs")
885 .unwrap()
886 .into(),
887 typ: lsp::FileChangeType::DELETED,
888 },
889 lsp::FileEvent {
890 uri: lsp::Uri::from_file_path("/the-root/src/c.rs")
891 .unwrap()
892 .into(),
893 typ: lsp::FileChangeType::CREATED,
894 },
895 lsp::FileEvent {
896 uri: lsp::Uri::from_file_path("/the-root/target/y/out/y2.rs")
897 .unwrap()
898 .into(),
899 typ: lsp::FileChangeType::CREATED,
900 },
901 ]
902 );
903}
904
905#[gpui::test]
906async fn test_single_file_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
907 init_test(cx);
908
909 let fs = FakeFs::new(cx.executor());
910 fs.insert_tree(
911 "/dir",
912 json!({
913 "a.rs": "let a = 1;",
914 "b.rs": "let b = 2;"
915 }),
916 )
917 .await;
918
919 let project = Project::test(fs, ["/dir/a.rs".as_ref(), "/dir/b.rs".as_ref()], cx).await;
920
921 let buffer_a = project
922 .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
923 .await
924 .unwrap();
925 let buffer_b = project
926 .update(cx, |project, cx| project.open_local_buffer("/dir/b.rs", cx))
927 .await
928 .unwrap();
929
930 project.update(cx, |project, cx| {
931 project
932 .update_diagnostics(
933 LanguageServerId(0),
934 lsp::PublishDiagnosticsParams {
935 uri: Uri::from_file_path("/dir/a.rs").unwrap().into(),
936 version: None,
937 diagnostics: vec![lsp::Diagnostic {
938 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 5)),
939 severity: Some(lsp::DiagnosticSeverity::ERROR),
940 message: "error 1".to_string(),
941 ..Default::default()
942 }],
943 },
944 &[],
945 cx,
946 )
947 .unwrap();
948 project
949 .update_diagnostics(
950 LanguageServerId(0),
951 lsp::PublishDiagnosticsParams {
952 uri: Uri::from_file_path("/dir/b.rs").unwrap().into(),
953 version: None,
954 diagnostics: vec![lsp::Diagnostic {
955 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 5)),
956 severity: Some(lsp::DiagnosticSeverity::WARNING),
957 message: "error 2".to_string(),
958 ..Default::default()
959 }],
960 },
961 &[],
962 cx,
963 )
964 .unwrap();
965 });
966
967 buffer_a.update(cx, |buffer, _| {
968 let chunks = chunks_with_diagnostics(buffer, 0..buffer.len());
969 assert_eq!(
970 chunks
971 .iter()
972 .map(|(s, d)| (s.as_str(), *d))
973 .collect::<Vec<_>>(),
974 &[
975 ("let ", None),
976 ("a", Some(DiagnosticSeverity::ERROR)),
977 (" = 1;", None),
978 ]
979 );
980 });
981 buffer_b.update(cx, |buffer, _| {
982 let chunks = chunks_with_diagnostics(buffer, 0..buffer.len());
983 assert_eq!(
984 chunks
985 .iter()
986 .map(|(s, d)| (s.as_str(), *d))
987 .collect::<Vec<_>>(),
988 &[
989 ("let ", None),
990 ("b", Some(DiagnosticSeverity::WARNING)),
991 (" = 2;", None),
992 ]
993 );
994 });
995}
996
997#[gpui::test]
998async fn test_omitted_diagnostics(cx: &mut gpui::TestAppContext) {
999 init_test(cx);
1000
1001 let fs = FakeFs::new(cx.executor());
1002 fs.insert_tree(
1003 "/root",
1004 json!({
1005 "dir": {
1006 ".git": {
1007 "HEAD": "ref: refs/heads/main",
1008 },
1009 ".gitignore": "b.rs",
1010 "a.rs": "let a = 1;",
1011 "b.rs": "let b = 2;",
1012 },
1013 "other.rs": "let b = c;"
1014 }),
1015 )
1016 .await;
1017
1018 let project = Project::test(fs, ["/root/dir".as_ref()], cx).await;
1019 let (worktree, _) = project
1020 .update(cx, |project, cx| {
1021 project.find_or_create_local_worktree("/root/dir", true, cx)
1022 })
1023 .await
1024 .unwrap();
1025 let main_worktree_id = worktree.read_with(cx, |tree, _| tree.id());
1026
1027 let (worktree, _) = project
1028 .update(cx, |project, cx| {
1029 project.find_or_create_local_worktree("/root/other.rs", false, cx)
1030 })
1031 .await
1032 .unwrap();
1033 let other_worktree_id = worktree.update(cx, |tree, _| tree.id());
1034
1035 let server_id = LanguageServerId(0);
1036 project.update(cx, |project, cx| {
1037 project
1038 .update_diagnostics(
1039 server_id,
1040 lsp::PublishDiagnosticsParams {
1041 uri: Uri::from_file_path("/root/dir/b.rs").unwrap().into(),
1042 version: None,
1043 diagnostics: vec![lsp::Diagnostic {
1044 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 5)),
1045 severity: Some(lsp::DiagnosticSeverity::ERROR),
1046 message: "unused variable 'b'".to_string(),
1047 ..Default::default()
1048 }],
1049 },
1050 &[],
1051 cx,
1052 )
1053 .unwrap();
1054 project
1055 .update_diagnostics(
1056 server_id,
1057 lsp::PublishDiagnosticsParams {
1058 uri: Uri::from_file_path("/root/other.rs").unwrap().into(),
1059 version: None,
1060 diagnostics: vec![lsp::Diagnostic {
1061 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 9)),
1062 severity: Some(lsp::DiagnosticSeverity::ERROR),
1063 message: "unknown variable 'c'".to_string(),
1064 ..Default::default()
1065 }],
1066 },
1067 &[],
1068 cx,
1069 )
1070 .unwrap();
1071 });
1072
1073 let main_ignored_buffer = project
1074 .update(cx, |project, cx| {
1075 project.open_buffer((main_worktree_id, "b.rs"), cx)
1076 })
1077 .await
1078 .unwrap();
1079 main_ignored_buffer.update(cx, |buffer, _| {
1080 let chunks = chunks_with_diagnostics(buffer, 0..buffer.len());
1081 assert_eq!(
1082 chunks
1083 .iter()
1084 .map(|(s, d)| (s.as_str(), *d))
1085 .collect::<Vec<_>>(),
1086 &[
1087 ("let ", None),
1088 ("b", Some(DiagnosticSeverity::ERROR)),
1089 (" = 2;", None),
1090 ],
1091 "Gigitnored buffers should still get in-buffer diagnostics",
1092 );
1093 });
1094 let other_buffer = project
1095 .update(cx, |project, cx| {
1096 project.open_buffer((other_worktree_id, ""), cx)
1097 })
1098 .await
1099 .unwrap();
1100 other_buffer.update(cx, |buffer, _| {
1101 let chunks = chunks_with_diagnostics(buffer, 0..buffer.len());
1102 assert_eq!(
1103 chunks
1104 .iter()
1105 .map(|(s, d)| (s.as_str(), *d))
1106 .collect::<Vec<_>>(),
1107 &[
1108 ("let b = ", None),
1109 ("c", Some(DiagnosticSeverity::ERROR)),
1110 (";", None),
1111 ],
1112 "Buffers from hidden projects should still get in-buffer diagnostics"
1113 );
1114 });
1115
1116 project.update(cx, |project, cx| {
1117 assert_eq!(project.diagnostic_summaries(false, cx).next(), None);
1118 assert_eq!(
1119 project.diagnostic_summaries(true, cx).collect::<Vec<_>>(),
1120 vec![(
1121 ProjectPath {
1122 worktree_id: main_worktree_id,
1123 path: Arc::from(Path::new("b.rs")),
1124 },
1125 server_id,
1126 DiagnosticSummary {
1127 error_count: 1,
1128 warning_count: 0,
1129 }
1130 )]
1131 );
1132 assert_eq!(project.diagnostic_summary(false, cx).error_count, 0);
1133 assert_eq!(project.diagnostic_summary(true, cx).error_count, 1);
1134 });
1135}
1136
1137#[gpui::test]
1138async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) {
1139 init_test(cx);
1140
1141 let progress_token = "the-progress-token";
1142
1143 let fs = FakeFs::new(cx.executor());
1144 fs.insert_tree(
1145 "/dir",
1146 json!({
1147 "a.rs": "fn a() { A }",
1148 "b.rs": "const y: i32 = 1",
1149 }),
1150 )
1151 .await;
1152
1153 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
1154 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
1155
1156 language_registry.add(rust_lang());
1157 let mut fake_servers = language_registry.register_fake_lsp_adapter(
1158 "Rust",
1159 FakeLspAdapter {
1160 disk_based_diagnostics_progress_token: Some(progress_token.into()),
1161 disk_based_diagnostics_sources: vec!["disk".into()],
1162 ..Default::default()
1163 },
1164 );
1165
1166 let worktree_id = project.update(cx, |p, cx| p.worktrees().next().unwrap().read(cx).id());
1167
1168 // Cause worktree to start the fake language server
1169 let _buffer = project
1170 .update(cx, |project, cx| project.open_local_buffer("/dir/b.rs", cx))
1171 .await
1172 .unwrap();
1173
1174 let mut events = cx.events(&project);
1175
1176 let fake_server = fake_servers.next().await.unwrap();
1177 assert_eq!(
1178 events.next().await.unwrap(),
1179 Event::LanguageServerAdded(LanguageServerId(0)),
1180 );
1181
1182 fake_server
1183 .start_progress(format!("{}/0", progress_token))
1184 .await;
1185 assert_eq!(
1186 events.next().await.unwrap(),
1187 Event::DiskBasedDiagnosticsStarted {
1188 language_server_id: LanguageServerId(0),
1189 }
1190 );
1191
1192 fake_server.notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
1193 uri: Uri::from_file_path("/dir/a.rs").unwrap().into(),
1194 version: None,
1195 diagnostics: vec![lsp::Diagnostic {
1196 range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
1197 severity: Some(lsp::DiagnosticSeverity::ERROR),
1198 message: "undefined variable 'A'".to_string(),
1199 ..Default::default()
1200 }],
1201 });
1202 assert_eq!(
1203 events.next().await.unwrap(),
1204 Event::DiagnosticsUpdated {
1205 language_server_id: LanguageServerId(0),
1206 path: (worktree_id, Path::new("a.rs")).into()
1207 }
1208 );
1209
1210 fake_server.end_progress(format!("{}/0", progress_token));
1211 assert_eq!(
1212 events.next().await.unwrap(),
1213 Event::DiskBasedDiagnosticsFinished {
1214 language_server_id: LanguageServerId(0)
1215 }
1216 );
1217
1218 let buffer = project
1219 .update(cx, |p, cx| p.open_local_buffer("/dir/a.rs", cx))
1220 .await
1221 .unwrap();
1222
1223 buffer.update(cx, |buffer, _| {
1224 let snapshot = buffer.snapshot();
1225 let diagnostics = snapshot
1226 .diagnostics_in_range::<_, Point>(0..buffer.len(), false)
1227 .collect::<Vec<_>>();
1228 assert_eq!(
1229 diagnostics,
1230 &[DiagnosticEntry {
1231 range: Point::new(0, 9)..Point::new(0, 10),
1232 diagnostic: Diagnostic {
1233 severity: lsp::DiagnosticSeverity::ERROR,
1234 message: "undefined variable 'A'".to_string(),
1235 group_id: 0,
1236 is_primary: true,
1237 ..Default::default()
1238 }
1239 }]
1240 )
1241 });
1242
1243 // Ensure publishing empty diagnostics twice only results in one update event.
1244 fake_server.notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
1245 uri: Uri::from_file_path("/dir/a.rs").unwrap().into(),
1246 version: None,
1247 diagnostics: Default::default(),
1248 });
1249 assert_eq!(
1250 events.next().await.unwrap(),
1251 Event::DiagnosticsUpdated {
1252 language_server_id: LanguageServerId(0),
1253 path: (worktree_id, Path::new("a.rs")).into()
1254 }
1255 );
1256
1257 fake_server.notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
1258 uri: Uri::from_file_path("/dir/a.rs").unwrap().into(),
1259 version: None,
1260 diagnostics: Default::default(),
1261 });
1262 cx.executor().run_until_parked();
1263 assert_eq!(futures::poll!(events.next()), Poll::Pending);
1264}
1265
1266#[gpui::test]
1267async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppContext) {
1268 init_test(cx);
1269
1270 let progress_token = "the-progress-token";
1271
1272 let fs = FakeFs::new(cx.executor());
1273 fs.insert_tree("/dir", json!({ "a.rs": "" })).await;
1274
1275 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
1276
1277 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
1278 language_registry.add(rust_lang());
1279 let mut fake_servers = language_registry.register_fake_lsp_adapter(
1280 "Rust",
1281 FakeLspAdapter {
1282 name: "the-language-server",
1283 disk_based_diagnostics_sources: vec!["disk".into()],
1284 disk_based_diagnostics_progress_token: Some(progress_token.into()),
1285 ..Default::default()
1286 },
1287 );
1288
1289 let buffer = project
1290 .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
1291 .await
1292 .unwrap();
1293
1294 // Simulate diagnostics starting to update.
1295 let fake_server = fake_servers.next().await.unwrap();
1296 fake_server.start_progress(progress_token).await;
1297
1298 // Restart the server before the diagnostics finish updating.
1299 project.update(cx, |project, cx| {
1300 project.restart_language_servers_for_buffers([buffer], cx);
1301 });
1302 let mut events = cx.events(&project);
1303
1304 // Simulate the newly started server sending more diagnostics.
1305 let fake_server = fake_servers.next().await.unwrap();
1306 assert_eq!(
1307 events.next().await.unwrap(),
1308 Event::LanguageServerAdded(LanguageServerId(1))
1309 );
1310 fake_server.start_progress(progress_token).await;
1311 assert_eq!(
1312 events.next().await.unwrap(),
1313 Event::DiskBasedDiagnosticsStarted {
1314 language_server_id: LanguageServerId(1)
1315 }
1316 );
1317 project.update(cx, |project, _| {
1318 assert_eq!(
1319 project
1320 .language_servers_running_disk_based_diagnostics()
1321 .collect::<Vec<_>>(),
1322 [LanguageServerId(1)]
1323 );
1324 });
1325
1326 // All diagnostics are considered done, despite the old server's diagnostic
1327 // task never completing.
1328 fake_server.end_progress(progress_token);
1329 assert_eq!(
1330 events.next().await.unwrap(),
1331 Event::DiskBasedDiagnosticsFinished {
1332 language_server_id: LanguageServerId(1)
1333 }
1334 );
1335 project.update(cx, |project, _| {
1336 assert_eq!(
1337 project
1338 .language_servers_running_disk_based_diagnostics()
1339 .collect::<Vec<_>>(),
1340 [] as [language::LanguageServerId; 0]
1341 );
1342 });
1343}
1344
1345#[gpui::test]
1346async fn test_restarting_server_with_diagnostics_published(cx: &mut gpui::TestAppContext) {
1347 init_test(cx);
1348
1349 let fs = FakeFs::new(cx.executor());
1350 fs.insert_tree("/dir", json!({ "a.rs": "x" })).await;
1351
1352 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
1353
1354 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
1355 language_registry.add(rust_lang());
1356 let mut fake_servers =
1357 language_registry.register_fake_lsp_adapter("Rust", FakeLspAdapter::default());
1358
1359 let buffer = project
1360 .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
1361 .await
1362 .unwrap();
1363
1364 // Publish diagnostics
1365 let fake_server = fake_servers.next().await.unwrap();
1366 fake_server.notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
1367 uri: Uri::from_file_path("/dir/a.rs").unwrap().into(),
1368 version: None,
1369 diagnostics: vec![lsp::Diagnostic {
1370 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
1371 severity: Some(lsp::DiagnosticSeverity::ERROR),
1372 message: "the message".to_string(),
1373 ..Default::default()
1374 }],
1375 });
1376
1377 cx.executor().run_until_parked();
1378 buffer.update(cx, |buffer, _| {
1379 assert_eq!(
1380 buffer
1381 .snapshot()
1382 .diagnostics_in_range::<_, usize>(0..1, false)
1383 .map(|entry| entry.diagnostic.message.clone())
1384 .collect::<Vec<_>>(),
1385 ["the message".to_string()]
1386 );
1387 });
1388 project.update(cx, |project, cx| {
1389 assert_eq!(
1390 project.diagnostic_summary(false, cx),
1391 DiagnosticSummary {
1392 error_count: 1,
1393 warning_count: 0,
1394 }
1395 );
1396 });
1397
1398 project.update(cx, |project, cx| {
1399 project.restart_language_servers_for_buffers([buffer.clone()], cx);
1400 });
1401
1402 // The diagnostics are cleared.
1403 cx.executor().run_until_parked();
1404 buffer.update(cx, |buffer, _| {
1405 assert_eq!(
1406 buffer
1407 .snapshot()
1408 .diagnostics_in_range::<_, usize>(0..1, false)
1409 .map(|entry| entry.diagnostic.message.clone())
1410 .collect::<Vec<_>>(),
1411 Vec::<String>::new(),
1412 );
1413 });
1414 project.update(cx, |project, cx| {
1415 assert_eq!(
1416 project.diagnostic_summary(false, cx),
1417 DiagnosticSummary {
1418 error_count: 0,
1419 warning_count: 0,
1420 }
1421 );
1422 });
1423}
1424
1425#[gpui::test]
1426async fn test_restarted_server_reporting_invalid_buffer_version(cx: &mut gpui::TestAppContext) {
1427 init_test(cx);
1428
1429 let fs = FakeFs::new(cx.executor());
1430 fs.insert_tree("/dir", json!({ "a.rs": "" })).await;
1431
1432 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
1433 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
1434
1435 language_registry.add(rust_lang());
1436 let mut fake_servers =
1437 language_registry.register_fake_lsp_adapter("Rust", FakeLspAdapter::default());
1438
1439 let buffer = project
1440 .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
1441 .await
1442 .unwrap();
1443
1444 // Before restarting the server, report diagnostics with an unknown buffer version.
1445 let fake_server = fake_servers.next().await.unwrap();
1446 fake_server.notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
1447 uri: lsp::Uri::from_file_path("/dir/a.rs").unwrap().into(),
1448 version: Some(10000),
1449 diagnostics: Vec::new(),
1450 });
1451 cx.executor().run_until_parked();
1452
1453 project.update(cx, |project, cx| {
1454 project.restart_language_servers_for_buffers([buffer.clone()], cx);
1455 });
1456 let mut fake_server = fake_servers.next().await.unwrap();
1457 let notification = fake_server
1458 .receive_notification::<lsp::notification::DidOpenTextDocument>()
1459 .await
1460 .text_document;
1461 assert_eq!(notification.version, 0);
1462}
1463
1464#[gpui::test]
1465async fn test_toggling_enable_language_server(cx: &mut gpui::TestAppContext) {
1466 init_test(cx);
1467
1468 let fs = FakeFs::new(cx.executor());
1469 fs.insert_tree("/dir", json!({ "a.rs": "", "b.js": "" }))
1470 .await;
1471
1472 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
1473 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
1474
1475 let mut fake_rust_servers = language_registry.register_fake_lsp_adapter(
1476 "Rust",
1477 FakeLspAdapter {
1478 name: "rust-lsp",
1479 ..Default::default()
1480 },
1481 );
1482 let mut fake_js_servers = language_registry.register_fake_lsp_adapter(
1483 "JavaScript",
1484 FakeLspAdapter {
1485 name: "js-lsp",
1486 ..Default::default()
1487 },
1488 );
1489 language_registry.add(rust_lang());
1490 language_registry.add(js_lang());
1491
1492 let _rs_buffer = project
1493 .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
1494 .await
1495 .unwrap();
1496 let _js_buffer = project
1497 .update(cx, |project, cx| project.open_local_buffer("/dir/b.js", cx))
1498 .await
1499 .unwrap();
1500
1501 let mut fake_rust_server_1 = fake_rust_servers.next().await.unwrap();
1502 assert_eq!(
1503 fake_rust_server_1
1504 .receive_notification::<lsp::notification::DidOpenTextDocument>()
1505 .await
1506 .text_document
1507 .uri
1508 .as_str(),
1509 "file:///dir/a.rs"
1510 );
1511
1512 let mut fake_js_server = fake_js_servers.next().await.unwrap();
1513 assert_eq!(
1514 fake_js_server
1515 .receive_notification::<lsp::notification::DidOpenTextDocument>()
1516 .await
1517 .text_document
1518 .uri
1519 .as_str(),
1520 "file:///dir/b.js"
1521 );
1522
1523 // Disable Rust language server, ensuring only that server gets stopped.
1524 cx.update(|cx| {
1525 SettingsStore::update_global(cx, |settings, cx| {
1526 settings.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1527 settings.languages.insert(
1528 Arc::from("Rust"),
1529 LanguageSettingsContent {
1530 enable_language_server: Some(false),
1531 ..Default::default()
1532 },
1533 );
1534 });
1535 })
1536 });
1537 fake_rust_server_1
1538 .receive_notification::<lsp::notification::Exit>()
1539 .await;
1540
1541 // Enable Rust and disable JavaScript language servers, ensuring that the
1542 // former gets started again and that the latter stops.
1543 cx.update(|cx| {
1544 SettingsStore::update_global(cx, |settings, cx| {
1545 settings.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1546 settings.languages.insert(
1547 Arc::from("Rust"),
1548 LanguageSettingsContent {
1549 enable_language_server: Some(true),
1550 ..Default::default()
1551 },
1552 );
1553 settings.languages.insert(
1554 Arc::from("JavaScript"),
1555 LanguageSettingsContent {
1556 enable_language_server: Some(false),
1557 ..Default::default()
1558 },
1559 );
1560 });
1561 })
1562 });
1563 let mut fake_rust_server_2 = fake_rust_servers.next().await.unwrap();
1564 assert_eq!(
1565 fake_rust_server_2
1566 .receive_notification::<lsp::notification::DidOpenTextDocument>()
1567 .await
1568 .text_document
1569 .uri
1570 .as_str(),
1571 "file:///dir/a.rs"
1572 );
1573 fake_js_server
1574 .receive_notification::<lsp::notification::Exit>()
1575 .await;
1576}
1577
1578#[gpui::test(iterations = 3)]
1579async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) {
1580 init_test(cx);
1581
1582 let text = "
1583 fn a() { A }
1584 fn b() { BB }
1585 fn c() { CCC }
1586 "
1587 .unindent();
1588
1589 let fs = FakeFs::new(cx.executor());
1590 fs.insert_tree("/dir", json!({ "a.rs": text })).await;
1591
1592 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
1593 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
1594
1595 language_registry.add(rust_lang());
1596 let mut fake_servers = language_registry.register_fake_lsp_adapter(
1597 "Rust",
1598 FakeLspAdapter {
1599 disk_based_diagnostics_sources: vec!["disk".into()],
1600 ..Default::default()
1601 },
1602 );
1603
1604 let buffer = project
1605 .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
1606 .await
1607 .unwrap();
1608
1609 let mut fake_server = fake_servers.next().await.unwrap();
1610 let open_notification = fake_server
1611 .receive_notification::<lsp::notification::DidOpenTextDocument>()
1612 .await;
1613
1614 // Edit the buffer, moving the content down
1615 buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "\n\n")], None, cx));
1616 let change_notification_1 = fake_server
1617 .receive_notification::<lsp::notification::DidChangeTextDocument>()
1618 .await;
1619 assert!(change_notification_1.text_document.version > open_notification.text_document.version);
1620
1621 // Report some diagnostics for the initial version of the buffer
1622 fake_server.notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
1623 uri: lsp::Uri::from_file_path("/dir/a.rs").unwrap().into(),
1624 version: Some(open_notification.text_document.version),
1625 diagnostics: vec![
1626 lsp::Diagnostic {
1627 range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
1628 severity: Some(DiagnosticSeverity::ERROR),
1629 message: "undefined variable 'A'".to_string(),
1630 source: Some("disk".to_string()),
1631 ..Default::default()
1632 },
1633 lsp::Diagnostic {
1634 range: lsp::Range::new(lsp::Position::new(1, 9), lsp::Position::new(1, 11)),
1635 severity: Some(DiagnosticSeverity::ERROR),
1636 message: "undefined variable 'BB'".to_string(),
1637 source: Some("disk".to_string()),
1638 ..Default::default()
1639 },
1640 lsp::Diagnostic {
1641 range: lsp::Range::new(lsp::Position::new(2, 9), lsp::Position::new(2, 12)),
1642 severity: Some(DiagnosticSeverity::ERROR),
1643 source: Some("disk".to_string()),
1644 message: "undefined variable 'CCC'".to_string(),
1645 ..Default::default()
1646 },
1647 ],
1648 });
1649
1650 // The diagnostics have moved down since they were created.
1651 cx.executor().run_until_parked();
1652 buffer.update(cx, |buffer, _| {
1653 assert_eq!(
1654 buffer
1655 .snapshot()
1656 .diagnostics_in_range::<_, Point>(Point::new(3, 0)..Point::new(5, 0), false)
1657 .collect::<Vec<_>>(),
1658 &[
1659 DiagnosticEntry {
1660 range: Point::new(3, 9)..Point::new(3, 11),
1661 diagnostic: Diagnostic {
1662 source: Some("disk".into()),
1663 severity: DiagnosticSeverity::ERROR,
1664 message: "undefined variable 'BB'".to_string(),
1665 is_disk_based: true,
1666 group_id: 1,
1667 is_primary: true,
1668 ..Default::default()
1669 },
1670 },
1671 DiagnosticEntry {
1672 range: Point::new(4, 9)..Point::new(4, 12),
1673 diagnostic: Diagnostic {
1674 source: Some("disk".into()),
1675 severity: DiagnosticSeverity::ERROR,
1676 message: "undefined variable 'CCC'".to_string(),
1677 is_disk_based: true,
1678 group_id: 2,
1679 is_primary: true,
1680 ..Default::default()
1681 }
1682 }
1683 ]
1684 );
1685 assert_eq!(
1686 chunks_with_diagnostics(buffer, 0..buffer.len()),
1687 [
1688 ("\n\nfn a() { ".to_string(), None),
1689 ("A".to_string(), Some(DiagnosticSeverity::ERROR)),
1690 (" }\nfn b() { ".to_string(), None),
1691 ("BB".to_string(), Some(DiagnosticSeverity::ERROR)),
1692 (" }\nfn c() { ".to_string(), None),
1693 ("CCC".to_string(), Some(DiagnosticSeverity::ERROR)),
1694 (" }\n".to_string(), None),
1695 ]
1696 );
1697 assert_eq!(
1698 chunks_with_diagnostics(buffer, Point::new(3, 10)..Point::new(4, 11)),
1699 [
1700 ("B".to_string(), Some(DiagnosticSeverity::ERROR)),
1701 (" }\nfn c() { ".to_string(), None),
1702 ("CC".to_string(), Some(DiagnosticSeverity::ERROR)),
1703 ]
1704 );
1705 });
1706
1707 // Ensure overlapping diagnostics are highlighted correctly.
1708 fake_server.notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
1709 uri: lsp::Uri::from_file_path("/dir/a.rs").unwrap().into(),
1710 version: Some(open_notification.text_document.version),
1711 diagnostics: vec![
1712 lsp::Diagnostic {
1713 range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
1714 severity: Some(DiagnosticSeverity::ERROR),
1715 message: "undefined variable 'A'".to_string(),
1716 source: Some("disk".to_string()),
1717 ..Default::default()
1718 },
1719 lsp::Diagnostic {
1720 range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 12)),
1721 severity: Some(DiagnosticSeverity::WARNING),
1722 message: "unreachable statement".to_string(),
1723 source: Some("disk".to_string()),
1724 ..Default::default()
1725 },
1726 ],
1727 });
1728
1729 cx.executor().run_until_parked();
1730 buffer.update(cx, |buffer, _| {
1731 assert_eq!(
1732 buffer
1733 .snapshot()
1734 .diagnostics_in_range::<_, Point>(Point::new(2, 0)..Point::new(3, 0), false)
1735 .collect::<Vec<_>>(),
1736 &[
1737 DiagnosticEntry {
1738 range: Point::new(2, 9)..Point::new(2, 12),
1739 diagnostic: Diagnostic {
1740 source: Some("disk".into()),
1741 severity: DiagnosticSeverity::WARNING,
1742 message: "unreachable statement".to_string(),
1743 is_disk_based: true,
1744 group_id: 4,
1745 is_primary: true,
1746 ..Default::default()
1747 }
1748 },
1749 DiagnosticEntry {
1750 range: Point::new(2, 9)..Point::new(2, 10),
1751 diagnostic: Diagnostic {
1752 source: Some("disk".into()),
1753 severity: DiagnosticSeverity::ERROR,
1754 message: "undefined variable 'A'".to_string(),
1755 is_disk_based: true,
1756 group_id: 3,
1757 is_primary: true,
1758 ..Default::default()
1759 },
1760 }
1761 ]
1762 );
1763 assert_eq!(
1764 chunks_with_diagnostics(buffer, Point::new(2, 0)..Point::new(3, 0)),
1765 [
1766 ("fn a() { ".to_string(), None),
1767 ("A".to_string(), Some(DiagnosticSeverity::ERROR)),
1768 (" }".to_string(), Some(DiagnosticSeverity::WARNING)),
1769 ("\n".to_string(), None),
1770 ]
1771 );
1772 assert_eq!(
1773 chunks_with_diagnostics(buffer, Point::new(2, 10)..Point::new(3, 0)),
1774 [
1775 (" }".to_string(), Some(DiagnosticSeverity::WARNING)),
1776 ("\n".to_string(), None),
1777 ]
1778 );
1779 });
1780
1781 // Keep editing the buffer and ensure disk-based diagnostics get translated according to the
1782 // changes since the last save.
1783 buffer.update(cx, |buffer, cx| {
1784 buffer.edit([(Point::new(2, 0)..Point::new(2, 0), " ")], None, cx);
1785 buffer.edit(
1786 [(Point::new(2, 8)..Point::new(2, 10), "(x: usize)")],
1787 None,
1788 cx,
1789 );
1790 buffer.edit([(Point::new(3, 10)..Point::new(3, 10), "xxx")], None, cx);
1791 });
1792 let change_notification_2 = fake_server
1793 .receive_notification::<lsp::notification::DidChangeTextDocument>()
1794 .await;
1795 assert!(
1796 change_notification_2.text_document.version > change_notification_1.text_document.version
1797 );
1798
1799 // Handle out-of-order diagnostics
1800 fake_server.notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
1801 uri: lsp::Uri::from_file_path("/dir/a.rs").unwrap().into(),
1802 version: Some(change_notification_2.text_document.version),
1803 diagnostics: vec![
1804 lsp::Diagnostic {
1805 range: lsp::Range::new(lsp::Position::new(1, 9), lsp::Position::new(1, 11)),
1806 severity: Some(DiagnosticSeverity::ERROR),
1807 message: "undefined variable 'BB'".to_string(),
1808 source: Some("disk".to_string()),
1809 ..Default::default()
1810 },
1811 lsp::Diagnostic {
1812 range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
1813 severity: Some(DiagnosticSeverity::WARNING),
1814 message: "undefined variable 'A'".to_string(),
1815 source: Some("disk".to_string()),
1816 ..Default::default()
1817 },
1818 ],
1819 });
1820
1821 cx.executor().run_until_parked();
1822 buffer.update(cx, |buffer, _| {
1823 assert_eq!(
1824 buffer
1825 .snapshot()
1826 .diagnostics_in_range::<_, Point>(0..buffer.len(), false)
1827 .collect::<Vec<_>>(),
1828 &[
1829 DiagnosticEntry {
1830 range: Point::new(2, 21)..Point::new(2, 22),
1831 diagnostic: Diagnostic {
1832 source: Some("disk".into()),
1833 severity: DiagnosticSeverity::WARNING,
1834 message: "undefined variable 'A'".to_string(),
1835 is_disk_based: true,
1836 group_id: 6,
1837 is_primary: true,
1838 ..Default::default()
1839 }
1840 },
1841 DiagnosticEntry {
1842 range: Point::new(3, 9)..Point::new(3, 14),
1843 diagnostic: Diagnostic {
1844 source: Some("disk".into()),
1845 severity: DiagnosticSeverity::ERROR,
1846 message: "undefined variable 'BB'".to_string(),
1847 is_disk_based: true,
1848 group_id: 5,
1849 is_primary: true,
1850 ..Default::default()
1851 },
1852 }
1853 ]
1854 );
1855 });
1856}
1857
1858#[gpui::test]
1859async fn test_empty_diagnostic_ranges(cx: &mut gpui::TestAppContext) {
1860 init_test(cx);
1861
1862 let text = concat!(
1863 "let one = ;\n", //
1864 "let two = \n",
1865 "let three = 3;\n",
1866 );
1867
1868 let fs = FakeFs::new(cx.executor());
1869 fs.insert_tree("/dir", json!({ "a.rs": text })).await;
1870
1871 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
1872 let buffer = project
1873 .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
1874 .await
1875 .unwrap();
1876
1877 project.update(cx, |project, cx| {
1878 project
1879 .update_buffer_diagnostics(
1880 &buffer,
1881 LanguageServerId(0),
1882 None,
1883 vec![
1884 DiagnosticEntry {
1885 range: Unclipped(PointUtf16::new(0, 10))..Unclipped(PointUtf16::new(0, 10)),
1886 diagnostic: Diagnostic {
1887 severity: DiagnosticSeverity::ERROR,
1888 message: "syntax error 1".to_string(),
1889 ..Default::default()
1890 },
1891 },
1892 DiagnosticEntry {
1893 range: Unclipped(PointUtf16::new(1, 10))..Unclipped(PointUtf16::new(1, 10)),
1894 diagnostic: Diagnostic {
1895 severity: DiagnosticSeverity::ERROR,
1896 message: "syntax error 2".to_string(),
1897 ..Default::default()
1898 },
1899 },
1900 ],
1901 cx,
1902 )
1903 .unwrap();
1904 });
1905
1906 // An empty range is extended forward to include the following character.
1907 // At the end of a line, an empty range is extended backward to include
1908 // the preceding character.
1909 buffer.update(cx, |buffer, _| {
1910 let chunks = chunks_with_diagnostics(buffer, 0..buffer.len());
1911 assert_eq!(
1912 chunks
1913 .iter()
1914 .map(|(s, d)| (s.as_str(), *d))
1915 .collect::<Vec<_>>(),
1916 &[
1917 ("let one = ", None),
1918 (";", Some(DiagnosticSeverity::ERROR)),
1919 ("\nlet two =", None),
1920 (" ", Some(DiagnosticSeverity::ERROR)),
1921 ("\nlet three = 3;\n", None)
1922 ]
1923 );
1924 });
1925}
1926
1927#[gpui::test]
1928async fn test_diagnostics_from_multiple_language_servers(cx: &mut gpui::TestAppContext) {
1929 init_test(cx);
1930
1931 let fs = FakeFs::new(cx.executor());
1932 fs.insert_tree("/dir", json!({ "a.rs": "one two three" }))
1933 .await;
1934
1935 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
1936
1937 project.update(cx, |project, cx| {
1938 project
1939 .update_diagnostic_entries(
1940 LanguageServerId(0),
1941 Path::new("/dir/a.rs").to_owned(),
1942 None,
1943 vec![DiagnosticEntry {
1944 range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 3)),
1945 diagnostic: Diagnostic {
1946 severity: DiagnosticSeverity::ERROR,
1947 is_primary: true,
1948 message: "syntax error a1".to_string(),
1949 ..Default::default()
1950 },
1951 }],
1952 cx,
1953 )
1954 .unwrap();
1955 project
1956 .update_diagnostic_entries(
1957 LanguageServerId(1),
1958 Path::new("/dir/a.rs").to_owned(),
1959 None,
1960 vec![DiagnosticEntry {
1961 range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 3)),
1962 diagnostic: Diagnostic {
1963 severity: DiagnosticSeverity::ERROR,
1964 is_primary: true,
1965 message: "syntax error b1".to_string(),
1966 ..Default::default()
1967 },
1968 }],
1969 cx,
1970 )
1971 .unwrap();
1972
1973 assert_eq!(
1974 project.diagnostic_summary(false, cx),
1975 DiagnosticSummary {
1976 error_count: 2,
1977 warning_count: 0,
1978 }
1979 );
1980 });
1981}
1982
1983#[gpui::test]
1984async fn test_edits_from_lsp2_with_past_version(cx: &mut gpui::TestAppContext) {
1985 init_test(cx);
1986
1987 let text = "
1988 fn a() {
1989 f1();
1990 }
1991 fn b() {
1992 f2();
1993 }
1994 fn c() {
1995 f3();
1996 }
1997 "
1998 .unindent();
1999
2000 let fs = FakeFs::new(cx.executor());
2001 fs.insert_tree(
2002 "/dir",
2003 json!({
2004 "a.rs": text.clone(),
2005 }),
2006 )
2007 .await;
2008
2009 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
2010
2011 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
2012 language_registry.add(rust_lang());
2013 let mut fake_servers =
2014 language_registry.register_fake_lsp_adapter("Rust", FakeLspAdapter::default());
2015
2016 let buffer = project
2017 .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
2018 .await
2019 .unwrap();
2020
2021 let mut fake_server = fake_servers.next().await.unwrap();
2022 let lsp_document_version = fake_server
2023 .receive_notification::<lsp::notification::DidOpenTextDocument>()
2024 .await
2025 .text_document
2026 .version;
2027
2028 // Simulate editing the buffer after the language server computes some edits.
2029 buffer.update(cx, |buffer, cx| {
2030 buffer.edit(
2031 [(
2032 Point::new(0, 0)..Point::new(0, 0),
2033 "// above first function\n",
2034 )],
2035 None,
2036 cx,
2037 );
2038 buffer.edit(
2039 [(
2040 Point::new(2, 0)..Point::new(2, 0),
2041 " // inside first function\n",
2042 )],
2043 None,
2044 cx,
2045 );
2046 buffer.edit(
2047 [(
2048 Point::new(6, 4)..Point::new(6, 4),
2049 "// inside second function ",
2050 )],
2051 None,
2052 cx,
2053 );
2054
2055 assert_eq!(
2056 buffer.text(),
2057 "
2058 // above first function
2059 fn a() {
2060 // inside first function
2061 f1();
2062 }
2063 fn b() {
2064 // inside second function f2();
2065 }
2066 fn c() {
2067 f3();
2068 }
2069 "
2070 .unindent()
2071 );
2072 });
2073
2074 let edits = project
2075 .update(cx, |project, cx| {
2076 project.edits_from_lsp(
2077 &buffer,
2078 vec![
2079 // replace body of first function
2080 lsp::TextEdit {
2081 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(3, 0)),
2082 new_text: "
2083 fn a() {
2084 f10();
2085 }
2086 "
2087 .unindent(),
2088 },
2089 // edit inside second function
2090 lsp::TextEdit {
2091 range: lsp::Range::new(lsp::Position::new(4, 6), lsp::Position::new(4, 6)),
2092 new_text: "00".into(),
2093 },
2094 // edit inside third function via two distinct edits
2095 lsp::TextEdit {
2096 range: lsp::Range::new(lsp::Position::new(7, 5), lsp::Position::new(7, 5)),
2097 new_text: "4000".into(),
2098 },
2099 lsp::TextEdit {
2100 range: lsp::Range::new(lsp::Position::new(7, 5), lsp::Position::new(7, 6)),
2101 new_text: "".into(),
2102 },
2103 ],
2104 LanguageServerId(0),
2105 Some(lsp_document_version),
2106 cx,
2107 )
2108 })
2109 .await
2110 .unwrap();
2111
2112 buffer.update(cx, |buffer, cx| {
2113 for (range, new_text) in edits {
2114 buffer.edit([(range, new_text)], None, cx);
2115 }
2116 assert_eq!(
2117 buffer.text(),
2118 "
2119 // above first function
2120 fn a() {
2121 // inside first function
2122 f10();
2123 }
2124 fn b() {
2125 // inside second function f200();
2126 }
2127 fn c() {
2128 f4000();
2129 }
2130 "
2131 .unindent()
2132 );
2133 });
2134}
2135
2136#[gpui::test]
2137async fn test_edits_from_lsp2_with_edits_on_adjacent_lines(cx: &mut gpui::TestAppContext) {
2138 init_test(cx);
2139
2140 let text = "
2141 use a::b;
2142 use a::c;
2143
2144 fn f() {
2145 b();
2146 c();
2147 }
2148 "
2149 .unindent();
2150
2151 let fs = FakeFs::new(cx.executor());
2152 fs.insert_tree(
2153 "/dir",
2154 json!({
2155 "a.rs": text.clone(),
2156 }),
2157 )
2158 .await;
2159
2160 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
2161 let buffer = project
2162 .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
2163 .await
2164 .unwrap();
2165
2166 // Simulate the language server sending us a small edit in the form of a very large diff.
2167 // Rust-analyzer does this when performing a merge-imports code action.
2168 let edits = project
2169 .update(cx, |project, cx| {
2170 project.edits_from_lsp(
2171 &buffer,
2172 [
2173 // Replace the first use statement without editing the semicolon.
2174 lsp::TextEdit {
2175 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 8)),
2176 new_text: "a::{b, c}".into(),
2177 },
2178 // Reinsert the remainder of the file between the semicolon and the final
2179 // newline of the file.
2180 lsp::TextEdit {
2181 range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 9)),
2182 new_text: "\n\n".into(),
2183 },
2184 lsp::TextEdit {
2185 range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 9)),
2186 new_text: "
2187 fn f() {
2188 b();
2189 c();
2190 }"
2191 .unindent(),
2192 },
2193 // Delete everything after the first newline of the file.
2194 lsp::TextEdit {
2195 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(7, 0)),
2196 new_text: "".into(),
2197 },
2198 ],
2199 LanguageServerId(0),
2200 None,
2201 cx,
2202 )
2203 })
2204 .await
2205 .unwrap();
2206
2207 buffer.update(cx, |buffer, cx| {
2208 let edits = edits
2209 .into_iter()
2210 .map(|(range, text)| {
2211 (
2212 range.start.to_point(buffer)..range.end.to_point(buffer),
2213 text,
2214 )
2215 })
2216 .collect::<Vec<_>>();
2217
2218 assert_eq!(
2219 edits,
2220 [
2221 (Point::new(0, 4)..Point::new(0, 8), "a::{b, c}".into()),
2222 (Point::new(1, 0)..Point::new(2, 0), "".into())
2223 ]
2224 );
2225
2226 for (range, new_text) in edits {
2227 buffer.edit([(range, new_text)], None, cx);
2228 }
2229 assert_eq!(
2230 buffer.text(),
2231 "
2232 use a::{b, c};
2233
2234 fn f() {
2235 b();
2236 c();
2237 }
2238 "
2239 .unindent()
2240 );
2241 });
2242}
2243
2244#[gpui::test]
2245async fn test_invalid_edits_from_lsp2(cx: &mut gpui::TestAppContext) {
2246 init_test(cx);
2247
2248 let text = "
2249 use a::b;
2250 use a::c;
2251
2252 fn f() {
2253 b();
2254 c();
2255 }
2256 "
2257 .unindent();
2258
2259 let fs = FakeFs::new(cx.executor());
2260 fs.insert_tree(
2261 "/dir",
2262 json!({
2263 "a.rs": text.clone(),
2264 }),
2265 )
2266 .await;
2267
2268 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
2269 let buffer = project
2270 .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
2271 .await
2272 .unwrap();
2273
2274 // Simulate the language server sending us edits in a non-ordered fashion,
2275 // with ranges sometimes being inverted or pointing to invalid locations.
2276 let edits = project
2277 .update(cx, |project, cx| {
2278 project.edits_from_lsp(
2279 &buffer,
2280 [
2281 lsp::TextEdit {
2282 range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 9)),
2283 new_text: "\n\n".into(),
2284 },
2285 lsp::TextEdit {
2286 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 4)),
2287 new_text: "a::{b, c}".into(),
2288 },
2289 lsp::TextEdit {
2290 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(99, 0)),
2291 new_text: "".into(),
2292 },
2293 lsp::TextEdit {
2294 range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 9)),
2295 new_text: "
2296 fn f() {
2297 b();
2298 c();
2299 }"
2300 .unindent(),
2301 },
2302 ],
2303 LanguageServerId(0),
2304 None,
2305 cx,
2306 )
2307 })
2308 .await
2309 .unwrap();
2310
2311 buffer.update(cx, |buffer, cx| {
2312 let edits = edits
2313 .into_iter()
2314 .map(|(range, text)| {
2315 (
2316 range.start.to_point(buffer)..range.end.to_point(buffer),
2317 text,
2318 )
2319 })
2320 .collect::<Vec<_>>();
2321
2322 assert_eq!(
2323 edits,
2324 [
2325 (Point::new(0, 4)..Point::new(0, 8), "a::{b, c}".into()),
2326 (Point::new(1, 0)..Point::new(2, 0), "".into())
2327 ]
2328 );
2329
2330 for (range, new_text) in edits {
2331 buffer.edit([(range, new_text)], None, cx);
2332 }
2333 assert_eq!(
2334 buffer.text(),
2335 "
2336 use a::{b, c};
2337
2338 fn f() {
2339 b();
2340 c();
2341 }
2342 "
2343 .unindent()
2344 );
2345 });
2346}
2347
2348fn chunks_with_diagnostics<T: ToOffset + ToPoint>(
2349 buffer: &Buffer,
2350 range: Range<T>,
2351) -> Vec<(String, Option<DiagnosticSeverity>)> {
2352 let mut chunks: Vec<(String, Option<DiagnosticSeverity>)> = Vec::new();
2353 for chunk in buffer.snapshot().chunks(range, true) {
2354 if chunks.last().map_or(false, |prev_chunk| {
2355 prev_chunk.1 == chunk.diagnostic_severity
2356 }) {
2357 chunks.last_mut().unwrap().0.push_str(chunk.text);
2358 } else {
2359 chunks.push((chunk.text.to_string(), chunk.diagnostic_severity));
2360 }
2361 }
2362 chunks
2363}
2364
2365#[gpui::test(iterations = 10)]
2366async fn test_definition(cx: &mut gpui::TestAppContext) {
2367 init_test(cx);
2368
2369 let fs = FakeFs::new(cx.executor());
2370 fs.insert_tree(
2371 "/dir",
2372 json!({
2373 "a.rs": "const fn a() { A }",
2374 "b.rs": "const y: i32 = crate::a()",
2375 }),
2376 )
2377 .await;
2378
2379 let project = Project::test(fs, ["/dir/b.rs".as_ref()], cx).await;
2380
2381 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
2382 language_registry.add(rust_lang());
2383 let mut fake_servers =
2384 language_registry.register_fake_lsp_adapter("Rust", FakeLspAdapter::default());
2385
2386 let buffer = project
2387 .update(cx, |project, cx| project.open_local_buffer("/dir/b.rs", cx))
2388 .await
2389 .unwrap();
2390
2391 let fake_server = fake_servers.next().await.unwrap();
2392 fake_server.handle_request::<lsp::request::GotoDefinition, _, _>(|params, _| async move {
2393 let params = params.text_document_position_params;
2394 assert_eq!(
2395 Uri::from(params.text_document.uri).to_file_path().unwrap(),
2396 Path::new("/dir/b.rs"),
2397 );
2398 assert_eq!(params.position, lsp::Position::new(0, 22));
2399
2400 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
2401 lsp::Location::new(
2402 lsp::Uri::from_file_path("/dir/a.rs").unwrap().into(),
2403 lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
2404 ),
2405 )))
2406 });
2407
2408 let mut definitions = project
2409 .update(cx, |project, cx| project.definition(&buffer, 22, cx))
2410 .await
2411 .unwrap();
2412
2413 // Assert no new language server started
2414 cx.executor().run_until_parked();
2415 assert!(fake_servers.try_next().is_err());
2416
2417 assert_eq!(definitions.len(), 1);
2418 let definition = definitions.pop().unwrap();
2419 cx.update(|cx| {
2420 let target_buffer = definition.target.buffer.read(cx);
2421 assert_eq!(
2422 target_buffer
2423 .file()
2424 .unwrap()
2425 .as_local()
2426 .unwrap()
2427 .abs_path(cx),
2428 Path::new("/dir/a.rs"),
2429 );
2430 assert_eq!(definition.target.range.to_offset(target_buffer), 9..10);
2431 assert_eq!(
2432 list_worktrees(&project, cx),
2433 [("/dir/a.rs".as_ref(), false), ("/dir/b.rs".as_ref(), true)],
2434 );
2435
2436 drop(definition);
2437 });
2438 cx.update(|cx| {
2439 assert_eq!(list_worktrees(&project, cx), [("/dir/b.rs".as_ref(), true)]);
2440 });
2441
2442 fn list_worktrees<'a>(
2443 project: &'a Model<Project>,
2444 cx: &'a AppContext,
2445 ) -> Vec<(&'a Path, bool)> {
2446 project
2447 .read(cx)
2448 .worktrees()
2449 .map(|worktree| {
2450 let worktree = worktree.read(cx);
2451 (
2452 worktree.as_local().unwrap().abs_path().as_ref(),
2453 worktree.is_visible(),
2454 )
2455 })
2456 .collect::<Vec<_>>()
2457 }
2458}
2459
2460#[gpui::test]
2461async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) {
2462 init_test(cx);
2463
2464 let fs = FakeFs::new(cx.executor());
2465 fs.insert_tree(
2466 "/dir",
2467 json!({
2468 "a.ts": "",
2469 }),
2470 )
2471 .await;
2472
2473 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
2474
2475 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
2476 language_registry.add(typescript_lang());
2477 let mut fake_language_servers = language_registry.register_fake_lsp_adapter(
2478 "TypeScript",
2479 FakeLspAdapter {
2480 capabilities: lsp::ServerCapabilities {
2481 completion_provider: Some(lsp::CompletionOptions {
2482 trigger_characters: Some(vec![":".to_string()]),
2483 ..Default::default()
2484 }),
2485 ..Default::default()
2486 },
2487 ..Default::default()
2488 },
2489 );
2490
2491 let buffer = project
2492 .update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
2493 .await
2494 .unwrap();
2495
2496 let fake_server = fake_language_servers.next().await.unwrap();
2497
2498 let text = "let a = b.fqn";
2499 buffer.update(cx, |buffer, cx| buffer.set_text(text, cx));
2500 let completions = project.update(cx, |project, cx| {
2501 project.completions(&buffer, text.len(), DEFAULT_COMPLETION_CONTEXT, cx)
2502 });
2503
2504 fake_server
2505 .handle_request::<lsp::request::Completion, _, _>(|_, _| async move {
2506 Ok(Some(lsp::CompletionResponse::Array(vec![
2507 lsp::CompletionItem {
2508 label: "fullyQualifiedName?".into(),
2509 insert_text: Some("fullyQualifiedName".into()),
2510 ..Default::default()
2511 },
2512 ])))
2513 })
2514 .next()
2515 .await;
2516 let completions = completions.await.unwrap();
2517 let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
2518 assert_eq!(completions.len(), 1);
2519 assert_eq!(completions[0].new_text, "fullyQualifiedName");
2520 assert_eq!(
2521 completions[0].old_range.to_offset(&snapshot),
2522 text.len() - 3..text.len()
2523 );
2524
2525 let text = "let a = \"atoms/cmp\"";
2526 buffer.update(cx, |buffer, cx| buffer.set_text(text, cx));
2527 let completions = project.update(cx, |project, cx| {
2528 project.completions(&buffer, text.len() - 1, DEFAULT_COMPLETION_CONTEXT, cx)
2529 });
2530
2531 fake_server
2532 .handle_request::<lsp::request::Completion, _, _>(|_, _| async move {
2533 Ok(Some(lsp::CompletionResponse::Array(vec![
2534 lsp::CompletionItem {
2535 label: "component".into(),
2536 ..Default::default()
2537 },
2538 ])))
2539 })
2540 .next()
2541 .await;
2542 let completions = completions.await.unwrap();
2543 let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
2544 assert_eq!(completions.len(), 1);
2545 assert_eq!(completions[0].new_text, "component");
2546 assert_eq!(
2547 completions[0].old_range.to_offset(&snapshot),
2548 text.len() - 4..text.len() - 1
2549 );
2550}
2551
2552#[gpui::test]
2553async fn test_completions_with_carriage_returns(cx: &mut gpui::TestAppContext) {
2554 init_test(cx);
2555
2556 let fs = FakeFs::new(cx.executor());
2557 fs.insert_tree(
2558 "/dir",
2559 json!({
2560 "a.ts": "",
2561 }),
2562 )
2563 .await;
2564
2565 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
2566
2567 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
2568 language_registry.add(typescript_lang());
2569 let mut fake_language_servers = language_registry.register_fake_lsp_adapter(
2570 "TypeScript",
2571 FakeLspAdapter {
2572 capabilities: lsp::ServerCapabilities {
2573 completion_provider: Some(lsp::CompletionOptions {
2574 trigger_characters: Some(vec![":".to_string()]),
2575 ..Default::default()
2576 }),
2577 ..Default::default()
2578 },
2579 ..Default::default()
2580 },
2581 );
2582
2583 let buffer = project
2584 .update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
2585 .await
2586 .unwrap();
2587
2588 let fake_server = fake_language_servers.next().await.unwrap();
2589
2590 let text = "let a = b.fqn";
2591 buffer.update(cx, |buffer, cx| buffer.set_text(text, cx));
2592 let completions = project.update(cx, |project, cx| {
2593 project.completions(&buffer, text.len(), DEFAULT_COMPLETION_CONTEXT, cx)
2594 });
2595
2596 fake_server
2597 .handle_request::<lsp::request::Completion, _, _>(|_, _| async move {
2598 Ok(Some(lsp::CompletionResponse::Array(vec![
2599 lsp::CompletionItem {
2600 label: "fullyQualifiedName?".into(),
2601 insert_text: Some("fully\rQualified\r\nName".into()),
2602 ..Default::default()
2603 },
2604 ])))
2605 })
2606 .next()
2607 .await;
2608 let completions = completions.await.unwrap();
2609 assert_eq!(completions.len(), 1);
2610 assert_eq!(completions[0].new_text, "fully\nQualified\nName");
2611}
2612
2613#[gpui::test(iterations = 10)]
2614async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) {
2615 init_test(cx);
2616
2617 let fs = FakeFs::new(cx.executor());
2618 fs.insert_tree(
2619 "/dir",
2620 json!({
2621 "a.ts": "a",
2622 }),
2623 )
2624 .await;
2625
2626 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
2627
2628 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
2629 language_registry.add(typescript_lang());
2630 let mut fake_language_servers = language_registry.register_fake_lsp_adapter(
2631 "TypeScript",
2632 FakeLspAdapter {
2633 capabilities: lsp::ServerCapabilities {
2634 code_action_provider: Some(lsp::CodeActionProviderCapability::Options(
2635 lsp::CodeActionOptions {
2636 resolve_provider: Some(true),
2637 ..lsp::CodeActionOptions::default()
2638 },
2639 )),
2640 ..lsp::ServerCapabilities::default()
2641 },
2642 ..FakeLspAdapter::default()
2643 },
2644 );
2645
2646 let buffer = project
2647 .update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
2648 .await
2649 .unwrap();
2650
2651 let fake_server = fake_language_servers.next().await.unwrap();
2652
2653 // Language server returns code actions that contain commands, and not edits.
2654 let actions = project.update(cx, |project, cx| project.code_actions(&buffer, 0..0, cx));
2655 fake_server
2656 .handle_request::<lsp::request::CodeActionRequest, _, _>(|_, _| async move {
2657 Ok(Some(vec![
2658 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
2659 title: "The code action".into(),
2660 data: Some(serde_json::json!({
2661 "command": "_the/command",
2662 })),
2663 ..lsp::CodeAction::default()
2664 }),
2665 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
2666 title: "two".into(),
2667 ..lsp::CodeAction::default()
2668 }),
2669 ]))
2670 })
2671 .next()
2672 .await;
2673
2674 let action = actions.await[0].clone();
2675 let apply = project.update(cx, |project, cx| {
2676 project.apply_code_action(buffer.clone(), action, true, cx)
2677 });
2678
2679 // Resolving the code action does not populate its edits. In absence of
2680 // edits, we must execute the given command.
2681 fake_server.handle_request::<lsp::request::CodeActionResolveRequest, _, _>(
2682 |mut action, _| async move {
2683 if action.data.is_some() {
2684 action.command = Some(lsp::Command {
2685 title: "The command".into(),
2686 command: "_the/command".into(),
2687 arguments: Some(vec![json!("the-argument")]),
2688 });
2689 }
2690 Ok(action)
2691 },
2692 );
2693
2694 // While executing the command, the language server sends the editor
2695 // a `workspaceEdit` request.
2696 fake_server
2697 .handle_request::<lsp::request::ExecuteCommand, _, _>({
2698 let fake = fake_server.clone();
2699 move |params, _| {
2700 assert_eq!(params.command, "_the/command");
2701 let fake = fake.clone();
2702 async move {
2703 fake.server
2704 .request::<lsp::request::ApplyWorkspaceEdit>(
2705 lsp::ApplyWorkspaceEditParams {
2706 label: None,
2707 edit: lsp::WorkspaceEdit {
2708 changes: Some(
2709 [(
2710 lsp::Uri::from_file_path("/dir/a.ts").unwrap().into(),
2711 vec![lsp::TextEdit {
2712 range: lsp::Range::new(
2713 lsp::Position::new(0, 0),
2714 lsp::Position::new(0, 0),
2715 ),
2716 new_text: "X".into(),
2717 }],
2718 )]
2719 .into_iter()
2720 .collect(),
2721 ),
2722 ..Default::default()
2723 },
2724 },
2725 )
2726 .await
2727 .unwrap();
2728 Ok(Some(json!(null)))
2729 }
2730 }
2731 })
2732 .next()
2733 .await;
2734
2735 // Applying the code action returns a project transaction containing the edits
2736 // sent by the language server in its `workspaceEdit` request.
2737 let transaction = apply.await.unwrap();
2738 assert!(transaction.0.contains_key(&buffer));
2739 buffer.update(cx, |buffer, cx| {
2740 assert_eq!(buffer.text(), "Xa");
2741 buffer.undo(cx);
2742 assert_eq!(buffer.text(), "a");
2743 });
2744}
2745
2746#[gpui::test(iterations = 10)]
2747async fn test_save_file(cx: &mut gpui::TestAppContext) {
2748 init_test(cx);
2749
2750 let fs = FakeFs::new(cx.executor());
2751 fs.insert_tree(
2752 "/dir",
2753 json!({
2754 "file1": "the old contents",
2755 }),
2756 )
2757 .await;
2758
2759 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
2760 let buffer = project
2761 .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
2762 .await
2763 .unwrap();
2764 buffer.update(cx, |buffer, cx| {
2765 assert_eq!(buffer.text(), "the old contents");
2766 buffer.edit([(0..0, "a line of text.\n".repeat(10 * 1024))], None, cx);
2767 });
2768
2769 project
2770 .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
2771 .await
2772 .unwrap();
2773
2774 let new_text = fs.load(Path::new("/dir/file1")).await.unwrap();
2775 assert_eq!(new_text, buffer.update(cx, |buffer, _| buffer.text()));
2776}
2777
2778#[gpui::test(iterations = 30)]
2779async fn test_file_changes_multiple_times_on_disk(cx: &mut gpui::TestAppContext) {
2780 init_test(cx);
2781
2782 let fs = FakeFs::new(cx.executor().clone());
2783 fs.insert_tree(
2784 "/dir",
2785 json!({
2786 "file1": "the original contents",
2787 }),
2788 )
2789 .await;
2790
2791 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
2792 let worktree = project.read_with(cx, |project, _| project.worktrees().next().unwrap());
2793 let buffer = project
2794 .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
2795 .await
2796 .unwrap();
2797
2798 // Simulate buffer diffs being slow, so that they don't complete before
2799 // the next file change occurs.
2800 cx.executor().deprioritize(*language::BUFFER_DIFF_TASK);
2801
2802 // Change the buffer's file on disk, and then wait for the file change
2803 // to be detected by the worktree, so that the buffer starts reloading.
2804 fs.save(
2805 "/dir/file1".as_ref(),
2806 &"the first contents".into(),
2807 Default::default(),
2808 )
2809 .await
2810 .unwrap();
2811 worktree.next_event(cx).await;
2812
2813 // Change the buffer's file again. Depending on the random seed, the
2814 // previous file change may still be in progress.
2815 fs.save(
2816 "/dir/file1".as_ref(),
2817 &"the second contents".into(),
2818 Default::default(),
2819 )
2820 .await
2821 .unwrap();
2822 worktree.next_event(cx).await;
2823
2824 cx.executor().run_until_parked();
2825 let on_disk_text = fs.load(Path::new("/dir/file1")).await.unwrap();
2826 buffer.read_with(cx, |buffer, _| {
2827 assert_eq!(buffer.text(), on_disk_text);
2828 assert!(!buffer.is_dirty(), "buffer should not be dirty");
2829 assert!(!buffer.has_conflict(), "buffer should not be dirty");
2830 });
2831}
2832
2833#[gpui::test(iterations = 30)]
2834async fn test_edit_buffer_while_it_reloads(cx: &mut gpui::TestAppContext) {
2835 init_test(cx);
2836
2837 let fs = FakeFs::new(cx.executor().clone());
2838 fs.insert_tree(
2839 "/dir",
2840 json!({
2841 "file1": "the original contents",
2842 }),
2843 )
2844 .await;
2845
2846 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
2847 let worktree = project.read_with(cx, |project, _| project.worktrees().next().unwrap());
2848 let buffer = project
2849 .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
2850 .await
2851 .unwrap();
2852
2853 // Simulate buffer diffs being slow, so that they don't complete before
2854 // the next file change occurs.
2855 cx.executor().deprioritize(*language::BUFFER_DIFF_TASK);
2856
2857 // Change the buffer's file on disk, and then wait for the file change
2858 // to be detected by the worktree, so that the buffer starts reloading.
2859 fs.save(
2860 "/dir/file1".as_ref(),
2861 &"the first contents".into(),
2862 Default::default(),
2863 )
2864 .await
2865 .unwrap();
2866 worktree.next_event(cx).await;
2867
2868 cx.executor()
2869 .spawn(cx.executor().simulate_random_delay())
2870 .await;
2871
2872 // Perform a noop edit, causing the buffer's version to increase.
2873 buffer.update(cx, |buffer, cx| {
2874 buffer.edit([(0..0, " ")], None, cx);
2875 buffer.undo(cx);
2876 });
2877
2878 cx.executor().run_until_parked();
2879 let on_disk_text = fs.load(Path::new("/dir/file1")).await.unwrap();
2880 buffer.read_with(cx, |buffer, _| {
2881 let buffer_text = buffer.text();
2882 if buffer_text == on_disk_text {
2883 assert!(
2884 !buffer.is_dirty() && !buffer.has_conflict(),
2885 "buffer shouldn't be dirty. text: {buffer_text:?}, disk text: {on_disk_text:?}",
2886 );
2887 }
2888 // If the file change occurred while the buffer was processing the first
2889 // change, the buffer will be in a conflicting state.
2890 else {
2891 assert!(buffer.is_dirty(), "buffer should report that it is dirty. text: {buffer_text:?}, disk text: {on_disk_text:?}");
2892 assert!(buffer.has_conflict(), "buffer should report that it is dirty. text: {buffer_text:?}, disk text: {on_disk_text:?}");
2893 }
2894 });
2895}
2896
2897#[gpui::test]
2898async fn test_save_in_single_file_worktree(cx: &mut gpui::TestAppContext) {
2899 init_test(cx);
2900
2901 let fs = FakeFs::new(cx.executor());
2902 fs.insert_tree(
2903 "/dir",
2904 json!({
2905 "file1": "the old contents",
2906 }),
2907 )
2908 .await;
2909
2910 let project = Project::test(fs.clone(), ["/dir/file1".as_ref()], cx).await;
2911 let buffer = project
2912 .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
2913 .await
2914 .unwrap();
2915 buffer.update(cx, |buffer, cx| {
2916 buffer.edit([(0..0, "a line of text.\n".repeat(10 * 1024))], None, cx);
2917 });
2918
2919 project
2920 .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
2921 .await
2922 .unwrap();
2923
2924 let new_text = fs.load(Path::new("/dir/file1")).await.unwrap();
2925 assert_eq!(new_text, buffer.update(cx, |buffer, _| buffer.text()));
2926}
2927
2928#[gpui::test]
2929async fn test_save_as(cx: &mut gpui::TestAppContext) {
2930 init_test(cx);
2931
2932 let fs = FakeFs::new(cx.executor());
2933 fs.insert_tree("/dir", json!({})).await;
2934
2935 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
2936
2937 let languages = project.update(cx, |project, _| project.languages().clone());
2938 languages.add(rust_lang());
2939
2940 let buffer = project.update(cx, |project, cx| project.create_local_buffer("", None, cx));
2941 buffer.update(cx, |buffer, cx| {
2942 buffer.edit([(0..0, "abc")], None, cx);
2943 assert!(buffer.is_dirty());
2944 assert!(!buffer.has_conflict());
2945 assert_eq!(buffer.language().unwrap().name().as_ref(), "Plain Text");
2946 });
2947 project
2948 .update(cx, |project, cx| {
2949 let worktree_id = project.worktrees().next().unwrap().read(cx).id();
2950 let path = ProjectPath {
2951 worktree_id,
2952 path: Arc::from(Path::new("file1.rs")),
2953 };
2954 project.save_buffer_as(buffer.clone(), path, cx)
2955 })
2956 .await
2957 .unwrap();
2958 assert_eq!(fs.load(Path::new("/dir/file1.rs")).await.unwrap(), "abc");
2959
2960 cx.executor().run_until_parked();
2961 buffer.update(cx, |buffer, cx| {
2962 assert_eq!(
2963 buffer.file().unwrap().full_path(cx),
2964 Path::new("dir/file1.rs")
2965 );
2966 assert!(!buffer.is_dirty());
2967 assert!(!buffer.has_conflict());
2968 assert_eq!(buffer.language().unwrap().name().as_ref(), "Rust");
2969 });
2970
2971 let opened_buffer = project
2972 .update(cx, |project, cx| {
2973 project.open_local_buffer("/dir/file1.rs", cx)
2974 })
2975 .await
2976 .unwrap();
2977 assert_eq!(opened_buffer, buffer);
2978}
2979
2980// This test is currently disabled on Linux as it fails fails pretty consistently on that target.
2981#[cfg(not(target_os = "linux"))]
2982#[gpui::test(retries = 5)]
2983async fn test_rescan_and_remote_updates(cx: &mut gpui::TestAppContext) {
2984 use worktree::WorktreeModelHandle as _;
2985
2986 init_test(cx);
2987 cx.executor().allow_parking();
2988
2989 let dir = temp_tree(json!({
2990 "a": {
2991 "file1": "",
2992 "file2": "",
2993 "file3": "",
2994 },
2995 "b": {
2996 "c": {
2997 "file4": "",
2998 "file5": "",
2999 }
3000 }
3001 }));
3002
3003 let project = Project::test(Arc::new(RealFs::default()), [dir.path()], cx).await;
3004
3005 let buffer_for_path = |path: &'static str, cx: &mut gpui::TestAppContext| {
3006 let buffer = project.update(cx, |p, cx| p.open_local_buffer(dir.path().join(path), cx));
3007 async move { buffer.await.unwrap() }
3008 };
3009 let id_for_path = |path: &'static str, cx: &mut gpui::TestAppContext| {
3010 project.update(cx, |project, cx| {
3011 let tree = project.worktrees().next().unwrap();
3012 tree.read(cx)
3013 .entry_for_path(path)
3014 .unwrap_or_else(|| panic!("no entry for path {}", path))
3015 .id
3016 })
3017 };
3018
3019 let buffer2 = buffer_for_path("a/file2", cx).await;
3020 let buffer3 = buffer_for_path("a/file3", cx).await;
3021 let buffer4 = buffer_for_path("b/c/file4", cx).await;
3022 let buffer5 = buffer_for_path("b/c/file5", cx).await;
3023
3024 let file2_id = id_for_path("a/file2", cx);
3025 let file3_id = id_for_path("a/file3", cx);
3026 let file4_id = id_for_path("b/c/file4", cx);
3027
3028 // Create a remote copy of this worktree.
3029 let tree = project.update(cx, |project, _| project.worktrees().next().unwrap());
3030 let metadata = tree.update(cx, |tree, _| tree.metadata_proto());
3031
3032 let updates = Arc::new(Mutex::new(Vec::new()));
3033 tree.update(cx, |tree, cx| {
3034 let updates = updates.clone();
3035 tree.observe_updates(0, cx, move |update| {
3036 updates.lock().push(update);
3037 async { true }
3038 });
3039 });
3040
3041 let remote = cx.update(|cx| {
3042 Worktree::remote(
3043 0,
3044 1,
3045 metadata,
3046 Box::new(CollabRemoteWorktreeClient(project.read(cx).client())),
3047 cx,
3048 )
3049 });
3050
3051 cx.executor().run_until_parked();
3052
3053 cx.update(|cx| {
3054 assert!(!buffer2.read(cx).is_dirty());
3055 assert!(!buffer3.read(cx).is_dirty());
3056 assert!(!buffer4.read(cx).is_dirty());
3057 assert!(!buffer5.read(cx).is_dirty());
3058 });
3059
3060 // Rename and delete files and directories.
3061 tree.flush_fs_events(cx).await;
3062 std::fs::rename(dir.path().join("a/file3"), dir.path().join("b/c/file3")).unwrap();
3063 std::fs::remove_file(dir.path().join("b/c/file5")).unwrap();
3064 std::fs::rename(dir.path().join("b/c"), dir.path().join("d")).unwrap();
3065 std::fs::rename(dir.path().join("a/file2"), dir.path().join("a/file2.new")).unwrap();
3066 tree.flush_fs_events(cx).await;
3067
3068 let expected_paths = vec![
3069 "a",
3070 "a/file1",
3071 "a/file2.new",
3072 "b",
3073 "d",
3074 "d/file3",
3075 "d/file4",
3076 ];
3077
3078 cx.update(|app| {
3079 assert_eq!(
3080 tree.read(app)
3081 .paths()
3082 .map(|p| p.to_str().unwrap())
3083 .collect::<Vec<_>>(),
3084 expected_paths
3085 );
3086 });
3087
3088 assert_eq!(id_for_path("a/file2.new", cx), file2_id);
3089 assert_eq!(id_for_path("d/file3", cx), file3_id);
3090 assert_eq!(id_for_path("d/file4", cx), file4_id);
3091
3092 cx.update(|cx| {
3093 assert_eq!(
3094 buffer2.read(cx).file().unwrap().path().as_ref(),
3095 Path::new("a/file2.new")
3096 );
3097 assert_eq!(
3098 buffer3.read(cx).file().unwrap().path().as_ref(),
3099 Path::new("d/file3")
3100 );
3101 assert_eq!(
3102 buffer4.read(cx).file().unwrap().path().as_ref(),
3103 Path::new("d/file4")
3104 );
3105 assert_eq!(
3106 buffer5.read(cx).file().unwrap().path().as_ref(),
3107 Path::new("b/c/file5")
3108 );
3109
3110 assert!(!buffer2.read(cx).file().unwrap().is_deleted());
3111 assert!(!buffer3.read(cx).file().unwrap().is_deleted());
3112 assert!(!buffer4.read(cx).file().unwrap().is_deleted());
3113 assert!(buffer5.read(cx).file().unwrap().is_deleted());
3114 });
3115
3116 // Update the remote worktree. Check that it becomes consistent with the
3117 // local worktree.
3118 cx.executor().run_until_parked();
3119
3120 remote.update(cx, |remote, _| {
3121 for update in updates.lock().drain(..) {
3122 remote.as_remote_mut().unwrap().update_from_remote(update);
3123 }
3124 });
3125 cx.executor().run_until_parked();
3126 remote.update(cx, |remote, _| {
3127 assert_eq!(
3128 remote
3129 .paths()
3130 .map(|p| p.to_str().unwrap())
3131 .collect::<Vec<_>>(),
3132 expected_paths
3133 );
3134 });
3135}
3136
3137#[gpui::test(iterations = 10)]
3138async fn test_buffer_identity_across_renames(cx: &mut gpui::TestAppContext) {
3139 init_test(cx);
3140
3141 let fs = FakeFs::new(cx.executor());
3142 fs.insert_tree(
3143 "/dir",
3144 json!({
3145 "a": {
3146 "file1": "",
3147 }
3148 }),
3149 )
3150 .await;
3151
3152 let project = Project::test(fs, [Path::new("/dir")], cx).await;
3153 let tree = project.update(cx, |project, _| project.worktrees().next().unwrap());
3154 let tree_id = tree.update(cx, |tree, _| tree.id());
3155
3156 let id_for_path = |path: &'static str, cx: &mut gpui::TestAppContext| {
3157 project.update(cx, |project, cx| {
3158 let tree = project.worktrees().next().unwrap();
3159 tree.read(cx)
3160 .entry_for_path(path)
3161 .unwrap_or_else(|| panic!("no entry for path {}", path))
3162 .id
3163 })
3164 };
3165
3166 let dir_id = id_for_path("a", cx);
3167 let file_id = id_for_path("a/file1", cx);
3168 let buffer = project
3169 .update(cx, |p, cx| p.open_buffer((tree_id, "a/file1"), cx))
3170 .await
3171 .unwrap();
3172 buffer.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
3173
3174 project
3175 .update(cx, |project, cx| {
3176 project.rename_entry(dir_id, Path::new("b"), cx)
3177 })
3178 .unwrap()
3179 .await
3180 .to_included()
3181 .unwrap();
3182 cx.executor().run_until_parked();
3183
3184 assert_eq!(id_for_path("b", cx), dir_id);
3185 assert_eq!(id_for_path("b/file1", cx), file_id);
3186 buffer.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
3187}
3188
3189#[gpui::test]
3190async fn test_buffer_deduping(cx: &mut gpui::TestAppContext) {
3191 init_test(cx);
3192
3193 let fs = FakeFs::new(cx.executor());
3194 fs.insert_tree(
3195 "/dir",
3196 json!({
3197 "a.txt": "a-contents",
3198 "b.txt": "b-contents",
3199 }),
3200 )
3201 .await;
3202
3203 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
3204
3205 // Spawn multiple tasks to open paths, repeating some paths.
3206 let (buffer_a_1, buffer_b, buffer_a_2) = project.update(cx, |p, cx| {
3207 (
3208 p.open_local_buffer("/dir/a.txt", cx),
3209 p.open_local_buffer("/dir/b.txt", cx),
3210 p.open_local_buffer("/dir/a.txt", cx),
3211 )
3212 });
3213
3214 let buffer_a_1 = buffer_a_1.await.unwrap();
3215 let buffer_a_2 = buffer_a_2.await.unwrap();
3216 let buffer_b = buffer_b.await.unwrap();
3217 assert_eq!(buffer_a_1.update(cx, |b, _| b.text()), "a-contents");
3218 assert_eq!(buffer_b.update(cx, |b, _| b.text()), "b-contents");
3219
3220 // There is only one buffer per path.
3221 let buffer_a_id = buffer_a_1.entity_id();
3222 assert_eq!(buffer_a_2.entity_id(), buffer_a_id);
3223
3224 // Open the same path again while it is still open.
3225 drop(buffer_a_1);
3226 let buffer_a_3 = project
3227 .update(cx, |p, cx| p.open_local_buffer("/dir/a.txt", cx))
3228 .await
3229 .unwrap();
3230
3231 // There's still only one buffer per path.
3232 assert_eq!(buffer_a_3.entity_id(), buffer_a_id);
3233}
3234
3235#[gpui::test]
3236async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) {
3237 init_test(cx);
3238
3239 let fs = FakeFs::new(cx.executor());
3240 fs.insert_tree(
3241 "/dir",
3242 json!({
3243 "file1": "abc",
3244 "file2": "def",
3245 "file3": "ghi",
3246 }),
3247 )
3248 .await;
3249
3250 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
3251
3252 let buffer1 = project
3253 .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
3254 .await
3255 .unwrap();
3256 let events = Arc::new(Mutex::new(Vec::new()));
3257
3258 // initially, the buffer isn't dirty.
3259 buffer1.update(cx, |buffer, cx| {
3260 cx.subscribe(&buffer1, {
3261 let events = events.clone();
3262 move |_, _, event, _| match event {
3263 BufferEvent::Operation(_) => {}
3264 _ => events.lock().push(event.clone()),
3265 }
3266 })
3267 .detach();
3268
3269 assert!(!buffer.is_dirty());
3270 assert!(events.lock().is_empty());
3271
3272 buffer.edit([(1..2, "")], None, cx);
3273 });
3274
3275 // after the first edit, the buffer is dirty, and emits a dirtied event.
3276 buffer1.update(cx, |buffer, cx| {
3277 assert!(buffer.text() == "ac");
3278 assert!(buffer.is_dirty());
3279 assert_eq!(
3280 *events.lock(),
3281 &[language::Event::Edited, language::Event::DirtyChanged]
3282 );
3283 events.lock().clear();
3284 buffer.did_save(buffer.version(), buffer.file().unwrap().mtime(), cx);
3285 });
3286
3287 // after saving, the buffer is not dirty, and emits a saved event.
3288 buffer1.update(cx, |buffer, cx| {
3289 assert!(!buffer.is_dirty());
3290 assert_eq!(*events.lock(), &[language::Event::Saved]);
3291 events.lock().clear();
3292
3293 buffer.edit([(1..1, "B")], None, cx);
3294 buffer.edit([(2..2, "D")], None, cx);
3295 });
3296
3297 // after editing again, the buffer is dirty, and emits another dirty event.
3298 buffer1.update(cx, |buffer, cx| {
3299 assert!(buffer.text() == "aBDc");
3300 assert!(buffer.is_dirty());
3301 assert_eq!(
3302 *events.lock(),
3303 &[
3304 language::Event::Edited,
3305 language::Event::DirtyChanged,
3306 language::Event::Edited,
3307 ],
3308 );
3309 events.lock().clear();
3310
3311 // After restoring the buffer to its previously-saved state,
3312 // the buffer is not considered dirty anymore.
3313 buffer.edit([(1..3, "")], None, cx);
3314 assert!(buffer.text() == "ac");
3315 assert!(!buffer.is_dirty());
3316 });
3317
3318 assert_eq!(
3319 *events.lock(),
3320 &[language::Event::Edited, language::Event::DirtyChanged]
3321 );
3322
3323 // When a file is deleted, the buffer is considered dirty.
3324 let events = Arc::new(Mutex::new(Vec::new()));
3325 let buffer2 = project
3326 .update(cx, |p, cx| p.open_local_buffer("/dir/file2", cx))
3327 .await
3328 .unwrap();
3329 buffer2.update(cx, |_, cx| {
3330 cx.subscribe(&buffer2, {
3331 let events = events.clone();
3332 move |_, _, event, _| events.lock().push(event.clone())
3333 })
3334 .detach();
3335 });
3336
3337 fs.remove_file("/dir/file2".as_ref(), Default::default())
3338 .await
3339 .unwrap();
3340 cx.executor().run_until_parked();
3341 buffer2.update(cx, |buffer, _| assert!(buffer.is_dirty()));
3342 assert_eq!(
3343 *events.lock(),
3344 &[
3345 language::Event::DirtyChanged,
3346 language::Event::FileHandleChanged
3347 ]
3348 );
3349
3350 // When a file is already dirty when deleted, we don't emit a Dirtied event.
3351 let events = Arc::new(Mutex::new(Vec::new()));
3352 let buffer3 = project
3353 .update(cx, |p, cx| p.open_local_buffer("/dir/file3", cx))
3354 .await
3355 .unwrap();
3356 buffer3.update(cx, |_, cx| {
3357 cx.subscribe(&buffer3, {
3358 let events = events.clone();
3359 move |_, _, event, _| events.lock().push(event.clone())
3360 })
3361 .detach();
3362 });
3363
3364 buffer3.update(cx, |buffer, cx| {
3365 buffer.edit([(0..0, "x")], None, cx);
3366 });
3367 events.lock().clear();
3368 fs.remove_file("/dir/file3".as_ref(), Default::default())
3369 .await
3370 .unwrap();
3371 cx.executor().run_until_parked();
3372 assert_eq!(*events.lock(), &[language::Event::FileHandleChanged]);
3373 cx.update(|cx| assert!(buffer3.read(cx).is_dirty()));
3374}
3375
3376#[gpui::test]
3377async fn test_buffer_file_changes_on_disk(cx: &mut gpui::TestAppContext) {
3378 init_test(cx);
3379
3380 let initial_contents = "aaa\nbbbbb\nc\n";
3381 let fs = FakeFs::new(cx.executor());
3382 fs.insert_tree(
3383 "/dir",
3384 json!({
3385 "the-file": initial_contents,
3386 }),
3387 )
3388 .await;
3389 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
3390 let buffer = project
3391 .update(cx, |p, cx| p.open_local_buffer("/dir/the-file", cx))
3392 .await
3393 .unwrap();
3394
3395 let anchors = (0..3)
3396 .map(|row| buffer.update(cx, |b, _| b.anchor_before(Point::new(row, 1))))
3397 .collect::<Vec<_>>();
3398
3399 // Change the file on disk, adding two new lines of text, and removing
3400 // one line.
3401 buffer.update(cx, |buffer, _| {
3402 assert!(!buffer.is_dirty());
3403 assert!(!buffer.has_conflict());
3404 });
3405 let new_contents = "AAAA\naaa\nBB\nbbbbb\n";
3406 fs.save(
3407 "/dir/the-file".as_ref(),
3408 &new_contents.into(),
3409 LineEnding::Unix,
3410 )
3411 .await
3412 .unwrap();
3413
3414 // Because the buffer was not modified, it is reloaded from disk. Its
3415 // contents are edited according to the diff between the old and new
3416 // file contents.
3417 cx.executor().run_until_parked();
3418 buffer.update(cx, |buffer, _| {
3419 assert_eq!(buffer.text(), new_contents);
3420 assert!(!buffer.is_dirty());
3421 assert!(!buffer.has_conflict());
3422
3423 let anchor_positions = anchors
3424 .iter()
3425 .map(|anchor| anchor.to_point(&*buffer))
3426 .collect::<Vec<_>>();
3427 assert_eq!(
3428 anchor_positions,
3429 [Point::new(1, 1), Point::new(3, 1), Point::new(3, 5)]
3430 );
3431 });
3432
3433 // Modify the buffer
3434 buffer.update(cx, |buffer, cx| {
3435 buffer.edit([(0..0, " ")], None, cx);
3436 assert!(buffer.is_dirty());
3437 assert!(!buffer.has_conflict());
3438 });
3439
3440 // Change the file on disk again, adding blank lines to the beginning.
3441 fs.save(
3442 "/dir/the-file".as_ref(),
3443 &"\n\n\nAAAA\naaa\nBB\nbbbbb\n".into(),
3444 LineEnding::Unix,
3445 )
3446 .await
3447 .unwrap();
3448
3449 // Because the buffer is modified, it doesn't reload from disk, but is
3450 // marked as having a conflict.
3451 cx.executor().run_until_parked();
3452 buffer.update(cx, |buffer, _| {
3453 assert!(buffer.has_conflict());
3454 });
3455}
3456
3457#[gpui::test]
3458async fn test_buffer_line_endings(cx: &mut gpui::TestAppContext) {
3459 init_test(cx);
3460
3461 let fs = FakeFs::new(cx.executor());
3462 fs.insert_tree(
3463 "/dir",
3464 json!({
3465 "file1": "a\nb\nc\n",
3466 "file2": "one\r\ntwo\r\nthree\r\n",
3467 }),
3468 )
3469 .await;
3470
3471 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
3472 let buffer1 = project
3473 .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
3474 .await
3475 .unwrap();
3476 let buffer2 = project
3477 .update(cx, |p, cx| p.open_local_buffer("/dir/file2", cx))
3478 .await
3479 .unwrap();
3480
3481 buffer1.update(cx, |buffer, _| {
3482 assert_eq!(buffer.text(), "a\nb\nc\n");
3483 assert_eq!(buffer.line_ending(), LineEnding::Unix);
3484 });
3485 buffer2.update(cx, |buffer, _| {
3486 assert_eq!(buffer.text(), "one\ntwo\nthree\n");
3487 assert_eq!(buffer.line_ending(), LineEnding::Windows);
3488 });
3489
3490 // Change a file's line endings on disk from unix to windows. The buffer's
3491 // state updates correctly.
3492 fs.save(
3493 "/dir/file1".as_ref(),
3494 &"aaa\nb\nc\n".into(),
3495 LineEnding::Windows,
3496 )
3497 .await
3498 .unwrap();
3499 cx.executor().run_until_parked();
3500 buffer1.update(cx, |buffer, _| {
3501 assert_eq!(buffer.text(), "aaa\nb\nc\n");
3502 assert_eq!(buffer.line_ending(), LineEnding::Windows);
3503 });
3504
3505 // Save a file with windows line endings. The file is written correctly.
3506 buffer2.update(cx, |buffer, cx| {
3507 buffer.set_text("one\ntwo\nthree\nfour\n", cx);
3508 });
3509 project
3510 .update(cx, |project, cx| project.save_buffer(buffer2, cx))
3511 .await
3512 .unwrap();
3513 assert_eq!(
3514 fs.load("/dir/file2".as_ref()).await.unwrap(),
3515 "one\r\ntwo\r\nthree\r\nfour\r\n",
3516 );
3517}
3518
3519#[gpui::test]
3520async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
3521 init_test(cx);
3522
3523 let fs = FakeFs::new(cx.executor());
3524 fs.insert_tree(
3525 "/the-dir",
3526 json!({
3527 "a.rs": "
3528 fn foo(mut v: Vec<usize>) {
3529 for x in &v {
3530 v.push(1);
3531 }
3532 }
3533 "
3534 .unindent(),
3535 }),
3536 )
3537 .await;
3538
3539 let project = Project::test(fs.clone(), ["/the-dir".as_ref()], cx).await;
3540 let buffer = project
3541 .update(cx, |p, cx| p.open_local_buffer("/the-dir/a.rs", cx))
3542 .await
3543 .unwrap();
3544
3545 let buffer_uri = Uri::from_file_path("/the-dir/a.rs").unwrap();
3546 let message = lsp::PublishDiagnosticsParams {
3547 uri: buffer_uri.clone().into(),
3548 diagnostics: vec![
3549 lsp::Diagnostic {
3550 range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
3551 severity: Some(DiagnosticSeverity::WARNING),
3552 message: "error 1".to_string(),
3553 related_information: Some(vec![lsp::DiagnosticRelatedInformation {
3554 location: lsp::Location {
3555 uri: buffer_uri.clone().into(),
3556 range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
3557 },
3558 message: "error 1 hint 1".to_string(),
3559 }]),
3560 ..Default::default()
3561 },
3562 lsp::Diagnostic {
3563 range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
3564 severity: Some(DiagnosticSeverity::HINT),
3565 message: "error 1 hint 1".to_string(),
3566 related_information: Some(vec![lsp::DiagnosticRelatedInformation {
3567 location: lsp::Location {
3568 uri: buffer_uri.clone().into(),
3569 range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
3570 },
3571 message: "original diagnostic".to_string(),
3572 }]),
3573 ..Default::default()
3574 },
3575 lsp::Diagnostic {
3576 range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)),
3577 severity: Some(DiagnosticSeverity::ERROR),
3578 message: "error 2".to_string(),
3579 related_information: Some(vec![
3580 lsp::DiagnosticRelatedInformation {
3581 location: lsp::Location {
3582 uri: buffer_uri.clone().into(),
3583 range: lsp::Range::new(
3584 lsp::Position::new(1, 13),
3585 lsp::Position::new(1, 15),
3586 ),
3587 },
3588 message: "error 2 hint 1".to_string(),
3589 },
3590 lsp::DiagnosticRelatedInformation {
3591 location: lsp::Location {
3592 uri: buffer_uri.clone().into(),
3593 range: lsp::Range::new(
3594 lsp::Position::new(1, 13),
3595 lsp::Position::new(1, 15),
3596 ),
3597 },
3598 message: "error 2 hint 2".to_string(),
3599 },
3600 ]),
3601 ..Default::default()
3602 },
3603 lsp::Diagnostic {
3604 range: lsp::Range::new(lsp::Position::new(1, 13), lsp::Position::new(1, 15)),
3605 severity: Some(DiagnosticSeverity::HINT),
3606 message: "error 2 hint 1".to_string(),
3607 related_information: Some(vec![lsp::DiagnosticRelatedInformation {
3608 location: lsp::Location {
3609 uri: buffer_uri.clone().into(),
3610 range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)),
3611 },
3612 message: "original diagnostic".to_string(),
3613 }]),
3614 ..Default::default()
3615 },
3616 lsp::Diagnostic {
3617 range: lsp::Range::new(lsp::Position::new(1, 13), lsp::Position::new(1, 15)),
3618 severity: Some(DiagnosticSeverity::HINT),
3619 message: "error 2 hint 2".to_string(),
3620 related_information: Some(vec![lsp::DiagnosticRelatedInformation {
3621 location: lsp::Location {
3622 uri: buffer_uri.into(),
3623 range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)),
3624 },
3625 message: "original diagnostic".to_string(),
3626 }]),
3627 ..Default::default()
3628 },
3629 ],
3630 version: None,
3631 };
3632
3633 project
3634 .update(cx, |p, cx| {
3635 p.update_diagnostics(LanguageServerId(0), message, &[], cx)
3636 })
3637 .unwrap();
3638 let buffer = buffer.update(cx, |buffer, _| buffer.snapshot());
3639
3640 assert_eq!(
3641 buffer
3642 .diagnostics_in_range::<_, Point>(0..buffer.len(), false)
3643 .collect::<Vec<_>>(),
3644 &[
3645 DiagnosticEntry {
3646 range: Point::new(1, 8)..Point::new(1, 9),
3647 diagnostic: Diagnostic {
3648 severity: DiagnosticSeverity::WARNING,
3649 message: "error 1".to_string(),
3650 group_id: 1,
3651 is_primary: true,
3652 ..Default::default()
3653 }
3654 },
3655 DiagnosticEntry {
3656 range: Point::new(1, 8)..Point::new(1, 9),
3657 diagnostic: Diagnostic {
3658 severity: DiagnosticSeverity::HINT,
3659 message: "error 1 hint 1".to_string(),
3660 group_id: 1,
3661 is_primary: false,
3662 ..Default::default()
3663 }
3664 },
3665 DiagnosticEntry {
3666 range: Point::new(1, 13)..Point::new(1, 15),
3667 diagnostic: Diagnostic {
3668 severity: DiagnosticSeverity::HINT,
3669 message: "error 2 hint 1".to_string(),
3670 group_id: 0,
3671 is_primary: false,
3672 ..Default::default()
3673 }
3674 },
3675 DiagnosticEntry {
3676 range: Point::new(1, 13)..Point::new(1, 15),
3677 diagnostic: Diagnostic {
3678 severity: DiagnosticSeverity::HINT,
3679 message: "error 2 hint 2".to_string(),
3680 group_id: 0,
3681 is_primary: false,
3682 ..Default::default()
3683 }
3684 },
3685 DiagnosticEntry {
3686 range: Point::new(2, 8)..Point::new(2, 17),
3687 diagnostic: Diagnostic {
3688 severity: DiagnosticSeverity::ERROR,
3689 message: "error 2".to_string(),
3690 group_id: 0,
3691 is_primary: true,
3692 ..Default::default()
3693 }
3694 }
3695 ]
3696 );
3697
3698 assert_eq!(
3699 buffer.diagnostic_group::<Point>(0).collect::<Vec<_>>(),
3700 &[
3701 DiagnosticEntry {
3702 range: Point::new(1, 13)..Point::new(1, 15),
3703 diagnostic: Diagnostic {
3704 severity: DiagnosticSeverity::HINT,
3705 message: "error 2 hint 1".to_string(),
3706 group_id: 0,
3707 is_primary: false,
3708 ..Default::default()
3709 }
3710 },
3711 DiagnosticEntry {
3712 range: Point::new(1, 13)..Point::new(1, 15),
3713 diagnostic: Diagnostic {
3714 severity: DiagnosticSeverity::HINT,
3715 message: "error 2 hint 2".to_string(),
3716 group_id: 0,
3717 is_primary: false,
3718 ..Default::default()
3719 }
3720 },
3721 DiagnosticEntry {
3722 range: Point::new(2, 8)..Point::new(2, 17),
3723 diagnostic: Diagnostic {
3724 severity: DiagnosticSeverity::ERROR,
3725 message: "error 2".to_string(),
3726 group_id: 0,
3727 is_primary: true,
3728 ..Default::default()
3729 }
3730 }
3731 ]
3732 );
3733
3734 assert_eq!(
3735 buffer.diagnostic_group::<Point>(1).collect::<Vec<_>>(),
3736 &[
3737 DiagnosticEntry {
3738 range: Point::new(1, 8)..Point::new(1, 9),
3739 diagnostic: Diagnostic {
3740 severity: DiagnosticSeverity::WARNING,
3741 message: "error 1".to_string(),
3742 group_id: 1,
3743 is_primary: true,
3744 ..Default::default()
3745 }
3746 },
3747 DiagnosticEntry {
3748 range: Point::new(1, 8)..Point::new(1, 9),
3749 diagnostic: Diagnostic {
3750 severity: DiagnosticSeverity::HINT,
3751 message: "error 1 hint 1".to_string(),
3752 group_id: 1,
3753 is_primary: false,
3754 ..Default::default()
3755 }
3756 },
3757 ]
3758 );
3759}
3760
3761#[gpui::test]
3762async fn test_rename(cx: &mut gpui::TestAppContext) {
3763 init_test(cx);
3764
3765 let fs = FakeFs::new(cx.executor());
3766 fs.insert_tree(
3767 "/dir",
3768 json!({
3769 "one.rs": "const ONE: usize = 1;",
3770 "two.rs": "const TWO: usize = one::ONE + one::ONE;"
3771 }),
3772 )
3773 .await;
3774
3775 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
3776
3777 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
3778 language_registry.add(rust_lang());
3779 let mut fake_servers = language_registry.register_fake_lsp_adapter(
3780 "Rust",
3781 FakeLspAdapter {
3782 capabilities: lsp::ServerCapabilities {
3783 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
3784 prepare_provider: Some(true),
3785 work_done_progress_options: Default::default(),
3786 })),
3787 ..Default::default()
3788 },
3789 ..Default::default()
3790 },
3791 );
3792
3793 let buffer = project
3794 .update(cx, |project, cx| {
3795 project.open_local_buffer("/dir/one.rs", cx)
3796 })
3797 .await
3798 .unwrap();
3799
3800 let fake_server = fake_servers.next().await.unwrap();
3801
3802 let response = project.update(cx, |project, cx| {
3803 project.prepare_rename(buffer.clone(), 7, cx)
3804 });
3805 fake_server
3806 .handle_request::<lsp::request::PrepareRenameRequest, _, _>(|params, _| async move {
3807 assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs");
3808 assert_eq!(params.position, lsp::Position::new(0, 7));
3809 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
3810 lsp::Position::new(0, 6),
3811 lsp::Position::new(0, 9),
3812 ))))
3813 })
3814 .next()
3815 .await
3816 .unwrap();
3817 let range = response.await.unwrap().unwrap();
3818 let range = buffer.update(cx, |buffer, _| range.to_offset(buffer));
3819 assert_eq!(range, 6..9);
3820
3821 let response = project.update(cx, |project, cx| {
3822 project.perform_rename(buffer.clone(), 7, "THREE".to_string(), true, cx)
3823 });
3824 fake_server
3825 .handle_request::<lsp::request::Rename, _, _>(|params, _| async move {
3826 assert_eq!(
3827 params.text_document_position.text_document.uri.as_str(),
3828 "file:///dir/one.rs"
3829 );
3830 assert_eq!(
3831 params.text_document_position.position,
3832 lsp::Position::new(0, 7)
3833 );
3834 assert_eq!(params.new_name, "THREE");
3835 Ok(Some(lsp::WorkspaceEdit {
3836 changes: Some(
3837 [
3838 (
3839 lsp::Uri::from_file_path("/dir/one.rs").unwrap().into(),
3840 vec![lsp::TextEdit::new(
3841 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
3842 "THREE".to_string(),
3843 )],
3844 ),
3845 (
3846 lsp::Uri::from_file_path("/dir/two.rs").unwrap().into(),
3847 vec![
3848 lsp::TextEdit::new(
3849 lsp::Range::new(
3850 lsp::Position::new(0, 24),
3851 lsp::Position::new(0, 27),
3852 ),
3853 "THREE".to_string(),
3854 ),
3855 lsp::TextEdit::new(
3856 lsp::Range::new(
3857 lsp::Position::new(0, 35),
3858 lsp::Position::new(0, 38),
3859 ),
3860 "THREE".to_string(),
3861 ),
3862 ],
3863 ),
3864 ]
3865 .into_iter()
3866 .collect(),
3867 ),
3868 ..Default::default()
3869 }))
3870 })
3871 .next()
3872 .await
3873 .unwrap();
3874 let mut transaction = response.await.unwrap().0;
3875 assert_eq!(transaction.len(), 2);
3876 assert_eq!(
3877 transaction
3878 .remove_entry(&buffer)
3879 .unwrap()
3880 .0
3881 .update(cx, |buffer, _| buffer.text()),
3882 "const THREE: usize = 1;"
3883 );
3884 assert_eq!(
3885 transaction
3886 .into_keys()
3887 .next()
3888 .unwrap()
3889 .update(cx, |buffer, _| buffer.text()),
3890 "const TWO: usize = one::THREE + one::THREE;"
3891 );
3892}
3893
3894#[gpui::test]
3895async fn test_search(cx: &mut gpui::TestAppContext) {
3896 init_test(cx);
3897
3898 let fs = FakeFs::new(cx.executor());
3899 fs.insert_tree(
3900 "/dir",
3901 json!({
3902 "one.rs": "const ONE: usize = 1;",
3903 "two.rs": "const TWO: usize = one::ONE + one::ONE;",
3904 "three.rs": "const THREE: usize = one::ONE + two::TWO;",
3905 "four.rs": "const FOUR: usize = one::ONE + three::THREE;",
3906 }),
3907 )
3908 .await;
3909 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
3910 assert_eq!(
3911 search(
3912 &project,
3913 SearchQuery::text("TWO", false, true, false, Vec::new(), Vec::new()).unwrap(),
3914 cx
3915 )
3916 .await
3917 .unwrap(),
3918 HashMap::from_iter([
3919 ("dir/two.rs".to_string(), vec![6..9]),
3920 ("dir/three.rs".to_string(), vec![37..40])
3921 ])
3922 );
3923
3924 let buffer_4 = project
3925 .update(cx, |project, cx| {
3926 project.open_local_buffer("/dir/four.rs", cx)
3927 })
3928 .await
3929 .unwrap();
3930 buffer_4.update(cx, |buffer, cx| {
3931 let text = "two::TWO";
3932 buffer.edit([(20..28, text), (31..43, text)], None, cx);
3933 });
3934
3935 assert_eq!(
3936 search(
3937 &project,
3938 SearchQuery::text("TWO", false, true, false, Vec::new(), Vec::new()).unwrap(),
3939 cx
3940 )
3941 .await
3942 .unwrap(),
3943 HashMap::from_iter([
3944 ("dir/two.rs".to_string(), vec![6..9]),
3945 ("dir/three.rs".to_string(), vec![37..40]),
3946 ("dir/four.rs".to_string(), vec![25..28, 36..39])
3947 ])
3948 );
3949}
3950
3951#[gpui::test]
3952async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
3953 init_test(cx);
3954
3955 let search_query = "file";
3956
3957 let fs = FakeFs::new(cx.executor());
3958 fs.insert_tree(
3959 "/dir",
3960 json!({
3961 "one.rs": r#"// Rust file one"#,
3962 "one.ts": r#"// TypeScript file one"#,
3963 "two.rs": r#"// Rust file two"#,
3964 "two.ts": r#"// TypeScript file two"#,
3965 }),
3966 )
3967 .await;
3968 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
3969
3970 assert!(
3971 search(
3972 &project,
3973 SearchQuery::text(
3974 search_query,
3975 false,
3976 true,
3977 false,
3978 vec![PathMatcher::new("*.odd").unwrap()],
3979 Vec::new()
3980 )
3981 .unwrap(),
3982 cx
3983 )
3984 .await
3985 .unwrap()
3986 .is_empty(),
3987 "If no inclusions match, no files should be returned"
3988 );
3989
3990 assert_eq!(
3991 search(
3992 &project,
3993 SearchQuery::text(
3994 search_query,
3995 false,
3996 true,
3997 false,
3998 vec![PathMatcher::new("*.rs").unwrap()],
3999 Vec::new()
4000 )
4001 .unwrap(),
4002 cx
4003 )
4004 .await
4005 .unwrap(),
4006 HashMap::from_iter([
4007 ("dir/one.rs".to_string(), vec![8..12]),
4008 ("dir/two.rs".to_string(), vec![8..12]),
4009 ]),
4010 "Rust only search should give only Rust files"
4011 );
4012
4013 assert_eq!(
4014 search(
4015 &project,
4016 SearchQuery::text(
4017 search_query,
4018 false,
4019 true,
4020 false,
4021 vec![
4022 PathMatcher::new("*.ts").unwrap(),
4023 PathMatcher::new("*.odd").unwrap(),
4024 ],
4025 Vec::new()
4026 ).unwrap(),
4027 cx
4028 )
4029 .await
4030 .unwrap(),
4031 HashMap::from_iter([
4032 ("dir/one.ts".to_string(), vec![14..18]),
4033 ("dir/two.ts".to_string(), vec![14..18]),
4034 ]),
4035 "TypeScript only search should give only TypeScript files, even if other inclusions don't match anything"
4036 );
4037
4038 assert_eq!(
4039 search(
4040 &project,
4041 SearchQuery::text(
4042 search_query,
4043 false,
4044 true,
4045 false,
4046 vec![
4047 PathMatcher::new("*.rs").unwrap(),
4048 PathMatcher::new("*.ts").unwrap(),
4049 PathMatcher::new("*.odd").unwrap(),
4050 ],
4051 Vec::new()
4052 ).unwrap(),
4053 cx
4054 )
4055 .await
4056 .unwrap(),
4057 HashMap::from_iter([
4058 ("dir/two.ts".to_string(), vec![14..18]),
4059 ("dir/one.rs".to_string(), vec![8..12]),
4060 ("dir/one.ts".to_string(), vec![14..18]),
4061 ("dir/two.rs".to_string(), vec![8..12]),
4062 ]),
4063 "Rust and typescript search should give both Rust and TypeScript files, even if other inclusions don't match anything"
4064 );
4065}
4066
4067#[gpui::test]
4068async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
4069 init_test(cx);
4070
4071 let search_query = "file";
4072
4073 let fs = FakeFs::new(cx.executor());
4074 fs.insert_tree(
4075 "/dir",
4076 json!({
4077 "one.rs": r#"// Rust file one"#,
4078 "one.ts": r#"// TypeScript file one"#,
4079 "two.rs": r#"// Rust file two"#,
4080 "two.ts": r#"// TypeScript file two"#,
4081 }),
4082 )
4083 .await;
4084 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
4085
4086 assert_eq!(
4087 search(
4088 &project,
4089 SearchQuery::text(
4090 search_query,
4091 false,
4092 true,
4093 false,
4094 Vec::new(),
4095 vec![PathMatcher::new("*.odd").unwrap()],
4096 )
4097 .unwrap(),
4098 cx
4099 )
4100 .await
4101 .unwrap(),
4102 HashMap::from_iter([
4103 ("dir/one.rs".to_string(), vec![8..12]),
4104 ("dir/one.ts".to_string(), vec![14..18]),
4105 ("dir/two.rs".to_string(), vec![8..12]),
4106 ("dir/two.ts".to_string(), vec![14..18]),
4107 ]),
4108 "If no exclusions match, all files should be returned"
4109 );
4110
4111 assert_eq!(
4112 search(
4113 &project,
4114 SearchQuery::text(
4115 search_query,
4116 false,
4117 true,
4118 false,
4119 Vec::new(),
4120 vec![PathMatcher::new("*.rs").unwrap()],
4121 )
4122 .unwrap(),
4123 cx
4124 )
4125 .await
4126 .unwrap(),
4127 HashMap::from_iter([
4128 ("dir/one.ts".to_string(), vec![14..18]),
4129 ("dir/two.ts".to_string(), vec![14..18]),
4130 ]),
4131 "Rust exclusion search should give only TypeScript files"
4132 );
4133
4134 assert_eq!(
4135 search(
4136 &project,
4137 SearchQuery::text(
4138 search_query,
4139 false,
4140 true,
4141 false,
4142 Vec::new(),
4143 vec![
4144 PathMatcher::new("*.ts").unwrap(),
4145 PathMatcher::new("*.odd").unwrap(),
4146 ],
4147 ).unwrap(),
4148 cx
4149 )
4150 .await
4151 .unwrap(),
4152 HashMap::from_iter([
4153 ("dir/one.rs".to_string(), vec![8..12]),
4154 ("dir/two.rs".to_string(), vec![8..12]),
4155 ]),
4156 "TypeScript exclusion search should give only Rust files, even if other exclusions don't match anything"
4157 );
4158
4159 assert!(
4160 search(
4161 &project,
4162 SearchQuery::text(
4163 search_query,
4164 false,
4165 true,
4166 false,
4167 Vec::new(),
4168 vec![
4169 PathMatcher::new("*.rs").unwrap(),
4170 PathMatcher::new("*.ts").unwrap(),
4171 PathMatcher::new("*.odd").unwrap(),
4172 ],
4173 ).unwrap(),
4174 cx
4175 )
4176 .await
4177 .unwrap().is_empty(),
4178 "Rust and typescript exclusion should give no files, even if other exclusions don't match anything"
4179 );
4180}
4181
4182#[gpui::test]
4183async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContext) {
4184 init_test(cx);
4185
4186 let search_query = "file";
4187
4188 let fs = FakeFs::new(cx.executor());
4189 fs.insert_tree(
4190 "/dir",
4191 json!({
4192 "one.rs": r#"// Rust file one"#,
4193 "one.ts": r#"// TypeScript file one"#,
4194 "two.rs": r#"// Rust file two"#,
4195 "two.ts": r#"// TypeScript file two"#,
4196 }),
4197 )
4198 .await;
4199 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
4200
4201 assert!(
4202 search(
4203 &project,
4204 SearchQuery::text(
4205 search_query,
4206 false,
4207 true,
4208 false,
4209 vec![PathMatcher::new("*.odd").unwrap()],
4210 vec![PathMatcher::new("*.odd").unwrap()],
4211 )
4212 .unwrap(),
4213 cx
4214 )
4215 .await
4216 .unwrap()
4217 .is_empty(),
4218 "If both no exclusions and inclusions match, exclusions should win and return nothing"
4219 );
4220
4221 assert!(
4222 search(
4223 &project,
4224 SearchQuery::text(
4225 search_query,
4226 false,
4227 true,
4228 false,
4229 vec![PathMatcher::new("*.ts").unwrap()],
4230 vec![PathMatcher::new("*.ts").unwrap()],
4231 ).unwrap(),
4232 cx
4233 )
4234 .await
4235 .unwrap()
4236 .is_empty(),
4237 "If both TypeScript exclusions and inclusions match, exclusions should win and return nothing files."
4238 );
4239
4240 assert!(
4241 search(
4242 &project,
4243 SearchQuery::text(
4244 search_query,
4245 false,
4246 true,
4247 false,
4248 vec![
4249 PathMatcher::new("*.ts").unwrap(),
4250 PathMatcher::new("*.odd").unwrap()
4251 ],
4252 vec![
4253 PathMatcher::new("*.ts").unwrap(),
4254 PathMatcher::new("*.odd").unwrap()
4255 ],
4256 )
4257 .unwrap(),
4258 cx
4259 )
4260 .await
4261 .unwrap()
4262 .is_empty(),
4263 "Non-matching inclusions and exclusions should not change that."
4264 );
4265
4266 assert_eq!(
4267 search(
4268 &project,
4269 SearchQuery::text(
4270 search_query,
4271 false,
4272 true,
4273 false,
4274 vec![
4275 PathMatcher::new("*.ts").unwrap(),
4276 PathMatcher::new("*.odd").unwrap()
4277 ],
4278 vec![
4279 PathMatcher::new("*.rs").unwrap(),
4280 PathMatcher::new("*.odd").unwrap()
4281 ],
4282 )
4283 .unwrap(),
4284 cx
4285 )
4286 .await
4287 .unwrap(),
4288 HashMap::from_iter([
4289 ("dir/one.ts".to_string(), vec![14..18]),
4290 ("dir/two.ts".to_string(), vec![14..18]),
4291 ]),
4292 "Non-intersecting TypeScript inclusions and Rust exclusions should return TypeScript files"
4293 );
4294}
4295
4296#[gpui::test]
4297async fn test_search_multiple_worktrees_with_inclusions(cx: &mut gpui::TestAppContext) {
4298 init_test(cx);
4299
4300 let fs = FakeFs::new(cx.executor());
4301 fs.insert_tree(
4302 "/worktree-a",
4303 json!({
4304 "haystack.rs": r#"// NEEDLE"#,
4305 "haystack.ts": r#"// NEEDLE"#,
4306 }),
4307 )
4308 .await;
4309 fs.insert_tree(
4310 "/worktree-b",
4311 json!({
4312 "haystack.rs": r#"// NEEDLE"#,
4313 "haystack.ts": r#"// NEEDLE"#,
4314 }),
4315 )
4316 .await;
4317
4318 let project = Project::test(
4319 fs.clone(),
4320 ["/worktree-a".as_ref(), "/worktree-b".as_ref()],
4321 cx,
4322 )
4323 .await;
4324
4325 assert_eq!(
4326 search(
4327 &project,
4328 SearchQuery::text(
4329 "NEEDLE",
4330 false,
4331 true,
4332 false,
4333 vec![PathMatcher::new("worktree-a/*.rs").unwrap()],
4334 Vec::new()
4335 )
4336 .unwrap(),
4337 cx
4338 )
4339 .await
4340 .unwrap(),
4341 HashMap::from_iter([("worktree-a/haystack.rs".to_string(), vec![3..9])]),
4342 "should only return results from included worktree"
4343 );
4344 assert_eq!(
4345 search(
4346 &project,
4347 SearchQuery::text(
4348 "NEEDLE",
4349 false,
4350 true,
4351 false,
4352 vec![PathMatcher::new("worktree-b/*.rs").unwrap()],
4353 Vec::new()
4354 )
4355 .unwrap(),
4356 cx
4357 )
4358 .await
4359 .unwrap(),
4360 HashMap::from_iter([("worktree-b/haystack.rs".to_string(), vec![3..9])]),
4361 "should only return results from included worktree"
4362 );
4363
4364 assert_eq!(
4365 search(
4366 &project,
4367 SearchQuery::text(
4368 "NEEDLE",
4369 false,
4370 true,
4371 false,
4372 vec![PathMatcher::new("*.ts").unwrap()],
4373 Vec::new()
4374 )
4375 .unwrap(),
4376 cx
4377 )
4378 .await
4379 .unwrap(),
4380 HashMap::from_iter([
4381 ("worktree-a/haystack.ts".to_string(), vec![3..9]),
4382 ("worktree-b/haystack.ts".to_string(), vec![3..9])
4383 ]),
4384 "should return results from both worktrees"
4385 );
4386}
4387
4388#[gpui::test]
4389async fn test_search_in_gitignored_dirs(cx: &mut gpui::TestAppContext) {
4390 init_test(cx);
4391
4392 let fs = FakeFs::new(cx.background_executor.clone());
4393 fs.insert_tree(
4394 "/dir",
4395 json!({
4396 ".git": {},
4397 ".gitignore": "**/target\n/node_modules\n",
4398 "target": {
4399 "index.txt": "index_key:index_value"
4400 },
4401 "node_modules": {
4402 "eslint": {
4403 "index.ts": "const eslint_key = 'eslint value'",
4404 "package.json": r#"{ "some_key": "some value" }"#,
4405 },
4406 "prettier": {
4407 "index.ts": "const prettier_key = 'prettier value'",
4408 "package.json": r#"{ "other_key": "other value" }"#,
4409 },
4410 },
4411 "package.json": r#"{ "main_key": "main value" }"#,
4412 }),
4413 )
4414 .await;
4415 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
4416
4417 let query = "key";
4418 assert_eq!(
4419 search(
4420 &project,
4421 SearchQuery::text(query, false, false, false, Vec::new(), Vec::new()).unwrap(),
4422 cx
4423 )
4424 .await
4425 .unwrap(),
4426 HashMap::from_iter([("dir/package.json".to_string(), vec![8..11])]),
4427 "Only one non-ignored file should have the query"
4428 );
4429
4430 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
4431 assert_eq!(
4432 search(
4433 &project,
4434 SearchQuery::text(query, false, false, true, Vec::new(), Vec::new()).unwrap(),
4435 cx
4436 )
4437 .await
4438 .unwrap(),
4439 HashMap::from_iter([
4440 ("dir/package.json".to_string(), vec![8..11]),
4441 ("dir/target/index.txt".to_string(), vec![6..9]),
4442 (
4443 "dir/node_modules/prettier/package.json".to_string(),
4444 vec![9..12]
4445 ),
4446 (
4447 "dir/node_modules/prettier/index.ts".to_string(),
4448 vec![15..18]
4449 ),
4450 ("dir/node_modules/eslint/index.ts".to_string(), vec![13..16]),
4451 (
4452 "dir/node_modules/eslint/package.json".to_string(),
4453 vec![8..11]
4454 ),
4455 ]),
4456 "Unrestricted search with ignored directories should find every file with the query"
4457 );
4458
4459 let files_to_include = vec![PathMatcher::new("/dir/node_modules/prettier/**").unwrap()];
4460 let files_to_exclude = vec![PathMatcher::new("*.ts").unwrap()];
4461 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
4462 assert_eq!(
4463 search(
4464 &project,
4465 SearchQuery::text(
4466 query,
4467 false,
4468 false,
4469 true,
4470 files_to_include,
4471 files_to_exclude,
4472 )
4473 .unwrap(),
4474 cx
4475 )
4476 .await
4477 .unwrap(),
4478 HashMap::from_iter([(
4479 "dir/node_modules/prettier/package.json".to_string(),
4480 vec![9..12]
4481 )]),
4482 "With search including ignored prettier directory and excluding TS files, only one file should be found"
4483 );
4484}
4485
4486#[test]
4487fn test_glob_literal_prefix() {
4488 assert_eq!(glob_literal_prefix("**/*.js"), "");
4489 assert_eq!(glob_literal_prefix("node_modules/**/*.js"), "node_modules");
4490 assert_eq!(glob_literal_prefix("foo/{bar,baz}.js"), "foo");
4491 assert_eq!(glob_literal_prefix("foo/bar/baz.js"), "foo/bar/baz.js");
4492}
4493
4494#[gpui::test]
4495async fn test_create_entry(cx: &mut gpui::TestAppContext) {
4496 init_test(cx);
4497
4498 let fs = FakeFs::new(cx.executor().clone());
4499 fs.insert_tree(
4500 "/one/two",
4501 json!({
4502 "three": {
4503 "a.txt": "",
4504 "four": {}
4505 },
4506 "c.rs": ""
4507 }),
4508 )
4509 .await;
4510
4511 let project = Project::test(fs.clone(), ["/one/two/three".as_ref()], cx).await;
4512 project
4513 .update(cx, |project, cx| {
4514 let id = project.worktrees().next().unwrap().read(cx).id();
4515 project.create_entry((id, "b.."), true, cx)
4516 })
4517 .unwrap()
4518 .await
4519 .to_included()
4520 .unwrap();
4521
4522 // Can't create paths outside the project
4523 let result = project
4524 .update(cx, |project, cx| {
4525 let id = project.worktrees().next().unwrap().read(cx).id();
4526 project.create_entry((id, "../../boop"), true, cx)
4527 })
4528 .await;
4529 assert!(result.is_err());
4530
4531 // Can't create paths with '..'
4532 let result = project
4533 .update(cx, |project, cx| {
4534 let id = project.worktrees().next().unwrap().read(cx).id();
4535 project.create_entry((id, "four/../beep"), true, cx)
4536 })
4537 .await;
4538 assert!(result.is_err());
4539
4540 assert_eq!(
4541 fs.paths(true),
4542 vec![
4543 PathBuf::from("/"),
4544 PathBuf::from("/one"),
4545 PathBuf::from("/one/two"),
4546 PathBuf::from("/one/two/c.rs"),
4547 PathBuf::from("/one/two/three"),
4548 PathBuf::from("/one/two/three/a.txt"),
4549 PathBuf::from("/one/two/three/b.."),
4550 PathBuf::from("/one/two/three/four"),
4551 ]
4552 );
4553
4554 // And we cannot open buffers with '..'
4555 let result = project
4556 .update(cx, |project, cx| {
4557 let id = project.worktrees().next().unwrap().read(cx).id();
4558 project.open_buffer((id, "../c.rs"), cx)
4559 })
4560 .await;
4561 assert!(result.is_err())
4562}
4563
4564#[gpui::test]
4565async fn test_multiple_language_server_hovers(cx: &mut gpui::TestAppContext) {
4566 init_test(cx);
4567
4568 let fs = FakeFs::new(cx.executor());
4569 fs.insert_tree(
4570 "/dir",
4571 json!({
4572 "a.tsx": "a",
4573 }),
4574 )
4575 .await;
4576
4577 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
4578
4579 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
4580 language_registry.add(tsx_lang());
4581 let language_server_names = [
4582 "TypeScriptServer",
4583 "TailwindServer",
4584 "ESLintServer",
4585 "NoHoverCapabilitiesServer",
4586 ];
4587 let mut fake_tsx_language_servers = language_registry.register_specific_fake_lsp_adapter(
4588 "tsx",
4589 true,
4590 FakeLspAdapter {
4591 name: &language_server_names[0],
4592 capabilities: lsp::ServerCapabilities {
4593 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
4594 ..lsp::ServerCapabilities::default()
4595 },
4596 ..FakeLspAdapter::default()
4597 },
4598 );
4599 let _a = language_registry.register_specific_fake_lsp_adapter(
4600 "tsx",
4601 false,
4602 FakeLspAdapter {
4603 name: &language_server_names[1],
4604 capabilities: lsp::ServerCapabilities {
4605 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
4606 ..lsp::ServerCapabilities::default()
4607 },
4608 ..FakeLspAdapter::default()
4609 },
4610 );
4611 let _b = language_registry.register_specific_fake_lsp_adapter(
4612 "tsx",
4613 false,
4614 FakeLspAdapter {
4615 name: &language_server_names[2],
4616 capabilities: lsp::ServerCapabilities {
4617 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
4618 ..lsp::ServerCapabilities::default()
4619 },
4620 ..FakeLspAdapter::default()
4621 },
4622 );
4623 let _c = language_registry.register_specific_fake_lsp_adapter(
4624 "tsx",
4625 false,
4626 FakeLspAdapter {
4627 name: &language_server_names[3],
4628 capabilities: lsp::ServerCapabilities {
4629 hover_provider: None,
4630 ..lsp::ServerCapabilities::default()
4631 },
4632 ..FakeLspAdapter::default()
4633 },
4634 );
4635
4636 let buffer = project
4637 .update(cx, |p, cx| p.open_local_buffer("/dir/a.tsx", cx))
4638 .await
4639 .unwrap();
4640 cx.executor().run_until_parked();
4641
4642 let mut servers_with_hover_requests = HashMap::default();
4643 for i in 0..language_server_names.len() {
4644 let new_server = fake_tsx_language_servers.next().await.unwrap_or_else(|| {
4645 panic!(
4646 "Failed to get language server #{i} with name {}",
4647 &language_server_names[i]
4648 )
4649 });
4650 let new_server_name = new_server.server.name();
4651 assert!(
4652 !servers_with_hover_requests.contains_key(new_server_name),
4653 "Unexpected: initialized server with the same name twice. Name: `{new_server_name}`"
4654 );
4655 let new_server_name = new_server_name.to_string();
4656 match new_server_name.as_str() {
4657 "TailwindServer" | "TypeScriptServer" => {
4658 servers_with_hover_requests.insert(
4659 new_server_name.clone(),
4660 new_server.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _| {
4661 let name = new_server_name.clone();
4662 async move {
4663 Ok(Some(lsp::Hover {
4664 contents: lsp::HoverContents::Scalar(lsp::MarkedString::String(
4665 format!("{name} hover"),
4666 )),
4667 range: None,
4668 }))
4669 }
4670 }),
4671 );
4672 }
4673 "ESLintServer" => {
4674 servers_with_hover_requests.insert(
4675 new_server_name,
4676 new_server.handle_request::<lsp::request::HoverRequest, _, _>(
4677 |_, _| async move { Ok(None) },
4678 ),
4679 );
4680 }
4681 "NoHoverCapabilitiesServer" => {
4682 let _never_handled = new_server.handle_request::<lsp::request::HoverRequest, _, _>(
4683 |_, _| async move {
4684 panic!(
4685 "Should not call for hovers server with no corresponding capabilities"
4686 )
4687 },
4688 );
4689 }
4690 unexpected => panic!("Unexpected server name: {unexpected}"),
4691 }
4692 }
4693
4694 let hover_task = project.update(cx, |project, cx| {
4695 project.hover(&buffer, Point::new(0, 0), cx)
4696 });
4697 let _: Vec<()> = futures::future::join_all(servers_with_hover_requests.into_values().map(
4698 |mut hover_request| async move {
4699 hover_request
4700 .next()
4701 .await
4702 .expect("All hover requests should have been triggered")
4703 },
4704 ))
4705 .await;
4706 assert_eq!(
4707 vec!["TailwindServer hover", "TypeScriptServer hover"],
4708 hover_task
4709 .await
4710 .into_iter()
4711 .map(|hover| hover.contents.iter().map(|block| &block.text).join("|"))
4712 .sorted()
4713 .collect::<Vec<_>>(),
4714 "Should receive hover responses from all related servers with hover capabilities"
4715 );
4716}
4717
4718#[gpui::test]
4719async fn test_hovers_with_empty_parts(cx: &mut gpui::TestAppContext) {
4720 init_test(cx);
4721
4722 let fs = FakeFs::new(cx.executor());
4723 fs.insert_tree(
4724 "/dir",
4725 json!({
4726 "a.ts": "a",
4727 }),
4728 )
4729 .await;
4730
4731 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
4732
4733 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
4734 language_registry.add(typescript_lang());
4735 let mut fake_language_servers = language_registry.register_fake_lsp_adapter(
4736 "TypeScript",
4737 FakeLspAdapter {
4738 capabilities: lsp::ServerCapabilities {
4739 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
4740 ..lsp::ServerCapabilities::default()
4741 },
4742 ..FakeLspAdapter::default()
4743 },
4744 );
4745
4746 let buffer = project
4747 .update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
4748 .await
4749 .unwrap();
4750 cx.executor().run_until_parked();
4751
4752 let fake_server = fake_language_servers
4753 .next()
4754 .await
4755 .expect("failed to get the language server");
4756
4757 let mut request_handled =
4758 fake_server.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _| async move {
4759 Ok(Some(lsp::Hover {
4760 contents: lsp::HoverContents::Array(vec![
4761 lsp::MarkedString::String("".to_string()),
4762 lsp::MarkedString::String(" ".to_string()),
4763 lsp::MarkedString::String("\n\n\n".to_string()),
4764 ]),
4765 range: None,
4766 }))
4767 });
4768
4769 let hover_task = project.update(cx, |project, cx| {
4770 project.hover(&buffer, Point::new(0, 0), cx)
4771 });
4772 let () = request_handled
4773 .next()
4774 .await
4775 .expect("All hover requests should have been triggered");
4776 assert_eq!(
4777 Vec::<String>::new(),
4778 hover_task
4779 .await
4780 .into_iter()
4781 .map(|hover| hover.contents.iter().map(|block| &block.text).join("|"))
4782 .sorted()
4783 .collect::<Vec<_>>(),
4784 "Empty hover parts should be ignored"
4785 );
4786}
4787
4788#[gpui::test]
4789async fn test_multiple_language_server_actions(cx: &mut gpui::TestAppContext) {
4790 init_test(cx);
4791
4792 let fs = FakeFs::new(cx.executor());
4793 fs.insert_tree(
4794 "/dir",
4795 json!({
4796 "a.tsx": "a",
4797 }),
4798 )
4799 .await;
4800
4801 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
4802
4803 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
4804 language_registry.add(tsx_lang());
4805 let language_server_names = [
4806 "TypeScriptServer",
4807 "TailwindServer",
4808 "ESLintServer",
4809 "NoActionsCapabilitiesServer",
4810 ];
4811 let mut fake_tsx_language_servers = language_registry.register_specific_fake_lsp_adapter(
4812 "tsx",
4813 true,
4814 FakeLspAdapter {
4815 name: &language_server_names[0],
4816 capabilities: lsp::ServerCapabilities {
4817 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
4818 ..lsp::ServerCapabilities::default()
4819 },
4820 ..FakeLspAdapter::default()
4821 },
4822 );
4823 let _a = language_registry.register_specific_fake_lsp_adapter(
4824 "tsx",
4825 false,
4826 FakeLspAdapter {
4827 name: &language_server_names[1],
4828 capabilities: lsp::ServerCapabilities {
4829 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
4830 ..lsp::ServerCapabilities::default()
4831 },
4832 ..FakeLspAdapter::default()
4833 },
4834 );
4835 let _b = language_registry.register_specific_fake_lsp_adapter(
4836 "tsx",
4837 false,
4838 FakeLspAdapter {
4839 name: &language_server_names[2],
4840 capabilities: lsp::ServerCapabilities {
4841 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
4842 ..lsp::ServerCapabilities::default()
4843 },
4844 ..FakeLspAdapter::default()
4845 },
4846 );
4847 let _c = language_registry.register_specific_fake_lsp_adapter(
4848 "tsx",
4849 false,
4850 FakeLspAdapter {
4851 name: &language_server_names[3],
4852 capabilities: lsp::ServerCapabilities {
4853 code_action_provider: None,
4854 ..lsp::ServerCapabilities::default()
4855 },
4856 ..FakeLspAdapter::default()
4857 },
4858 );
4859
4860 let buffer = project
4861 .update(cx, |p, cx| p.open_local_buffer("/dir/a.tsx", cx))
4862 .await
4863 .unwrap();
4864 cx.executor().run_until_parked();
4865
4866 let mut servers_with_actions_requests = HashMap::default();
4867 for i in 0..language_server_names.len() {
4868 let new_server = fake_tsx_language_servers.next().await.unwrap_or_else(|| {
4869 panic!(
4870 "Failed to get language server #{i} with name {}",
4871 &language_server_names[i]
4872 )
4873 });
4874 let new_server_name = new_server.server.name();
4875 assert!(
4876 !servers_with_actions_requests.contains_key(new_server_name),
4877 "Unexpected: initialized server with the same name twice. Name: `{new_server_name}`"
4878 );
4879 let new_server_name = new_server_name.to_string();
4880 match new_server_name.as_str() {
4881 "TailwindServer" | "TypeScriptServer" => {
4882 servers_with_actions_requests.insert(
4883 new_server_name.clone(),
4884 new_server.handle_request::<lsp::request::CodeActionRequest, _, _>(
4885 move |_, _| {
4886 let name = new_server_name.clone();
4887 async move {
4888 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
4889 lsp::CodeAction {
4890 title: format!("{name} code action"),
4891 ..lsp::CodeAction::default()
4892 },
4893 )]))
4894 }
4895 },
4896 ),
4897 );
4898 }
4899 "ESLintServer" => {
4900 servers_with_actions_requests.insert(
4901 new_server_name,
4902 new_server.handle_request::<lsp::request::CodeActionRequest, _, _>(
4903 |_, _| async move { Ok(None) },
4904 ),
4905 );
4906 }
4907 "NoActionsCapabilitiesServer" => {
4908 let _never_handled = new_server
4909 .handle_request::<lsp::request::CodeActionRequest, _, _>(|_, _| async move {
4910 panic!(
4911 "Should not call for code actions server with no corresponding capabilities"
4912 )
4913 });
4914 }
4915 unexpected => panic!("Unexpected server name: {unexpected}"),
4916 }
4917 }
4918
4919 let code_actions_task = project.update(cx, |project, cx| {
4920 project.code_actions(&buffer, 0..buffer.read(cx).len(), cx)
4921 });
4922 let _: Vec<()> = futures::future::join_all(servers_with_actions_requests.into_values().map(
4923 |mut code_actions_request| async move {
4924 code_actions_request
4925 .next()
4926 .await
4927 .expect("All code actions requests should have been triggered")
4928 },
4929 ))
4930 .await;
4931 assert_eq!(
4932 vec!["TailwindServer code action", "TypeScriptServer code action"],
4933 code_actions_task
4934 .await
4935 .into_iter()
4936 .map(|code_action| code_action.lsp_action.title)
4937 .sorted()
4938 .collect::<Vec<_>>(),
4939 "Should receive code actions responses from all related servers with hover capabilities"
4940 );
4941}
4942
4943#[gpui::test]
4944async fn test_reordering_worktrees(cx: &mut gpui::TestAppContext) {
4945 init_test(cx);
4946
4947 let fs = FakeFs::new(cx.executor());
4948 fs.insert_tree(
4949 "/dir",
4950 json!({
4951 "a.rs": "let a = 1;",
4952 "b.rs": "let b = 2;",
4953 "c.rs": "let c = 2;",
4954 }),
4955 )
4956 .await;
4957
4958 let project = Project::test(
4959 fs,
4960 [
4961 "/dir/a.rs".as_ref(),
4962 "/dir/b.rs".as_ref(),
4963 "/dir/c.rs".as_ref(),
4964 ],
4965 cx,
4966 )
4967 .await;
4968
4969 // check the initial state and get the worktrees
4970 let (worktree_a, worktree_b, worktree_c) = project.update(cx, |project, cx| {
4971 let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
4972 assert_eq!(worktrees.len(), 3);
4973
4974 let worktree_a = worktrees[0].read(cx);
4975 let worktree_b = worktrees[1].read(cx);
4976 let worktree_c = worktrees[2].read(cx);
4977
4978 // check they start in the right order
4979 assert_eq!(worktree_a.abs_path().to_str().unwrap(), "/dir/a.rs");
4980 assert_eq!(worktree_b.abs_path().to_str().unwrap(), "/dir/b.rs");
4981 assert_eq!(worktree_c.abs_path().to_str().unwrap(), "/dir/c.rs");
4982
4983 (
4984 worktrees[0].clone(),
4985 worktrees[1].clone(),
4986 worktrees[2].clone(),
4987 )
4988 });
4989
4990 // move first worktree to after the second
4991 // [a, b, c] -> [b, a, c]
4992 project
4993 .update(cx, |project, cx| {
4994 let first = worktree_a.read(cx);
4995 let second = worktree_b.read(cx);
4996 project.move_worktree(first.id(), second.id(), cx)
4997 })
4998 .expect("moving first after second");
4999
5000 // check the state after moving
5001 project.update(cx, |project, cx| {
5002 let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
5003 assert_eq!(worktrees.len(), 3);
5004
5005 let first = worktrees[0].read(cx);
5006 let second = worktrees[1].read(cx);
5007 let third = worktrees[2].read(cx);
5008
5009 // check they are now in the right order
5010 assert_eq!(first.abs_path().to_str().unwrap(), "/dir/b.rs");
5011 assert_eq!(second.abs_path().to_str().unwrap(), "/dir/a.rs");
5012 assert_eq!(third.abs_path().to_str().unwrap(), "/dir/c.rs");
5013 });
5014
5015 // move the second worktree to before the first
5016 // [b, a, c] -> [a, b, c]
5017 project
5018 .update(cx, |project, cx| {
5019 let second = worktree_a.read(cx);
5020 let first = worktree_b.read(cx);
5021 project.move_worktree(first.id(), second.id(), cx)
5022 })
5023 .expect("moving second before first");
5024
5025 // check the state after moving
5026 project.update(cx, |project, cx| {
5027 let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
5028 assert_eq!(worktrees.len(), 3);
5029
5030 let first = worktrees[0].read(cx);
5031 let second = worktrees[1].read(cx);
5032 let third = worktrees[2].read(cx);
5033
5034 // check they are now in the right order
5035 assert_eq!(first.abs_path().to_str().unwrap(), "/dir/a.rs");
5036 assert_eq!(second.abs_path().to_str().unwrap(), "/dir/b.rs");
5037 assert_eq!(third.abs_path().to_str().unwrap(), "/dir/c.rs");
5038 });
5039
5040 // move the second worktree to after the third
5041 // [a, b, c] -> [a, c, b]
5042 project
5043 .update(cx, |project, cx| {
5044 let second = worktree_b.read(cx);
5045 let third = worktree_c.read(cx);
5046 project.move_worktree(second.id(), third.id(), cx)
5047 })
5048 .expect("moving second after third");
5049
5050 // check the state after moving
5051 project.update(cx, |project, cx| {
5052 let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
5053 assert_eq!(worktrees.len(), 3);
5054
5055 let first = worktrees[0].read(cx);
5056 let second = worktrees[1].read(cx);
5057 let third = worktrees[2].read(cx);
5058
5059 // check they are now in the right order
5060 assert_eq!(first.abs_path().to_str().unwrap(), "/dir/a.rs");
5061 assert_eq!(second.abs_path().to_str().unwrap(), "/dir/c.rs");
5062 assert_eq!(third.abs_path().to_str().unwrap(), "/dir/b.rs");
5063 });
5064
5065 // move the third worktree to before the second
5066 // [a, c, b] -> [a, b, c]
5067 project
5068 .update(cx, |project, cx| {
5069 let third = worktree_c.read(cx);
5070 let second = worktree_b.read(cx);
5071 project.move_worktree(third.id(), second.id(), cx)
5072 })
5073 .expect("moving third before second");
5074
5075 // check the state after moving
5076 project.update(cx, |project, cx| {
5077 let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
5078 assert_eq!(worktrees.len(), 3);
5079
5080 let first = worktrees[0].read(cx);
5081 let second = worktrees[1].read(cx);
5082 let third = worktrees[2].read(cx);
5083
5084 // check they are now in the right order
5085 assert_eq!(first.abs_path().to_str().unwrap(), "/dir/a.rs");
5086 assert_eq!(second.abs_path().to_str().unwrap(), "/dir/b.rs");
5087 assert_eq!(third.abs_path().to_str().unwrap(), "/dir/c.rs");
5088 });
5089
5090 // move the first worktree to after the third
5091 // [a, b, c] -> [b, c, a]
5092 project
5093 .update(cx, |project, cx| {
5094 let first = worktree_a.read(cx);
5095 let third = worktree_c.read(cx);
5096 project.move_worktree(first.id(), third.id(), cx)
5097 })
5098 .expect("moving first after third");
5099
5100 // check the state after moving
5101 project.update(cx, |project, cx| {
5102 let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
5103 assert_eq!(worktrees.len(), 3);
5104
5105 let first = worktrees[0].read(cx);
5106 let second = worktrees[1].read(cx);
5107 let third = worktrees[2].read(cx);
5108
5109 // check they are now in the right order
5110 assert_eq!(first.abs_path().to_str().unwrap(), "/dir/b.rs");
5111 assert_eq!(second.abs_path().to_str().unwrap(), "/dir/c.rs");
5112 assert_eq!(third.abs_path().to_str().unwrap(), "/dir/a.rs");
5113 });
5114
5115 // move the third worktree to before the first
5116 // [b, c, a] -> [a, b, c]
5117 project
5118 .update(cx, |project, cx| {
5119 let third = worktree_a.read(cx);
5120 let first = worktree_b.read(cx);
5121 project.move_worktree(third.id(), first.id(), cx)
5122 })
5123 .expect("moving third before first");
5124
5125 // check the state after moving
5126 project.update(cx, |project, cx| {
5127 let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
5128 assert_eq!(worktrees.len(), 3);
5129
5130 let first = worktrees[0].read(cx);
5131 let second = worktrees[1].read(cx);
5132 let third = worktrees[2].read(cx);
5133
5134 // check they are now in the right order
5135 assert_eq!(first.abs_path().to_str().unwrap(), "/dir/a.rs");
5136 assert_eq!(second.abs_path().to_str().unwrap(), "/dir/b.rs");
5137 assert_eq!(third.abs_path().to_str().unwrap(), "/dir/c.rs");
5138 });
5139}
5140
5141async fn search(
5142 project: &Model<Project>,
5143 query: SearchQuery,
5144 cx: &mut gpui::TestAppContext,
5145) -> Result<HashMap<String, Vec<Range<usize>>>> {
5146 let mut search_rx = project.update(cx, |project, cx| project.search(query, cx));
5147 let mut results = HashMap::default();
5148 while let Some(search_result) = search_rx.next().await {
5149 match search_result {
5150 SearchResult::Buffer { buffer, ranges } => {
5151 results.entry(buffer).or_insert(ranges);
5152 }
5153 SearchResult::LimitReached => {}
5154 }
5155 }
5156 Ok(results
5157 .into_iter()
5158 .map(|(buffer, ranges)| {
5159 buffer.update(cx, |buffer, cx| {
5160 let path = buffer
5161 .file()
5162 .unwrap()
5163 .full_path(cx)
5164 .to_string_lossy()
5165 .to_string();
5166 let ranges = ranges
5167 .into_iter()
5168 .map(|range| range.to_offset(buffer))
5169 .collect::<Vec<_>>();
5170 (path, ranges)
5171 })
5172 })
5173 .collect())
5174}
5175
5176fn init_test(cx: &mut gpui::TestAppContext) {
5177 if std::env::var("RUST_LOG").is_ok() {
5178 env_logger::try_init().ok();
5179 }
5180
5181 cx.update(|cx| {
5182 let settings_store = SettingsStore::test(cx);
5183 cx.set_global(settings_store);
5184 release_channel::init(SemanticVersion::default(), cx);
5185 language::init(cx);
5186 Project::init_settings(cx);
5187 });
5188}
5189
5190fn json_lang() -> Arc<Language> {
5191 Arc::new(Language::new(
5192 LanguageConfig {
5193 name: "JSON".into(),
5194 matcher: LanguageMatcher {
5195 path_suffixes: vec!["json".to_string()],
5196 ..Default::default()
5197 },
5198 ..Default::default()
5199 },
5200 None,
5201 ))
5202}
5203
5204fn js_lang() -> Arc<Language> {
5205 Arc::new(Language::new(
5206 LanguageConfig {
5207 name: Arc::from("JavaScript"),
5208 matcher: LanguageMatcher {
5209 path_suffixes: vec!["js".to_string()],
5210 ..Default::default()
5211 },
5212 ..Default::default()
5213 },
5214 None,
5215 ))
5216}
5217
5218fn rust_lang() -> Arc<Language> {
5219 Arc::new(Language::new(
5220 LanguageConfig {
5221 name: "Rust".into(),
5222 matcher: LanguageMatcher {
5223 path_suffixes: vec!["rs".to_string()],
5224 ..Default::default()
5225 },
5226 ..Default::default()
5227 },
5228 Some(tree_sitter_rust::language()),
5229 ))
5230}
5231
5232fn typescript_lang() -> Arc<Language> {
5233 Arc::new(Language::new(
5234 LanguageConfig {
5235 name: "TypeScript".into(),
5236 matcher: LanguageMatcher {
5237 path_suffixes: vec!["ts".to_string()],
5238 ..Default::default()
5239 },
5240 ..Default::default()
5241 },
5242 Some(tree_sitter_typescript::language_typescript()),
5243 ))
5244}
5245
5246fn tsx_lang() -> Arc<Language> {
5247 Arc::new(Language::new(
5248 LanguageConfig {
5249 name: "tsx".into(),
5250 matcher: LanguageMatcher {
5251 path_suffixes: vec!["tsx".to_string()],
5252 ..Default::default()
5253 },
5254 ..Default::default()
5255 },
5256 Some(tree_sitter_typescript::language_tsx()),
5257 ))
5258}
5259
5260fn get_all_tasks(
5261 project: &Model<Project>,
5262 worktree_id: Option<WorktreeId>,
5263 task_context: &TaskContext,
5264 cx: &mut AppContext,
5265) -> Task<Vec<(TaskSourceKind, ResolvedTask)>> {
5266 let resolved_tasks = project.update(cx, |project, cx| {
5267 project
5268 .task_inventory()
5269 .read(cx)
5270 .used_and_current_resolved_tasks(None, worktree_id, None, task_context, cx)
5271 });
5272
5273 cx.spawn(|_| async move {
5274 let (mut old, new) = resolved_tasks.await;
5275 old.extend(new);
5276 old
5277 })
5278}