1use crate::{worktree::WorktreeHandle, Event, *};
2use fs::LineEnding;
3use fs::{FakeFs, RealFs};
4use futures::{future, StreamExt};
5use gpui::{executor::Deterministic, test::subscribe};
6use language::{
7 tree_sitter_rust, tree_sitter_typescript, Diagnostic, FakeLspAdapter, LanguageConfig,
8 OffsetRangeExt, Point, ToPoint,
9};
10use lsp::Url;
11use pretty_assertions::assert_eq;
12use serde_json::json;
13use std::{cell::RefCell, os::unix, rc::Rc, task::Poll};
14use unindent::Unindent as _;
15use util::{assert_set_eq, test::temp_tree};
16
17#[cfg(test)]
18#[ctor::ctor]
19fn init_logger() {
20 if std::env::var("RUST_LOG").is_ok() {
21 env_logger::init();
22 }
23}
24
25#[gpui::test]
26async fn test_symlinks(cx: &mut gpui::TestAppContext) {
27 let dir = temp_tree(json!({
28 "root": {
29 "apple": "",
30 "banana": {
31 "carrot": {
32 "date": "",
33 "endive": "",
34 }
35 },
36 "fennel": {
37 "grape": "",
38 }
39 }
40 }));
41
42 let root_link_path = dir.path().join("root_link");
43 unix::fs::symlink(&dir.path().join("root"), &root_link_path).unwrap();
44 unix::fs::symlink(
45 &dir.path().join("root/fennel"),
46 &dir.path().join("root/finnochio"),
47 )
48 .unwrap();
49
50 let project = Project::test(Arc::new(RealFs), [root_link_path.as_ref()], cx).await;
51 project.read_with(cx, |project, cx| {
52 let tree = project.worktrees(cx).next().unwrap().read(cx);
53 assert_eq!(tree.file_count(), 5);
54 assert_eq!(
55 tree.inode_for_path("fennel/grape"),
56 tree.inode_for_path("finnochio/grape")
57 );
58 });
59}
60
61#[gpui::test]
62async fn test_managing_language_servers(
63 deterministic: Arc<Deterministic>,
64 cx: &mut gpui::TestAppContext,
65) {
66 cx.foreground().forbid_parking();
67
68 let mut rust_language = Language::new(
69 LanguageConfig {
70 name: "Rust".into(),
71 path_suffixes: vec!["rs".to_string()],
72 ..Default::default()
73 },
74 Some(tree_sitter_rust::language()),
75 );
76 let mut json_language = Language::new(
77 LanguageConfig {
78 name: "JSON".into(),
79 path_suffixes: vec!["json".to_string()],
80 ..Default::default()
81 },
82 None,
83 );
84 let mut fake_rust_servers = rust_language
85 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
86 name: "the-rust-language-server",
87 capabilities: lsp::ServerCapabilities {
88 completion_provider: Some(lsp::CompletionOptions {
89 trigger_characters: Some(vec![".".to_string(), "::".to_string()]),
90 ..Default::default()
91 }),
92 ..Default::default()
93 },
94 ..Default::default()
95 }))
96 .await;
97 let mut fake_json_servers = json_language
98 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
99 name: "the-json-language-server",
100 capabilities: lsp::ServerCapabilities {
101 completion_provider: Some(lsp::CompletionOptions {
102 trigger_characters: Some(vec![":".to_string()]),
103 ..Default::default()
104 }),
105 ..Default::default()
106 },
107 ..Default::default()
108 }))
109 .await;
110
111 let fs = FakeFs::new(cx.background());
112 fs.insert_tree(
113 "/the-root",
114 json!({
115 "test.rs": "const A: i32 = 1;",
116 "test2.rs": "",
117 "Cargo.toml": "a = 1",
118 "package.json": "{\"a\": 1}",
119 }),
120 )
121 .await;
122
123 let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await;
124
125 // Open a buffer without an associated language server.
126 let toml_buffer = project
127 .update(cx, |project, cx| {
128 project.open_local_buffer("/the-root/Cargo.toml", cx)
129 })
130 .await
131 .unwrap();
132
133 // Open a buffer with an associated language server before the language for it has been loaded.
134 let rust_buffer = project
135 .update(cx, |project, cx| {
136 project.open_local_buffer("/the-root/test.rs", cx)
137 })
138 .await
139 .unwrap();
140 rust_buffer.read_with(cx, |buffer, _| {
141 assert_eq!(buffer.language().map(|l| l.name()), None);
142 });
143
144 // Now we add the languages to the project, and ensure they get assigned to all
145 // the relevant open buffers.
146 project.update(cx, |project, _| {
147 project.languages.add(Arc::new(json_language));
148 project.languages.add(Arc::new(rust_language));
149 });
150 deterministic.run_until_parked();
151 rust_buffer.read_with(cx, |buffer, _| {
152 assert_eq!(buffer.language().map(|l| l.name()), Some("Rust".into()));
153 });
154
155 // A server is started up, and it is notified about Rust files.
156 let mut fake_rust_server = fake_rust_servers.next().await.unwrap();
157 assert_eq!(
158 fake_rust_server
159 .receive_notification::<lsp::notification::DidOpenTextDocument>()
160 .await
161 .text_document,
162 lsp::TextDocumentItem {
163 uri: lsp::Url::from_file_path("/the-root/test.rs").unwrap(),
164 version: 0,
165 text: "const A: i32 = 1;".to_string(),
166 language_id: Default::default()
167 }
168 );
169
170 // The buffer is configured based on the language server's capabilities.
171 rust_buffer.read_with(cx, |buffer, _| {
172 assert_eq!(
173 buffer.completion_triggers(),
174 &[".".to_string(), "::".to_string()]
175 );
176 });
177 toml_buffer.read_with(cx, |buffer, _| {
178 assert!(buffer.completion_triggers().is_empty());
179 });
180
181 // Edit a buffer. The changes are reported to the language server.
182 rust_buffer.update(cx, |buffer, cx| buffer.edit([(16..16, "2")], None, cx));
183 assert_eq!(
184 fake_rust_server
185 .receive_notification::<lsp::notification::DidChangeTextDocument>()
186 .await
187 .text_document,
188 lsp::VersionedTextDocumentIdentifier::new(
189 lsp::Url::from_file_path("/the-root/test.rs").unwrap(),
190 1
191 )
192 );
193
194 // Open a third buffer with a different associated language server.
195 let json_buffer = project
196 .update(cx, |project, cx| {
197 project.open_local_buffer("/the-root/package.json", cx)
198 })
199 .await
200 .unwrap();
201
202 // A json language server is started up and is only notified about the json buffer.
203 let mut fake_json_server = fake_json_servers.next().await.unwrap();
204 assert_eq!(
205 fake_json_server
206 .receive_notification::<lsp::notification::DidOpenTextDocument>()
207 .await
208 .text_document,
209 lsp::TextDocumentItem {
210 uri: lsp::Url::from_file_path("/the-root/package.json").unwrap(),
211 version: 0,
212 text: "{\"a\": 1}".to_string(),
213 language_id: Default::default()
214 }
215 );
216
217 // This buffer is configured based on the second language server's
218 // capabilities.
219 json_buffer.read_with(cx, |buffer, _| {
220 assert_eq!(buffer.completion_triggers(), &[":".to_string()]);
221 });
222
223 // When opening another buffer whose language server is already running,
224 // it is also configured based on the existing language server's capabilities.
225 let rust_buffer2 = project
226 .update(cx, |project, cx| {
227 project.open_local_buffer("/the-root/test2.rs", cx)
228 })
229 .await
230 .unwrap();
231 rust_buffer2.read_with(cx, |buffer, _| {
232 assert_eq!(
233 buffer.completion_triggers(),
234 &[".".to_string(), "::".to_string()]
235 );
236 });
237
238 // Changes are reported only to servers matching the buffer's language.
239 toml_buffer.update(cx, |buffer, cx| buffer.edit([(5..5, "23")], None, cx));
240 rust_buffer2.update(cx, |buffer, cx| {
241 buffer.edit([(0..0, "let x = 1;")], None, cx)
242 });
243 assert_eq!(
244 fake_rust_server
245 .receive_notification::<lsp::notification::DidChangeTextDocument>()
246 .await
247 .text_document,
248 lsp::VersionedTextDocumentIdentifier::new(
249 lsp::Url::from_file_path("/the-root/test2.rs").unwrap(),
250 1
251 )
252 );
253
254 // Save notifications are reported to all servers.
255 project
256 .update(cx, |project, cx| project.save_buffer(toml_buffer, cx))
257 .await
258 .unwrap();
259 assert_eq!(
260 fake_rust_server
261 .receive_notification::<lsp::notification::DidSaveTextDocument>()
262 .await
263 .text_document,
264 lsp::TextDocumentIdentifier::new(lsp::Url::from_file_path("/the-root/Cargo.toml").unwrap())
265 );
266 assert_eq!(
267 fake_json_server
268 .receive_notification::<lsp::notification::DidSaveTextDocument>()
269 .await
270 .text_document,
271 lsp::TextDocumentIdentifier::new(lsp::Url::from_file_path("/the-root/Cargo.toml").unwrap())
272 );
273
274 // Renames are reported only to servers matching the buffer's language.
275 fs.rename(
276 Path::new("/the-root/test2.rs"),
277 Path::new("/the-root/test3.rs"),
278 Default::default(),
279 )
280 .await
281 .unwrap();
282 assert_eq!(
283 fake_rust_server
284 .receive_notification::<lsp::notification::DidCloseTextDocument>()
285 .await
286 .text_document,
287 lsp::TextDocumentIdentifier::new(lsp::Url::from_file_path("/the-root/test2.rs").unwrap()),
288 );
289 assert_eq!(
290 fake_rust_server
291 .receive_notification::<lsp::notification::DidOpenTextDocument>()
292 .await
293 .text_document,
294 lsp::TextDocumentItem {
295 uri: lsp::Url::from_file_path("/the-root/test3.rs").unwrap(),
296 version: 0,
297 text: rust_buffer2.read_with(cx, |buffer, _| buffer.text()),
298 language_id: Default::default()
299 },
300 );
301
302 rust_buffer2.update(cx, |buffer, cx| {
303 buffer.update_diagnostics(
304 DiagnosticSet::from_sorted_entries(
305 vec![DiagnosticEntry {
306 diagnostic: Default::default(),
307 range: Anchor::MIN..Anchor::MAX,
308 }],
309 &buffer.snapshot(),
310 ),
311 cx,
312 );
313 assert_eq!(
314 buffer
315 .snapshot()
316 .diagnostics_in_range::<_, usize>(0..buffer.len(), false)
317 .count(),
318 1
319 );
320 });
321
322 // When the rename changes the extension of the file, the buffer gets closed on the old
323 // language server and gets opened on the new one.
324 fs.rename(
325 Path::new("/the-root/test3.rs"),
326 Path::new("/the-root/test3.json"),
327 Default::default(),
328 )
329 .await
330 .unwrap();
331 assert_eq!(
332 fake_rust_server
333 .receive_notification::<lsp::notification::DidCloseTextDocument>()
334 .await
335 .text_document,
336 lsp::TextDocumentIdentifier::new(lsp::Url::from_file_path("/the-root/test3.rs").unwrap(),),
337 );
338 assert_eq!(
339 fake_json_server
340 .receive_notification::<lsp::notification::DidOpenTextDocument>()
341 .await
342 .text_document,
343 lsp::TextDocumentItem {
344 uri: lsp::Url::from_file_path("/the-root/test3.json").unwrap(),
345 version: 0,
346 text: rust_buffer2.read_with(cx, |buffer, _| buffer.text()),
347 language_id: Default::default()
348 },
349 );
350
351 // We clear the diagnostics, since the language has changed.
352 rust_buffer2.read_with(cx, |buffer, _| {
353 assert_eq!(
354 buffer
355 .snapshot()
356 .diagnostics_in_range::<_, usize>(0..buffer.len(), false)
357 .count(),
358 0
359 );
360 });
361
362 // The renamed file's version resets after changing language server.
363 rust_buffer2.update(cx, |buffer, cx| buffer.edit([(0..0, "// ")], None, cx));
364 assert_eq!(
365 fake_json_server
366 .receive_notification::<lsp::notification::DidChangeTextDocument>()
367 .await
368 .text_document,
369 lsp::VersionedTextDocumentIdentifier::new(
370 lsp::Url::from_file_path("/the-root/test3.json").unwrap(),
371 1
372 )
373 );
374
375 // Restart language servers
376 project.update(cx, |project, cx| {
377 project.restart_language_servers_for_buffers(
378 vec![rust_buffer.clone(), json_buffer.clone()],
379 cx,
380 );
381 });
382
383 let mut rust_shutdown_requests = fake_rust_server
384 .handle_request::<lsp::request::Shutdown, _, _>(|_, _| future::ready(Ok(())));
385 let mut json_shutdown_requests = fake_json_server
386 .handle_request::<lsp::request::Shutdown, _, _>(|_, _| future::ready(Ok(())));
387 futures::join!(rust_shutdown_requests.next(), json_shutdown_requests.next());
388
389 let mut fake_rust_server = fake_rust_servers.next().await.unwrap();
390 let mut fake_json_server = fake_json_servers.next().await.unwrap();
391
392 // Ensure rust document is reopened in new rust language server
393 assert_eq!(
394 fake_rust_server
395 .receive_notification::<lsp::notification::DidOpenTextDocument>()
396 .await
397 .text_document,
398 lsp::TextDocumentItem {
399 uri: lsp::Url::from_file_path("/the-root/test.rs").unwrap(),
400 version: 1,
401 text: rust_buffer.read_with(cx, |buffer, _| buffer.text()),
402 language_id: Default::default()
403 }
404 );
405
406 // Ensure json documents are reopened in new json language server
407 assert_set_eq!(
408 [
409 fake_json_server
410 .receive_notification::<lsp::notification::DidOpenTextDocument>()
411 .await
412 .text_document,
413 fake_json_server
414 .receive_notification::<lsp::notification::DidOpenTextDocument>()
415 .await
416 .text_document,
417 ],
418 [
419 lsp::TextDocumentItem {
420 uri: lsp::Url::from_file_path("/the-root/package.json").unwrap(),
421 version: 0,
422 text: json_buffer.read_with(cx, |buffer, _| buffer.text()),
423 language_id: Default::default()
424 },
425 lsp::TextDocumentItem {
426 uri: lsp::Url::from_file_path("/the-root/test3.json").unwrap(),
427 version: 1,
428 text: rust_buffer2.read_with(cx, |buffer, _| buffer.text()),
429 language_id: Default::default()
430 }
431 ]
432 );
433
434 // Close notifications are reported only to servers matching the buffer's language.
435 cx.update(|_| drop(json_buffer));
436 let close_message = lsp::DidCloseTextDocumentParams {
437 text_document: lsp::TextDocumentIdentifier::new(
438 lsp::Url::from_file_path("/the-root/package.json").unwrap(),
439 ),
440 };
441 assert_eq!(
442 fake_json_server
443 .receive_notification::<lsp::notification::DidCloseTextDocument>()
444 .await,
445 close_message,
446 );
447}
448
449#[gpui::test]
450async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppContext) {
451 cx.foreground().forbid_parking();
452
453 let mut language = Language::new(
454 LanguageConfig {
455 name: "Rust".into(),
456 path_suffixes: vec!["rs".to_string()],
457 ..Default::default()
458 },
459 Some(tree_sitter_rust::language()),
460 );
461 let mut fake_servers = language
462 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
463 name: "the-language-server",
464 ..Default::default()
465 }))
466 .await;
467
468 let fs = FakeFs::new(cx.background());
469 fs.insert_tree(
470 "/the-root",
471 json!({
472 "a.rs": "",
473 "b.rs": "",
474 }),
475 )
476 .await;
477
478 let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await;
479 project.update(cx, |project, _| {
480 project.languages.add(Arc::new(language));
481 });
482 cx.foreground().run_until_parked();
483
484 // Start the language server by opening a buffer with a compatible file extension.
485 let _buffer = project
486 .update(cx, |project, cx| {
487 project.open_local_buffer("/the-root/a.rs", cx)
488 })
489 .await
490 .unwrap();
491
492 // Keep track of the FS events reported to the language server.
493 let fake_server = fake_servers.next().await.unwrap();
494 let file_changes = Arc::new(Mutex::new(Vec::new()));
495 fake_server.handle_notification::<lsp::notification::DidChangeWatchedFiles, _>({
496 let file_changes = file_changes.clone();
497 move |params, _| {
498 let mut file_changes = file_changes.lock();
499 file_changes.extend(params.changes);
500 file_changes.sort_by(|a, b| a.uri.cmp(&b.uri));
501 }
502 });
503
504 cx.foreground().run_until_parked();
505 assert_eq!(file_changes.lock().len(), 0);
506
507 // Perform some file system mutations.
508 fs.create_file("/the-root/c.rs".as_ref(), Default::default())
509 .await
510 .unwrap();
511 fs.remove_file("/the-root/b.rs".as_ref(), Default::default())
512 .await
513 .unwrap();
514
515 // The language server receives events for both FS mutations.
516 cx.foreground().run_until_parked();
517 assert_eq!(
518 &*file_changes.lock(),
519 &[
520 lsp::FileEvent {
521 uri: lsp::Url::from_file_path("/the-root/b.rs").unwrap(),
522 typ: lsp::FileChangeType::DELETED,
523 },
524 lsp::FileEvent {
525 uri: lsp::Url::from_file_path("/the-root/c.rs").unwrap(),
526 typ: lsp::FileChangeType::CREATED,
527 },
528 ]
529 );
530}
531
532#[gpui::test]
533async fn test_single_file_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
534 cx.foreground().forbid_parking();
535
536 let fs = FakeFs::new(cx.background());
537 fs.insert_tree(
538 "/dir",
539 json!({
540 "a.rs": "let a = 1;",
541 "b.rs": "let b = 2;"
542 }),
543 )
544 .await;
545
546 let project = Project::test(fs, ["/dir/a.rs".as_ref(), "/dir/b.rs".as_ref()], cx).await;
547
548 let buffer_a = project
549 .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
550 .await
551 .unwrap();
552 let buffer_b = project
553 .update(cx, |project, cx| project.open_local_buffer("/dir/b.rs", cx))
554 .await
555 .unwrap();
556
557 project.update(cx, |project, cx| {
558 project
559 .update_diagnostics(
560 0,
561 lsp::PublishDiagnosticsParams {
562 uri: Url::from_file_path("/dir/a.rs").unwrap(),
563 version: None,
564 diagnostics: vec![lsp::Diagnostic {
565 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 5)),
566 severity: Some(lsp::DiagnosticSeverity::ERROR),
567 message: "error 1".to_string(),
568 ..Default::default()
569 }],
570 },
571 &[],
572 cx,
573 )
574 .unwrap();
575 project
576 .update_diagnostics(
577 0,
578 lsp::PublishDiagnosticsParams {
579 uri: Url::from_file_path("/dir/b.rs").unwrap(),
580 version: None,
581 diagnostics: vec![lsp::Diagnostic {
582 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 5)),
583 severity: Some(lsp::DiagnosticSeverity::WARNING),
584 message: "error 2".to_string(),
585 ..Default::default()
586 }],
587 },
588 &[],
589 cx,
590 )
591 .unwrap();
592 });
593
594 buffer_a.read_with(cx, |buffer, _| {
595 let chunks = chunks_with_diagnostics(buffer, 0..buffer.len());
596 assert_eq!(
597 chunks
598 .iter()
599 .map(|(s, d)| (s.as_str(), *d))
600 .collect::<Vec<_>>(),
601 &[
602 ("let ", None),
603 ("a", Some(DiagnosticSeverity::ERROR)),
604 (" = 1;", None),
605 ]
606 );
607 });
608 buffer_b.read_with(cx, |buffer, _| {
609 let chunks = chunks_with_diagnostics(buffer, 0..buffer.len());
610 assert_eq!(
611 chunks
612 .iter()
613 .map(|(s, d)| (s.as_str(), *d))
614 .collect::<Vec<_>>(),
615 &[
616 ("let ", None),
617 ("b", Some(DiagnosticSeverity::WARNING)),
618 (" = 2;", None),
619 ]
620 );
621 });
622}
623
624#[gpui::test]
625async fn test_hidden_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
626 cx.foreground().forbid_parking();
627
628 let fs = FakeFs::new(cx.background());
629 fs.insert_tree(
630 "/root",
631 json!({
632 "dir": {
633 "a.rs": "let a = 1;",
634 },
635 "other.rs": "let b = c;"
636 }),
637 )
638 .await;
639
640 let project = Project::test(fs, ["/root/dir".as_ref()], cx).await;
641
642 let (worktree, _) = project
643 .update(cx, |project, cx| {
644 project.find_or_create_local_worktree("/root/other.rs", false, cx)
645 })
646 .await
647 .unwrap();
648 let worktree_id = worktree.read_with(cx, |tree, _| tree.id());
649
650 project.update(cx, |project, cx| {
651 project
652 .update_diagnostics(
653 0,
654 lsp::PublishDiagnosticsParams {
655 uri: Url::from_file_path("/root/other.rs").unwrap(),
656 version: None,
657 diagnostics: vec![lsp::Diagnostic {
658 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 9)),
659 severity: Some(lsp::DiagnosticSeverity::ERROR),
660 message: "unknown variable 'c'".to_string(),
661 ..Default::default()
662 }],
663 },
664 &[],
665 cx,
666 )
667 .unwrap();
668 });
669
670 let buffer = project
671 .update(cx, |project, cx| project.open_buffer((worktree_id, ""), cx))
672 .await
673 .unwrap();
674 buffer.read_with(cx, |buffer, _| {
675 let chunks = chunks_with_diagnostics(buffer, 0..buffer.len());
676 assert_eq!(
677 chunks
678 .iter()
679 .map(|(s, d)| (s.as_str(), *d))
680 .collect::<Vec<_>>(),
681 &[
682 ("let b = ", None),
683 ("c", Some(DiagnosticSeverity::ERROR)),
684 (";", None),
685 ]
686 );
687 });
688
689 project.read_with(cx, |project, cx| {
690 assert_eq!(project.diagnostic_summaries(cx).next(), None);
691 assert_eq!(project.diagnostic_summary(cx).error_count, 0);
692 });
693}
694
695#[gpui::test]
696async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) {
697 cx.foreground().forbid_parking();
698
699 let progress_token = "the-progress-token";
700 let mut language = Language::new(
701 LanguageConfig {
702 name: "Rust".into(),
703 path_suffixes: vec!["rs".to_string()],
704 ..Default::default()
705 },
706 Some(tree_sitter_rust::language()),
707 );
708 let mut fake_servers = language
709 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
710 disk_based_diagnostics_progress_token: Some(progress_token.into()),
711 disk_based_diagnostics_sources: vec!["disk".into()],
712 ..Default::default()
713 }))
714 .await;
715
716 let fs = FakeFs::new(cx.background());
717 fs.insert_tree(
718 "/dir",
719 json!({
720 "a.rs": "fn a() { A }",
721 "b.rs": "const y: i32 = 1",
722 }),
723 )
724 .await;
725
726 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
727 project.update(cx, |project, _| project.languages.add(Arc::new(language)));
728 let worktree_id = project.read_with(cx, |p, cx| p.worktrees(cx).next().unwrap().read(cx).id());
729
730 // Cause worktree to start the fake language server
731 let _buffer = project
732 .update(cx, |project, cx| project.open_local_buffer("/dir/b.rs", cx))
733 .await
734 .unwrap();
735
736 let mut events = subscribe(&project, cx);
737
738 let fake_server = fake_servers.next().await.unwrap();
739 fake_server
740 .start_progress(format!("{}/0", progress_token))
741 .await;
742 assert_eq!(
743 events.next().await.unwrap(),
744 Event::DiskBasedDiagnosticsStarted {
745 language_server_id: 0,
746 }
747 );
748
749 fake_server.notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
750 uri: Url::from_file_path("/dir/a.rs").unwrap(),
751 version: None,
752 diagnostics: vec![lsp::Diagnostic {
753 range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
754 severity: Some(lsp::DiagnosticSeverity::ERROR),
755 message: "undefined variable 'A'".to_string(),
756 ..Default::default()
757 }],
758 });
759 assert_eq!(
760 events.next().await.unwrap(),
761 Event::DiagnosticsUpdated {
762 language_server_id: 0,
763 path: (worktree_id, Path::new("a.rs")).into()
764 }
765 );
766
767 fake_server.end_progress(format!("{}/0", progress_token));
768 assert_eq!(
769 events.next().await.unwrap(),
770 Event::DiskBasedDiagnosticsFinished {
771 language_server_id: 0
772 }
773 );
774
775 let buffer = project
776 .update(cx, |p, cx| p.open_local_buffer("/dir/a.rs", cx))
777 .await
778 .unwrap();
779
780 buffer.read_with(cx, |buffer, _| {
781 let snapshot = buffer.snapshot();
782 let diagnostics = snapshot
783 .diagnostics_in_range::<_, Point>(0..buffer.len(), false)
784 .collect::<Vec<_>>();
785 assert_eq!(
786 diagnostics,
787 &[DiagnosticEntry {
788 range: Point::new(0, 9)..Point::new(0, 10),
789 diagnostic: Diagnostic {
790 severity: lsp::DiagnosticSeverity::ERROR,
791 message: "undefined variable 'A'".to_string(),
792 group_id: 0,
793 is_primary: true,
794 ..Default::default()
795 }
796 }]
797 )
798 });
799
800 // Ensure publishing empty diagnostics twice only results in one update event.
801 fake_server.notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
802 uri: Url::from_file_path("/dir/a.rs").unwrap(),
803 version: None,
804 diagnostics: Default::default(),
805 });
806 assert_eq!(
807 events.next().await.unwrap(),
808 Event::DiagnosticsUpdated {
809 language_server_id: 0,
810 path: (worktree_id, Path::new("a.rs")).into()
811 }
812 );
813
814 fake_server.notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
815 uri: Url::from_file_path("/dir/a.rs").unwrap(),
816 version: None,
817 diagnostics: Default::default(),
818 });
819 cx.foreground().run_until_parked();
820 assert_eq!(futures::poll!(events.next()), Poll::Pending);
821}
822
823#[gpui::test]
824async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppContext) {
825 cx.foreground().forbid_parking();
826
827 let progress_token = "the-progress-token";
828 let mut language = Language::new(
829 LanguageConfig {
830 path_suffixes: vec!["rs".to_string()],
831 ..Default::default()
832 },
833 None,
834 );
835 let mut fake_servers = language
836 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
837 disk_based_diagnostics_sources: vec!["disk".into()],
838 disk_based_diagnostics_progress_token: Some(progress_token.into()),
839 ..Default::default()
840 }))
841 .await;
842
843 let fs = FakeFs::new(cx.background());
844 fs.insert_tree("/dir", json!({ "a.rs": "" })).await;
845
846 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
847 project.update(cx, |project, _| project.languages.add(Arc::new(language)));
848
849 let buffer = project
850 .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
851 .await
852 .unwrap();
853
854 // Simulate diagnostics starting to update.
855 let fake_server = fake_servers.next().await.unwrap();
856 fake_server.start_progress(progress_token).await;
857
858 // Restart the server before the diagnostics finish updating.
859 project.update(cx, |project, cx| {
860 project.restart_language_servers_for_buffers([buffer], cx);
861 });
862 let mut events = subscribe(&project, cx);
863
864 // Simulate the newly started server sending more diagnostics.
865 let fake_server = fake_servers.next().await.unwrap();
866 fake_server.start_progress(progress_token).await;
867 assert_eq!(
868 events.next().await.unwrap(),
869 Event::DiskBasedDiagnosticsStarted {
870 language_server_id: 1
871 }
872 );
873 project.read_with(cx, |project, _| {
874 assert_eq!(
875 project
876 .language_servers_running_disk_based_diagnostics()
877 .collect::<Vec<_>>(),
878 [1]
879 );
880 });
881
882 // All diagnostics are considered done, despite the old server's diagnostic
883 // task never completing.
884 fake_server.end_progress(progress_token);
885 assert_eq!(
886 events.next().await.unwrap(),
887 Event::DiskBasedDiagnosticsFinished {
888 language_server_id: 1
889 }
890 );
891 project.read_with(cx, |project, _| {
892 assert_eq!(
893 project
894 .language_servers_running_disk_based_diagnostics()
895 .collect::<Vec<_>>(),
896 [0; 0]
897 );
898 });
899}
900
901#[gpui::test]
902async fn test_restarted_server_reporting_invalid_buffer_version(cx: &mut gpui::TestAppContext) {
903 cx.foreground().forbid_parking();
904
905 let mut language = Language::new(
906 LanguageConfig {
907 path_suffixes: vec!["rs".to_string()],
908 ..Default::default()
909 },
910 None,
911 );
912 let mut fake_servers = language
913 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
914 name: "the-lsp",
915 ..Default::default()
916 }))
917 .await;
918
919 let fs = FakeFs::new(cx.background());
920 fs.insert_tree("/dir", json!({ "a.rs": "" })).await;
921
922 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
923 project.update(cx, |project, _| project.languages.add(Arc::new(language)));
924
925 let buffer = project
926 .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
927 .await
928 .unwrap();
929
930 // Before restarting the server, report diagnostics with an unknown buffer version.
931 let fake_server = fake_servers.next().await.unwrap();
932 fake_server.notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
933 uri: lsp::Url::from_file_path("/dir/a.rs").unwrap(),
934 version: Some(10000),
935 diagnostics: Vec::new(),
936 });
937 cx.foreground().run_until_parked();
938
939 project.update(cx, |project, cx| {
940 project.restart_language_servers_for_buffers([buffer.clone()], cx);
941 });
942 let mut fake_server = fake_servers.next().await.unwrap();
943 let notification = fake_server
944 .receive_notification::<lsp::notification::DidOpenTextDocument>()
945 .await
946 .text_document;
947 assert_eq!(notification.version, 0);
948}
949
950#[gpui::test]
951async fn test_toggling_enable_language_server(
952 deterministic: Arc<Deterministic>,
953 cx: &mut gpui::TestAppContext,
954) {
955 deterministic.forbid_parking();
956
957 let mut rust = Language::new(
958 LanguageConfig {
959 name: Arc::from("Rust"),
960 path_suffixes: vec!["rs".to_string()],
961 ..Default::default()
962 },
963 None,
964 );
965 let mut fake_rust_servers = rust
966 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
967 name: "rust-lsp",
968 ..Default::default()
969 }))
970 .await;
971 let mut js = Language::new(
972 LanguageConfig {
973 name: Arc::from("JavaScript"),
974 path_suffixes: vec!["js".to_string()],
975 ..Default::default()
976 },
977 None,
978 );
979 let mut fake_js_servers = js
980 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
981 name: "js-lsp",
982 ..Default::default()
983 }))
984 .await;
985
986 let fs = FakeFs::new(cx.background());
987 fs.insert_tree("/dir", json!({ "a.rs": "", "b.js": "" }))
988 .await;
989
990 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
991 project.update(cx, |project, _| {
992 project.languages.add(Arc::new(rust));
993 project.languages.add(Arc::new(js));
994 });
995
996 let _rs_buffer = project
997 .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
998 .await
999 .unwrap();
1000 let _js_buffer = project
1001 .update(cx, |project, cx| project.open_local_buffer("/dir/b.js", cx))
1002 .await
1003 .unwrap();
1004
1005 let mut fake_rust_server_1 = fake_rust_servers.next().await.unwrap();
1006 assert_eq!(
1007 fake_rust_server_1
1008 .receive_notification::<lsp::notification::DidOpenTextDocument>()
1009 .await
1010 .text_document
1011 .uri
1012 .as_str(),
1013 "file:///dir/a.rs"
1014 );
1015
1016 let mut fake_js_server = fake_js_servers.next().await.unwrap();
1017 assert_eq!(
1018 fake_js_server
1019 .receive_notification::<lsp::notification::DidOpenTextDocument>()
1020 .await
1021 .text_document
1022 .uri
1023 .as_str(),
1024 "file:///dir/b.js"
1025 );
1026
1027 // Disable Rust language server, ensuring only that server gets stopped.
1028 cx.update(|cx| {
1029 cx.update_global(|settings: &mut Settings, _| {
1030 settings.language_overrides.insert(
1031 Arc::from("Rust"),
1032 settings::EditorSettings {
1033 enable_language_server: Some(false),
1034 ..Default::default()
1035 },
1036 );
1037 })
1038 });
1039 fake_rust_server_1
1040 .receive_notification::<lsp::notification::Exit>()
1041 .await;
1042
1043 // Enable Rust and disable JavaScript language servers, ensuring that the
1044 // former gets started again and that the latter stops.
1045 cx.update(|cx| {
1046 cx.update_global(|settings: &mut Settings, _| {
1047 settings.language_overrides.insert(
1048 Arc::from("Rust"),
1049 settings::EditorSettings {
1050 enable_language_server: Some(true),
1051 ..Default::default()
1052 },
1053 );
1054 settings.language_overrides.insert(
1055 Arc::from("JavaScript"),
1056 settings::EditorSettings {
1057 enable_language_server: Some(false),
1058 ..Default::default()
1059 },
1060 );
1061 })
1062 });
1063 let mut fake_rust_server_2 = fake_rust_servers.next().await.unwrap();
1064 assert_eq!(
1065 fake_rust_server_2
1066 .receive_notification::<lsp::notification::DidOpenTextDocument>()
1067 .await
1068 .text_document
1069 .uri
1070 .as_str(),
1071 "file:///dir/a.rs"
1072 );
1073 fake_js_server
1074 .receive_notification::<lsp::notification::Exit>()
1075 .await;
1076}
1077
1078#[gpui::test]
1079async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) {
1080 cx.foreground().forbid_parking();
1081
1082 let mut language = Language::new(
1083 LanguageConfig {
1084 name: "Rust".into(),
1085 path_suffixes: vec!["rs".to_string()],
1086 ..Default::default()
1087 },
1088 Some(tree_sitter_rust::language()),
1089 );
1090 let mut fake_servers = language
1091 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
1092 disk_based_diagnostics_sources: vec!["disk".into()],
1093 ..Default::default()
1094 }))
1095 .await;
1096
1097 let text = "
1098 fn a() { A }
1099 fn b() { BB }
1100 fn c() { CCC }
1101 "
1102 .unindent();
1103
1104 let fs = FakeFs::new(cx.background());
1105 fs.insert_tree("/dir", json!({ "a.rs": text })).await;
1106
1107 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
1108 project.update(cx, |project, _| project.languages.add(Arc::new(language)));
1109
1110 let buffer = project
1111 .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
1112 .await
1113 .unwrap();
1114
1115 let mut fake_server = fake_servers.next().await.unwrap();
1116 let open_notification = fake_server
1117 .receive_notification::<lsp::notification::DidOpenTextDocument>()
1118 .await;
1119
1120 // Edit the buffer, moving the content down
1121 buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "\n\n")], None, cx));
1122 let change_notification_1 = fake_server
1123 .receive_notification::<lsp::notification::DidChangeTextDocument>()
1124 .await;
1125 assert!(change_notification_1.text_document.version > open_notification.text_document.version);
1126
1127 // Report some diagnostics for the initial version of the buffer
1128 fake_server.notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
1129 uri: lsp::Url::from_file_path("/dir/a.rs").unwrap(),
1130 version: Some(open_notification.text_document.version),
1131 diagnostics: vec![
1132 lsp::Diagnostic {
1133 range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
1134 severity: Some(DiagnosticSeverity::ERROR),
1135 message: "undefined variable 'A'".to_string(),
1136 source: Some("disk".to_string()),
1137 ..Default::default()
1138 },
1139 lsp::Diagnostic {
1140 range: lsp::Range::new(lsp::Position::new(1, 9), lsp::Position::new(1, 11)),
1141 severity: Some(DiagnosticSeverity::ERROR),
1142 message: "undefined variable 'BB'".to_string(),
1143 source: Some("disk".to_string()),
1144 ..Default::default()
1145 },
1146 lsp::Diagnostic {
1147 range: lsp::Range::new(lsp::Position::new(2, 9), lsp::Position::new(2, 12)),
1148 severity: Some(DiagnosticSeverity::ERROR),
1149 source: Some("disk".to_string()),
1150 message: "undefined variable 'CCC'".to_string(),
1151 ..Default::default()
1152 },
1153 ],
1154 });
1155
1156 // The diagnostics have moved down since they were created.
1157 buffer.next_notification(cx).await;
1158 buffer.read_with(cx, |buffer, _| {
1159 assert_eq!(
1160 buffer
1161 .snapshot()
1162 .diagnostics_in_range::<_, Point>(Point::new(3, 0)..Point::new(5, 0), false)
1163 .collect::<Vec<_>>(),
1164 &[
1165 DiagnosticEntry {
1166 range: Point::new(3, 9)..Point::new(3, 11),
1167 diagnostic: Diagnostic {
1168 severity: DiagnosticSeverity::ERROR,
1169 message: "undefined variable 'BB'".to_string(),
1170 is_disk_based: true,
1171 group_id: 1,
1172 is_primary: true,
1173 ..Default::default()
1174 },
1175 },
1176 DiagnosticEntry {
1177 range: Point::new(4, 9)..Point::new(4, 12),
1178 diagnostic: Diagnostic {
1179 severity: DiagnosticSeverity::ERROR,
1180 message: "undefined variable 'CCC'".to_string(),
1181 is_disk_based: true,
1182 group_id: 2,
1183 is_primary: true,
1184 ..Default::default()
1185 }
1186 }
1187 ]
1188 );
1189 assert_eq!(
1190 chunks_with_diagnostics(buffer, 0..buffer.len()),
1191 [
1192 ("\n\nfn a() { ".to_string(), None),
1193 ("A".to_string(), Some(DiagnosticSeverity::ERROR)),
1194 (" }\nfn b() { ".to_string(), None),
1195 ("BB".to_string(), Some(DiagnosticSeverity::ERROR)),
1196 (" }\nfn c() { ".to_string(), None),
1197 ("CCC".to_string(), Some(DiagnosticSeverity::ERROR)),
1198 (" }\n".to_string(), None),
1199 ]
1200 );
1201 assert_eq!(
1202 chunks_with_diagnostics(buffer, Point::new(3, 10)..Point::new(4, 11)),
1203 [
1204 ("B".to_string(), Some(DiagnosticSeverity::ERROR)),
1205 (" }\nfn c() { ".to_string(), None),
1206 ("CC".to_string(), Some(DiagnosticSeverity::ERROR)),
1207 ]
1208 );
1209 });
1210
1211 // Ensure overlapping diagnostics are highlighted correctly.
1212 fake_server.notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
1213 uri: lsp::Url::from_file_path("/dir/a.rs").unwrap(),
1214 version: Some(open_notification.text_document.version),
1215 diagnostics: vec![
1216 lsp::Diagnostic {
1217 range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
1218 severity: Some(DiagnosticSeverity::ERROR),
1219 message: "undefined variable 'A'".to_string(),
1220 source: Some("disk".to_string()),
1221 ..Default::default()
1222 },
1223 lsp::Diagnostic {
1224 range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 12)),
1225 severity: Some(DiagnosticSeverity::WARNING),
1226 message: "unreachable statement".to_string(),
1227 source: Some("disk".to_string()),
1228 ..Default::default()
1229 },
1230 ],
1231 });
1232
1233 buffer.next_notification(cx).await;
1234 buffer.read_with(cx, |buffer, _| {
1235 assert_eq!(
1236 buffer
1237 .snapshot()
1238 .diagnostics_in_range::<_, Point>(Point::new(2, 0)..Point::new(3, 0), false)
1239 .collect::<Vec<_>>(),
1240 &[
1241 DiagnosticEntry {
1242 range: Point::new(2, 9)..Point::new(2, 12),
1243 diagnostic: Diagnostic {
1244 severity: DiagnosticSeverity::WARNING,
1245 message: "unreachable statement".to_string(),
1246 is_disk_based: true,
1247 group_id: 4,
1248 is_primary: true,
1249 ..Default::default()
1250 }
1251 },
1252 DiagnosticEntry {
1253 range: Point::new(2, 9)..Point::new(2, 10),
1254 diagnostic: Diagnostic {
1255 severity: DiagnosticSeverity::ERROR,
1256 message: "undefined variable 'A'".to_string(),
1257 is_disk_based: true,
1258 group_id: 3,
1259 is_primary: true,
1260 ..Default::default()
1261 },
1262 }
1263 ]
1264 );
1265 assert_eq!(
1266 chunks_with_diagnostics(buffer, Point::new(2, 0)..Point::new(3, 0)),
1267 [
1268 ("fn a() { ".to_string(), None),
1269 ("A".to_string(), Some(DiagnosticSeverity::ERROR)),
1270 (" }".to_string(), Some(DiagnosticSeverity::WARNING)),
1271 ("\n".to_string(), None),
1272 ]
1273 );
1274 assert_eq!(
1275 chunks_with_diagnostics(buffer, Point::new(2, 10)..Point::new(3, 0)),
1276 [
1277 (" }".to_string(), Some(DiagnosticSeverity::WARNING)),
1278 ("\n".to_string(), None),
1279 ]
1280 );
1281 });
1282
1283 // Keep editing the buffer and ensure disk-based diagnostics get translated according to the
1284 // changes since the last save.
1285 buffer.update(cx, |buffer, cx| {
1286 buffer.edit([(Point::new(2, 0)..Point::new(2, 0), " ")], None, cx);
1287 buffer.edit(
1288 [(Point::new(2, 8)..Point::new(2, 10), "(x: usize)")],
1289 None,
1290 cx,
1291 );
1292 buffer.edit([(Point::new(3, 10)..Point::new(3, 10), "xxx")], None, cx);
1293 });
1294 let change_notification_2 = fake_server
1295 .receive_notification::<lsp::notification::DidChangeTextDocument>()
1296 .await;
1297 assert!(
1298 change_notification_2.text_document.version > change_notification_1.text_document.version
1299 );
1300
1301 // Handle out-of-order diagnostics
1302 fake_server.notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
1303 uri: lsp::Url::from_file_path("/dir/a.rs").unwrap(),
1304 version: Some(change_notification_2.text_document.version),
1305 diagnostics: vec![
1306 lsp::Diagnostic {
1307 range: lsp::Range::new(lsp::Position::new(1, 9), lsp::Position::new(1, 11)),
1308 severity: Some(DiagnosticSeverity::ERROR),
1309 message: "undefined variable 'BB'".to_string(),
1310 source: Some("disk".to_string()),
1311 ..Default::default()
1312 },
1313 lsp::Diagnostic {
1314 range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
1315 severity: Some(DiagnosticSeverity::WARNING),
1316 message: "undefined variable 'A'".to_string(),
1317 source: Some("disk".to_string()),
1318 ..Default::default()
1319 },
1320 ],
1321 });
1322
1323 buffer.next_notification(cx).await;
1324 buffer.read_with(cx, |buffer, _| {
1325 assert_eq!(
1326 buffer
1327 .snapshot()
1328 .diagnostics_in_range::<_, Point>(0..buffer.len(), false)
1329 .collect::<Vec<_>>(),
1330 &[
1331 DiagnosticEntry {
1332 range: Point::new(2, 21)..Point::new(2, 22),
1333 diagnostic: Diagnostic {
1334 severity: DiagnosticSeverity::WARNING,
1335 message: "undefined variable 'A'".to_string(),
1336 is_disk_based: true,
1337 group_id: 6,
1338 is_primary: true,
1339 ..Default::default()
1340 }
1341 },
1342 DiagnosticEntry {
1343 range: Point::new(3, 9)..Point::new(3, 14),
1344 diagnostic: Diagnostic {
1345 severity: DiagnosticSeverity::ERROR,
1346 message: "undefined variable 'BB'".to_string(),
1347 is_disk_based: true,
1348 group_id: 5,
1349 is_primary: true,
1350 ..Default::default()
1351 },
1352 }
1353 ]
1354 );
1355 });
1356}
1357
1358#[gpui::test]
1359async fn test_empty_diagnostic_ranges(cx: &mut gpui::TestAppContext) {
1360 cx.foreground().forbid_parking();
1361
1362 let text = concat!(
1363 "let one = ;\n", //
1364 "let two = \n",
1365 "let three = 3;\n",
1366 );
1367
1368 let fs = FakeFs::new(cx.background());
1369 fs.insert_tree("/dir", json!({ "a.rs": text })).await;
1370
1371 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
1372 let buffer = project
1373 .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
1374 .await
1375 .unwrap();
1376
1377 project.update(cx, |project, cx| {
1378 project
1379 .update_buffer_diagnostics(
1380 &buffer,
1381 vec![
1382 DiagnosticEntry {
1383 range: Unclipped(PointUtf16::new(0, 10))..Unclipped(PointUtf16::new(0, 10)),
1384 diagnostic: Diagnostic {
1385 severity: DiagnosticSeverity::ERROR,
1386 message: "syntax error 1".to_string(),
1387 ..Default::default()
1388 },
1389 },
1390 DiagnosticEntry {
1391 range: Unclipped(PointUtf16::new(1, 10))..Unclipped(PointUtf16::new(1, 10)),
1392 diagnostic: Diagnostic {
1393 severity: DiagnosticSeverity::ERROR,
1394 message: "syntax error 2".to_string(),
1395 ..Default::default()
1396 },
1397 },
1398 ],
1399 None,
1400 cx,
1401 )
1402 .unwrap();
1403 });
1404
1405 // An empty range is extended forward to include the following character.
1406 // At the end of a line, an empty range is extended backward to include
1407 // the preceding character.
1408 buffer.read_with(cx, |buffer, _| {
1409 let chunks = chunks_with_diagnostics(buffer, 0..buffer.len());
1410 assert_eq!(
1411 chunks
1412 .iter()
1413 .map(|(s, d)| (s.as_str(), *d))
1414 .collect::<Vec<_>>(),
1415 &[
1416 ("let one = ", None),
1417 (";", Some(DiagnosticSeverity::ERROR)),
1418 ("\nlet two =", None),
1419 (" ", Some(DiagnosticSeverity::ERROR)),
1420 ("\nlet three = 3;\n", None)
1421 ]
1422 );
1423 });
1424}
1425
1426#[gpui::test]
1427async fn test_edits_from_lsp_with_past_version(cx: &mut gpui::TestAppContext) {
1428 cx.foreground().forbid_parking();
1429
1430 let mut language = Language::new(
1431 LanguageConfig {
1432 name: "Rust".into(),
1433 path_suffixes: vec!["rs".to_string()],
1434 ..Default::default()
1435 },
1436 Some(tree_sitter_rust::language()),
1437 );
1438 let mut fake_servers = language.set_fake_lsp_adapter(Default::default()).await;
1439
1440 let text = "
1441 fn a() {
1442 f1();
1443 }
1444 fn b() {
1445 f2();
1446 }
1447 fn c() {
1448 f3();
1449 }
1450 "
1451 .unindent();
1452
1453 let fs = FakeFs::new(cx.background());
1454 fs.insert_tree(
1455 "/dir",
1456 json!({
1457 "a.rs": text.clone(),
1458 }),
1459 )
1460 .await;
1461
1462 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
1463 project.update(cx, |project, _| project.languages.add(Arc::new(language)));
1464 let buffer = project
1465 .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
1466 .await
1467 .unwrap();
1468
1469 let mut fake_server = fake_servers.next().await.unwrap();
1470 let lsp_document_version = fake_server
1471 .receive_notification::<lsp::notification::DidOpenTextDocument>()
1472 .await
1473 .text_document
1474 .version;
1475
1476 // Simulate editing the buffer after the language server computes some edits.
1477 buffer.update(cx, |buffer, cx| {
1478 buffer.edit(
1479 [(
1480 Point::new(0, 0)..Point::new(0, 0),
1481 "// above first function\n",
1482 )],
1483 None,
1484 cx,
1485 );
1486 buffer.edit(
1487 [(
1488 Point::new(2, 0)..Point::new(2, 0),
1489 " // inside first function\n",
1490 )],
1491 None,
1492 cx,
1493 );
1494 buffer.edit(
1495 [(
1496 Point::new(6, 4)..Point::new(6, 4),
1497 "// inside second function ",
1498 )],
1499 None,
1500 cx,
1501 );
1502
1503 assert_eq!(
1504 buffer.text(),
1505 "
1506 // above first function
1507 fn a() {
1508 // inside first function
1509 f1();
1510 }
1511 fn b() {
1512 // inside second function f2();
1513 }
1514 fn c() {
1515 f3();
1516 }
1517 "
1518 .unindent()
1519 );
1520 });
1521
1522 let edits = project
1523 .update(cx, |project, cx| {
1524 project.edits_from_lsp(
1525 &buffer,
1526 vec![
1527 // replace body of first function
1528 lsp::TextEdit {
1529 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(3, 0)),
1530 new_text: "
1531 fn a() {
1532 f10();
1533 }
1534 "
1535 .unindent(),
1536 },
1537 // edit inside second function
1538 lsp::TextEdit {
1539 range: lsp::Range::new(lsp::Position::new(4, 6), lsp::Position::new(4, 6)),
1540 new_text: "00".into(),
1541 },
1542 // edit inside third function via two distinct edits
1543 lsp::TextEdit {
1544 range: lsp::Range::new(lsp::Position::new(7, 5), lsp::Position::new(7, 5)),
1545 new_text: "4000".into(),
1546 },
1547 lsp::TextEdit {
1548 range: lsp::Range::new(lsp::Position::new(7, 5), lsp::Position::new(7, 6)),
1549 new_text: "".into(),
1550 },
1551 ],
1552 Some(lsp_document_version),
1553 cx,
1554 )
1555 })
1556 .await
1557 .unwrap();
1558
1559 buffer.update(cx, |buffer, cx| {
1560 for (range, new_text) in edits {
1561 buffer.edit([(range, new_text)], None, cx);
1562 }
1563 assert_eq!(
1564 buffer.text(),
1565 "
1566 // above first function
1567 fn a() {
1568 // inside first function
1569 f10();
1570 }
1571 fn b() {
1572 // inside second function f200();
1573 }
1574 fn c() {
1575 f4000();
1576 }
1577 "
1578 .unindent()
1579 );
1580 });
1581}
1582
1583#[gpui::test]
1584async fn test_edits_from_lsp_with_edits_on_adjacent_lines(cx: &mut gpui::TestAppContext) {
1585 cx.foreground().forbid_parking();
1586
1587 let text = "
1588 use a::b;
1589 use a::c;
1590
1591 fn f() {
1592 b();
1593 c();
1594 }
1595 "
1596 .unindent();
1597
1598 let fs = FakeFs::new(cx.background());
1599 fs.insert_tree(
1600 "/dir",
1601 json!({
1602 "a.rs": text.clone(),
1603 }),
1604 )
1605 .await;
1606
1607 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
1608 let buffer = project
1609 .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
1610 .await
1611 .unwrap();
1612
1613 // Simulate the language server sending us a small edit in the form of a very large diff.
1614 // Rust-analyzer does this when performing a merge-imports code action.
1615 let edits = project
1616 .update(cx, |project, cx| {
1617 project.edits_from_lsp(
1618 &buffer,
1619 [
1620 // Replace the first use statement without editing the semicolon.
1621 lsp::TextEdit {
1622 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 8)),
1623 new_text: "a::{b, c}".into(),
1624 },
1625 // Reinsert the remainder of the file between the semicolon and the final
1626 // newline of the file.
1627 lsp::TextEdit {
1628 range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 9)),
1629 new_text: "\n\n".into(),
1630 },
1631 lsp::TextEdit {
1632 range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 9)),
1633 new_text: "
1634 fn f() {
1635 b();
1636 c();
1637 }"
1638 .unindent(),
1639 },
1640 // Delete everything after the first newline of the file.
1641 lsp::TextEdit {
1642 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(7, 0)),
1643 new_text: "".into(),
1644 },
1645 ],
1646 None,
1647 cx,
1648 )
1649 })
1650 .await
1651 .unwrap();
1652
1653 buffer.update(cx, |buffer, cx| {
1654 let edits = edits
1655 .into_iter()
1656 .map(|(range, text)| {
1657 (
1658 range.start.to_point(buffer)..range.end.to_point(buffer),
1659 text,
1660 )
1661 })
1662 .collect::<Vec<_>>();
1663
1664 assert_eq!(
1665 edits,
1666 [
1667 (Point::new(0, 4)..Point::new(0, 8), "a::{b, c}".into()),
1668 (Point::new(1, 0)..Point::new(2, 0), "".into())
1669 ]
1670 );
1671
1672 for (range, new_text) in edits {
1673 buffer.edit([(range, new_text)], None, cx);
1674 }
1675 assert_eq!(
1676 buffer.text(),
1677 "
1678 use a::{b, c};
1679
1680 fn f() {
1681 b();
1682 c();
1683 }
1684 "
1685 .unindent()
1686 );
1687 });
1688}
1689
1690#[gpui::test]
1691async fn test_invalid_edits_from_lsp(cx: &mut gpui::TestAppContext) {
1692 cx.foreground().forbid_parking();
1693
1694 let text = "
1695 use a::b;
1696 use a::c;
1697
1698 fn f() {
1699 b();
1700 c();
1701 }
1702 "
1703 .unindent();
1704
1705 let fs = FakeFs::new(cx.background());
1706 fs.insert_tree(
1707 "/dir",
1708 json!({
1709 "a.rs": text.clone(),
1710 }),
1711 )
1712 .await;
1713
1714 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
1715 let buffer = project
1716 .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
1717 .await
1718 .unwrap();
1719
1720 // Simulate the language server sending us edits in a non-ordered fashion,
1721 // with ranges sometimes being inverted or pointing to invalid locations.
1722 let edits = project
1723 .update(cx, |project, cx| {
1724 project.edits_from_lsp(
1725 &buffer,
1726 [
1727 lsp::TextEdit {
1728 range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 9)),
1729 new_text: "\n\n".into(),
1730 },
1731 lsp::TextEdit {
1732 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 4)),
1733 new_text: "a::{b, c}".into(),
1734 },
1735 lsp::TextEdit {
1736 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(99, 0)),
1737 new_text: "".into(),
1738 },
1739 lsp::TextEdit {
1740 range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 9)),
1741 new_text: "
1742 fn f() {
1743 b();
1744 c();
1745 }"
1746 .unindent(),
1747 },
1748 ],
1749 None,
1750 cx,
1751 )
1752 })
1753 .await
1754 .unwrap();
1755
1756 buffer.update(cx, |buffer, cx| {
1757 let edits = edits
1758 .into_iter()
1759 .map(|(range, text)| {
1760 (
1761 range.start.to_point(buffer)..range.end.to_point(buffer),
1762 text,
1763 )
1764 })
1765 .collect::<Vec<_>>();
1766
1767 assert_eq!(
1768 edits,
1769 [
1770 (Point::new(0, 4)..Point::new(0, 8), "a::{b, c}".into()),
1771 (Point::new(1, 0)..Point::new(2, 0), "".into())
1772 ]
1773 );
1774
1775 for (range, new_text) in edits {
1776 buffer.edit([(range, new_text)], None, cx);
1777 }
1778 assert_eq!(
1779 buffer.text(),
1780 "
1781 use a::{b, c};
1782
1783 fn f() {
1784 b();
1785 c();
1786 }
1787 "
1788 .unindent()
1789 );
1790 });
1791}
1792
1793fn chunks_with_diagnostics<T: ToOffset + ToPoint>(
1794 buffer: &Buffer,
1795 range: Range<T>,
1796) -> Vec<(String, Option<DiagnosticSeverity>)> {
1797 let mut chunks: Vec<(String, Option<DiagnosticSeverity>)> = Vec::new();
1798 for chunk in buffer.snapshot().chunks(range, true) {
1799 if chunks.last().map_or(false, |prev_chunk| {
1800 prev_chunk.1 == chunk.diagnostic_severity
1801 }) {
1802 chunks.last_mut().unwrap().0.push_str(chunk.text);
1803 } else {
1804 chunks.push((chunk.text.to_string(), chunk.diagnostic_severity));
1805 }
1806 }
1807 chunks
1808}
1809
1810#[gpui::test(iterations = 10)]
1811async fn test_definition(cx: &mut gpui::TestAppContext) {
1812 let mut language = Language::new(
1813 LanguageConfig {
1814 name: "Rust".into(),
1815 path_suffixes: vec!["rs".to_string()],
1816 ..Default::default()
1817 },
1818 Some(tree_sitter_rust::language()),
1819 );
1820 let mut fake_servers = language.set_fake_lsp_adapter(Default::default()).await;
1821
1822 let fs = FakeFs::new(cx.background());
1823 fs.insert_tree(
1824 "/dir",
1825 json!({
1826 "a.rs": "const fn a() { A }",
1827 "b.rs": "const y: i32 = crate::a()",
1828 }),
1829 )
1830 .await;
1831
1832 let project = Project::test(fs, ["/dir/b.rs".as_ref()], cx).await;
1833 project.update(cx, |project, _| project.languages.add(Arc::new(language)));
1834
1835 let buffer = project
1836 .update(cx, |project, cx| project.open_local_buffer("/dir/b.rs", cx))
1837 .await
1838 .unwrap();
1839
1840 let fake_server = fake_servers.next().await.unwrap();
1841 fake_server.handle_request::<lsp::request::GotoDefinition, _, _>(|params, _| async move {
1842 let params = params.text_document_position_params;
1843 assert_eq!(
1844 params.text_document.uri.to_file_path().unwrap(),
1845 Path::new("/dir/b.rs"),
1846 );
1847 assert_eq!(params.position, lsp::Position::new(0, 22));
1848
1849 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
1850 lsp::Location::new(
1851 lsp::Url::from_file_path("/dir/a.rs").unwrap(),
1852 lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
1853 ),
1854 )))
1855 });
1856
1857 let mut definitions = project
1858 .update(cx, |project, cx| project.definition(&buffer, 22, cx))
1859 .await
1860 .unwrap();
1861
1862 // Assert no new language server started
1863 cx.foreground().run_until_parked();
1864 assert!(fake_servers.try_next().is_err());
1865
1866 assert_eq!(definitions.len(), 1);
1867 let definition = definitions.pop().unwrap();
1868 cx.update(|cx| {
1869 let target_buffer = definition.target.buffer.read(cx);
1870 assert_eq!(
1871 target_buffer
1872 .file()
1873 .unwrap()
1874 .as_local()
1875 .unwrap()
1876 .abs_path(cx),
1877 Path::new("/dir/a.rs"),
1878 );
1879 assert_eq!(definition.target.range.to_offset(target_buffer), 9..10);
1880 assert_eq!(
1881 list_worktrees(&project, cx),
1882 [("/dir/b.rs".as_ref(), true), ("/dir/a.rs".as_ref(), false)]
1883 );
1884
1885 drop(definition);
1886 });
1887 cx.read(|cx| {
1888 assert_eq!(list_worktrees(&project, cx), [("/dir/b.rs".as_ref(), true)]);
1889 });
1890
1891 fn list_worktrees<'a>(
1892 project: &'a ModelHandle<Project>,
1893 cx: &'a AppContext,
1894 ) -> Vec<(&'a Path, bool)> {
1895 project
1896 .read(cx)
1897 .worktrees(cx)
1898 .map(|worktree| {
1899 let worktree = worktree.read(cx);
1900 (
1901 worktree.as_local().unwrap().abs_path().as_ref(),
1902 worktree.is_visible(),
1903 )
1904 })
1905 .collect::<Vec<_>>()
1906 }
1907}
1908
1909#[gpui::test]
1910async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) {
1911 let mut language = Language::new(
1912 LanguageConfig {
1913 name: "TypeScript".into(),
1914 path_suffixes: vec!["ts".to_string()],
1915 ..Default::default()
1916 },
1917 Some(tree_sitter_typescript::language_typescript()),
1918 );
1919 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
1920
1921 let fs = FakeFs::new(cx.background());
1922 fs.insert_tree(
1923 "/dir",
1924 json!({
1925 "a.ts": "",
1926 }),
1927 )
1928 .await;
1929
1930 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
1931 project.update(cx, |project, _| project.languages.add(Arc::new(language)));
1932 let buffer = project
1933 .update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
1934 .await
1935 .unwrap();
1936
1937 let fake_server = fake_language_servers.next().await.unwrap();
1938
1939 let text = "let a = b.fqn";
1940 buffer.update(cx, |buffer, cx| buffer.set_text(text, cx));
1941 let completions = project.update(cx, |project, cx| {
1942 project.completions(&buffer, text.len(), cx)
1943 });
1944
1945 fake_server
1946 .handle_request::<lsp::request::Completion, _, _>(|_, _| async move {
1947 Ok(Some(lsp::CompletionResponse::Array(vec![
1948 lsp::CompletionItem {
1949 label: "fullyQualifiedName?".into(),
1950 insert_text: Some("fullyQualifiedName".into()),
1951 ..Default::default()
1952 },
1953 ])))
1954 })
1955 .next()
1956 .await;
1957 let completions = completions.await.unwrap();
1958 let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
1959 assert_eq!(completions.len(), 1);
1960 assert_eq!(completions[0].new_text, "fullyQualifiedName");
1961 assert_eq!(
1962 completions[0].old_range.to_offset(&snapshot),
1963 text.len() - 3..text.len()
1964 );
1965
1966 let text = "let a = \"atoms/cmp\"";
1967 buffer.update(cx, |buffer, cx| buffer.set_text(text, cx));
1968 let completions = project.update(cx, |project, cx| {
1969 project.completions(&buffer, text.len() - 1, cx)
1970 });
1971
1972 fake_server
1973 .handle_request::<lsp::request::Completion, _, _>(|_, _| async move {
1974 Ok(Some(lsp::CompletionResponse::Array(vec![
1975 lsp::CompletionItem {
1976 label: "component".into(),
1977 ..Default::default()
1978 },
1979 ])))
1980 })
1981 .next()
1982 .await;
1983 let completions = completions.await.unwrap();
1984 let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
1985 assert_eq!(completions.len(), 1);
1986 assert_eq!(completions[0].new_text, "component");
1987 assert_eq!(
1988 completions[0].old_range.to_offset(&snapshot),
1989 text.len() - 4..text.len() - 1
1990 );
1991}
1992
1993#[gpui::test]
1994async fn test_completions_with_carriage_returns(cx: &mut gpui::TestAppContext) {
1995 let mut language = Language::new(
1996 LanguageConfig {
1997 name: "TypeScript".into(),
1998 path_suffixes: vec!["ts".to_string()],
1999 ..Default::default()
2000 },
2001 Some(tree_sitter_typescript::language_typescript()),
2002 );
2003 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
2004
2005 let fs = FakeFs::new(cx.background());
2006 fs.insert_tree(
2007 "/dir",
2008 json!({
2009 "a.ts": "",
2010 }),
2011 )
2012 .await;
2013
2014 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
2015 project.update(cx, |project, _| project.languages.add(Arc::new(language)));
2016 let buffer = project
2017 .update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
2018 .await
2019 .unwrap();
2020
2021 let fake_server = fake_language_servers.next().await.unwrap();
2022
2023 let text = "let a = b.fqn";
2024 buffer.update(cx, |buffer, cx| buffer.set_text(text, cx));
2025 let completions = project.update(cx, |project, cx| {
2026 project.completions(&buffer, text.len(), cx)
2027 });
2028
2029 fake_server
2030 .handle_request::<lsp::request::Completion, _, _>(|_, _| async move {
2031 Ok(Some(lsp::CompletionResponse::Array(vec![
2032 lsp::CompletionItem {
2033 label: "fullyQualifiedName?".into(),
2034 insert_text: Some("fully\rQualified\r\nName".into()),
2035 ..Default::default()
2036 },
2037 ])))
2038 })
2039 .next()
2040 .await;
2041 let completions = completions.await.unwrap();
2042 assert_eq!(completions.len(), 1);
2043 assert_eq!(completions[0].new_text, "fully\nQualified\nName");
2044}
2045
2046#[gpui::test(iterations = 10)]
2047async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) {
2048 let mut language = Language::new(
2049 LanguageConfig {
2050 name: "TypeScript".into(),
2051 path_suffixes: vec!["ts".to_string()],
2052 ..Default::default()
2053 },
2054 None,
2055 );
2056 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
2057
2058 let fs = FakeFs::new(cx.background());
2059 fs.insert_tree(
2060 "/dir",
2061 json!({
2062 "a.ts": "a",
2063 }),
2064 )
2065 .await;
2066
2067 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
2068 project.update(cx, |project, _| project.languages.add(Arc::new(language)));
2069 let buffer = project
2070 .update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
2071 .await
2072 .unwrap();
2073
2074 let fake_server = fake_language_servers.next().await.unwrap();
2075
2076 // Language server returns code actions that contain commands, and not edits.
2077 let actions = project.update(cx, |project, cx| project.code_actions(&buffer, 0..0, cx));
2078 fake_server
2079 .handle_request::<lsp::request::CodeActionRequest, _, _>(|_, _| async move {
2080 Ok(Some(vec![
2081 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
2082 title: "The code action".into(),
2083 command: Some(lsp::Command {
2084 title: "The command".into(),
2085 command: "_the/command".into(),
2086 arguments: Some(vec![json!("the-argument")]),
2087 }),
2088 ..Default::default()
2089 }),
2090 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
2091 title: "two".into(),
2092 ..Default::default()
2093 }),
2094 ]))
2095 })
2096 .next()
2097 .await;
2098
2099 let action = actions.await.unwrap()[0].clone();
2100 let apply = project.update(cx, |project, cx| {
2101 project.apply_code_action(buffer.clone(), action, true, cx)
2102 });
2103
2104 // Resolving the code action does not populate its edits. In absence of
2105 // edits, we must execute the given command.
2106 fake_server.handle_request::<lsp::request::CodeActionResolveRequest, _, _>(
2107 |action, _| async move { Ok(action) },
2108 );
2109
2110 // While executing the command, the language server sends the editor
2111 // a `workspaceEdit` request.
2112 fake_server
2113 .handle_request::<lsp::request::ExecuteCommand, _, _>({
2114 let fake = fake_server.clone();
2115 move |params, _| {
2116 assert_eq!(params.command, "_the/command");
2117 let fake = fake.clone();
2118 async move {
2119 fake.server
2120 .request::<lsp::request::ApplyWorkspaceEdit>(
2121 lsp::ApplyWorkspaceEditParams {
2122 label: None,
2123 edit: lsp::WorkspaceEdit {
2124 changes: Some(
2125 [(
2126 lsp::Url::from_file_path("/dir/a.ts").unwrap(),
2127 vec![lsp::TextEdit {
2128 range: lsp::Range::new(
2129 lsp::Position::new(0, 0),
2130 lsp::Position::new(0, 0),
2131 ),
2132 new_text: "X".into(),
2133 }],
2134 )]
2135 .into_iter()
2136 .collect(),
2137 ),
2138 ..Default::default()
2139 },
2140 },
2141 )
2142 .await
2143 .unwrap();
2144 Ok(Some(json!(null)))
2145 }
2146 }
2147 })
2148 .next()
2149 .await;
2150
2151 // Applying the code action returns a project transaction containing the edits
2152 // sent by the language server in its `workspaceEdit` request.
2153 let transaction = apply.await.unwrap();
2154 assert!(transaction.0.contains_key(&buffer));
2155 buffer.update(cx, |buffer, cx| {
2156 assert_eq!(buffer.text(), "Xa");
2157 buffer.undo(cx);
2158 assert_eq!(buffer.text(), "a");
2159 });
2160}
2161
2162#[gpui::test]
2163async fn test_save_file(cx: &mut gpui::TestAppContext) {
2164 let fs = FakeFs::new(cx.background());
2165 fs.insert_tree(
2166 "/dir",
2167 json!({
2168 "file1": "the old contents",
2169 }),
2170 )
2171 .await;
2172
2173 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
2174 let buffer = project
2175 .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
2176 .await
2177 .unwrap();
2178 buffer.update(cx, |buffer, cx| {
2179 assert_eq!(buffer.text(), "the old contents");
2180 buffer.edit([(0..0, "a line of text.\n".repeat(10 * 1024))], None, cx);
2181 });
2182
2183 project
2184 .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
2185 .await
2186 .unwrap();
2187
2188 let new_text = fs.load(Path::new("/dir/file1")).await.unwrap();
2189 assert_eq!(new_text, buffer.read_with(cx, |buffer, _| buffer.text()));
2190}
2191
2192#[gpui::test]
2193async fn test_save_in_single_file_worktree(cx: &mut gpui::TestAppContext) {
2194 let fs = FakeFs::new(cx.background());
2195 fs.insert_tree(
2196 "/dir",
2197 json!({
2198 "file1": "the old contents",
2199 }),
2200 )
2201 .await;
2202
2203 let project = Project::test(fs.clone(), ["/dir/file1".as_ref()], cx).await;
2204 let buffer = project
2205 .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
2206 .await
2207 .unwrap();
2208 buffer.update(cx, |buffer, cx| {
2209 buffer.edit([(0..0, "a line of text.\n".repeat(10 * 1024))], None, cx);
2210 });
2211
2212 project
2213 .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
2214 .await
2215 .unwrap();
2216
2217 let new_text = fs.load(Path::new("/dir/file1")).await.unwrap();
2218 assert_eq!(new_text, buffer.read_with(cx, |buffer, _| buffer.text()));
2219}
2220
2221#[gpui::test]
2222async fn test_save_as(cx: &mut gpui::TestAppContext) {
2223 let fs = FakeFs::new(cx.background());
2224 fs.insert_tree("/dir", json!({})).await;
2225
2226 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
2227
2228 let languages = project.read_with(cx, |project, _| project.languages().clone());
2229 languages.register(
2230 "/some/path",
2231 LanguageConfig {
2232 name: "Rust".into(),
2233 path_suffixes: vec!["rs".into()],
2234 ..Default::default()
2235 },
2236 tree_sitter_rust::language(),
2237 None,
2238 |_| Default::default(),
2239 );
2240
2241 let buffer = project.update(cx, |project, cx| {
2242 project.create_buffer("", None, cx).unwrap()
2243 });
2244 buffer.update(cx, |buffer, cx| {
2245 buffer.edit([(0..0, "abc")], None, cx);
2246 assert!(buffer.is_dirty());
2247 assert!(!buffer.has_conflict());
2248 assert_eq!(buffer.language().unwrap().name().as_ref(), "Plain Text");
2249 });
2250 project
2251 .update(cx, |project, cx| {
2252 project.save_buffer_as(buffer.clone(), "/dir/file1.rs".into(), cx)
2253 })
2254 .await
2255 .unwrap();
2256 assert_eq!(fs.load(Path::new("/dir/file1.rs")).await.unwrap(), "abc");
2257
2258 cx.foreground().run_until_parked();
2259 buffer.read_with(cx, |buffer, cx| {
2260 assert_eq!(
2261 buffer.file().unwrap().full_path(cx),
2262 Path::new("dir/file1.rs")
2263 );
2264 assert!(!buffer.is_dirty());
2265 assert!(!buffer.has_conflict());
2266 assert_eq!(buffer.language().unwrap().name().as_ref(), "Rust");
2267 });
2268
2269 let opened_buffer = project
2270 .update(cx, |project, cx| {
2271 project.open_local_buffer("/dir/file1.rs", cx)
2272 })
2273 .await
2274 .unwrap();
2275 assert_eq!(opened_buffer, buffer);
2276}
2277
2278#[gpui::test(retries = 5)]
2279async fn test_rescan_and_remote_updates(
2280 deterministic: Arc<Deterministic>,
2281 cx: &mut gpui::TestAppContext,
2282) {
2283 let dir = temp_tree(json!({
2284 "a": {
2285 "file1": "",
2286 "file2": "",
2287 "file3": "",
2288 },
2289 "b": {
2290 "c": {
2291 "file4": "",
2292 "file5": "",
2293 }
2294 }
2295 }));
2296
2297 let project = Project::test(Arc::new(RealFs), [dir.path()], cx).await;
2298 let rpc = project.read_with(cx, |p, _| p.client.clone());
2299
2300 let buffer_for_path = |path: &'static str, cx: &mut gpui::TestAppContext| {
2301 let buffer = project.update(cx, |p, cx| p.open_local_buffer(dir.path().join(path), cx));
2302 async move { buffer.await.unwrap() }
2303 };
2304 let id_for_path = |path: &'static str, cx: &gpui::TestAppContext| {
2305 project.read_with(cx, |project, cx| {
2306 let tree = project.worktrees(cx).next().unwrap();
2307 tree.read(cx)
2308 .entry_for_path(path)
2309 .unwrap_or_else(|| panic!("no entry for path {}", path))
2310 .id
2311 })
2312 };
2313
2314 let buffer2 = buffer_for_path("a/file2", cx).await;
2315 let buffer3 = buffer_for_path("a/file3", cx).await;
2316 let buffer4 = buffer_for_path("b/c/file4", cx).await;
2317 let buffer5 = buffer_for_path("b/c/file5", cx).await;
2318
2319 let file2_id = id_for_path("a/file2", cx);
2320 let file3_id = id_for_path("a/file3", cx);
2321 let file4_id = id_for_path("b/c/file4", cx);
2322
2323 // Create a remote copy of this worktree.
2324 let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
2325 let initial_snapshot = tree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
2326 let remote = cx.update(|cx| {
2327 Worktree::remote(
2328 1,
2329 1,
2330 proto::WorktreeMetadata {
2331 id: initial_snapshot.id().to_proto(),
2332 root_name: initial_snapshot.root_name().into(),
2333 abs_path: initial_snapshot
2334 .abs_path()
2335 .as_os_str()
2336 .to_string_lossy()
2337 .into(),
2338 visible: true,
2339 },
2340 rpc.clone(),
2341 cx,
2342 )
2343 });
2344 remote.update(cx, |remote, _| {
2345 let update = initial_snapshot.build_initial_update(1);
2346 remote.as_remote_mut().unwrap().update_from_remote(update);
2347 });
2348 deterministic.run_until_parked();
2349
2350 cx.read(|cx| {
2351 assert!(!buffer2.read(cx).is_dirty());
2352 assert!(!buffer3.read(cx).is_dirty());
2353 assert!(!buffer4.read(cx).is_dirty());
2354 assert!(!buffer5.read(cx).is_dirty());
2355 });
2356
2357 // Rename and delete files and directories.
2358 tree.flush_fs_events(cx).await;
2359 std::fs::rename(dir.path().join("a/file3"), dir.path().join("b/c/file3")).unwrap();
2360 std::fs::remove_file(dir.path().join("b/c/file5")).unwrap();
2361 std::fs::rename(dir.path().join("b/c"), dir.path().join("d")).unwrap();
2362 std::fs::rename(dir.path().join("a/file2"), dir.path().join("a/file2.new")).unwrap();
2363 tree.flush_fs_events(cx).await;
2364
2365 let expected_paths = vec![
2366 "a",
2367 "a/file1",
2368 "a/file2.new",
2369 "b",
2370 "d",
2371 "d/file3",
2372 "d/file4",
2373 ];
2374
2375 cx.read(|app| {
2376 assert_eq!(
2377 tree.read(app)
2378 .paths()
2379 .map(|p| p.to_str().unwrap())
2380 .collect::<Vec<_>>(),
2381 expected_paths
2382 );
2383
2384 assert_eq!(id_for_path("a/file2.new", cx), file2_id);
2385 assert_eq!(id_for_path("d/file3", cx), file3_id);
2386 assert_eq!(id_for_path("d/file4", cx), file4_id);
2387
2388 assert_eq!(
2389 buffer2.read(app).file().unwrap().path().as_ref(),
2390 Path::new("a/file2.new")
2391 );
2392 assert_eq!(
2393 buffer3.read(app).file().unwrap().path().as_ref(),
2394 Path::new("d/file3")
2395 );
2396 assert_eq!(
2397 buffer4.read(app).file().unwrap().path().as_ref(),
2398 Path::new("d/file4")
2399 );
2400 assert_eq!(
2401 buffer5.read(app).file().unwrap().path().as_ref(),
2402 Path::new("b/c/file5")
2403 );
2404
2405 assert!(!buffer2.read(app).file().unwrap().is_deleted());
2406 assert!(!buffer3.read(app).file().unwrap().is_deleted());
2407 assert!(!buffer4.read(app).file().unwrap().is_deleted());
2408 assert!(buffer5.read(app).file().unwrap().is_deleted());
2409 });
2410
2411 // Update the remote worktree. Check that it becomes consistent with the
2412 // local worktree.
2413 remote.update(cx, |remote, cx| {
2414 let update = tree.read(cx).as_local().unwrap().snapshot().build_update(
2415 &initial_snapshot,
2416 1,
2417 1,
2418 true,
2419 );
2420 remote.as_remote_mut().unwrap().update_from_remote(update);
2421 });
2422 deterministic.run_until_parked();
2423 remote.read_with(cx, |remote, _| {
2424 assert_eq!(
2425 remote
2426 .paths()
2427 .map(|p| p.to_str().unwrap())
2428 .collect::<Vec<_>>(),
2429 expected_paths
2430 );
2431 });
2432}
2433
2434#[gpui::test(iterations = 10)]
2435async fn test_buffer_identity_across_renames(
2436 deterministic: Arc<Deterministic>,
2437 cx: &mut gpui::TestAppContext,
2438) {
2439 let fs = FakeFs::new(cx.background());
2440 fs.insert_tree(
2441 "/dir",
2442 json!({
2443 "a": {
2444 "file1": "",
2445 }
2446 }),
2447 )
2448 .await;
2449
2450 let project = Project::test(fs, [Path::new("/dir")], cx).await;
2451 let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
2452 let tree_id = tree.read_with(cx, |tree, _| tree.id());
2453
2454 let id_for_path = |path: &'static str, cx: &gpui::TestAppContext| {
2455 project.read_with(cx, |project, cx| {
2456 let tree = project.worktrees(cx).next().unwrap();
2457 tree.read(cx)
2458 .entry_for_path(path)
2459 .unwrap_or_else(|| panic!("no entry for path {}", path))
2460 .id
2461 })
2462 };
2463
2464 let dir_id = id_for_path("a", cx);
2465 let file_id = id_for_path("a/file1", cx);
2466 let buffer = project
2467 .update(cx, |p, cx| p.open_buffer((tree_id, "a/file1"), cx))
2468 .await
2469 .unwrap();
2470 buffer.read_with(cx, |buffer, _| assert!(!buffer.is_dirty()));
2471
2472 project
2473 .update(cx, |project, cx| {
2474 project.rename_entry(dir_id, Path::new("b"), cx)
2475 })
2476 .unwrap()
2477 .await
2478 .unwrap();
2479 deterministic.run_until_parked();
2480 assert_eq!(id_for_path("b", cx), dir_id);
2481 assert_eq!(id_for_path("b/file1", cx), file_id);
2482 buffer.read_with(cx, |buffer, _| assert!(!buffer.is_dirty()));
2483}
2484
2485#[gpui::test]
2486async fn test_buffer_deduping(cx: &mut gpui::TestAppContext) {
2487 let fs = FakeFs::new(cx.background());
2488 fs.insert_tree(
2489 "/dir",
2490 json!({
2491 "a.txt": "a-contents",
2492 "b.txt": "b-contents",
2493 }),
2494 )
2495 .await;
2496
2497 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
2498
2499 // Spawn multiple tasks to open paths, repeating some paths.
2500 let (buffer_a_1, buffer_b, buffer_a_2) = project.update(cx, |p, cx| {
2501 (
2502 p.open_local_buffer("/dir/a.txt", cx),
2503 p.open_local_buffer("/dir/b.txt", cx),
2504 p.open_local_buffer("/dir/a.txt", cx),
2505 )
2506 });
2507
2508 let buffer_a_1 = buffer_a_1.await.unwrap();
2509 let buffer_a_2 = buffer_a_2.await.unwrap();
2510 let buffer_b = buffer_b.await.unwrap();
2511 assert_eq!(buffer_a_1.read_with(cx, |b, _| b.text()), "a-contents");
2512 assert_eq!(buffer_b.read_with(cx, |b, _| b.text()), "b-contents");
2513
2514 // There is only one buffer per path.
2515 let buffer_a_id = buffer_a_1.id();
2516 assert_eq!(buffer_a_2.id(), buffer_a_id);
2517
2518 // Open the same path again while it is still open.
2519 drop(buffer_a_1);
2520 let buffer_a_3 = project
2521 .update(cx, |p, cx| p.open_local_buffer("/dir/a.txt", cx))
2522 .await
2523 .unwrap();
2524
2525 // There's still only one buffer per path.
2526 assert_eq!(buffer_a_3.id(), buffer_a_id);
2527}
2528
2529#[gpui::test]
2530async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) {
2531 let fs = FakeFs::new(cx.background());
2532 fs.insert_tree(
2533 "/dir",
2534 json!({
2535 "file1": "abc",
2536 "file2": "def",
2537 "file3": "ghi",
2538 }),
2539 )
2540 .await;
2541
2542 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
2543
2544 let buffer1 = project
2545 .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
2546 .await
2547 .unwrap();
2548 let events = Rc::new(RefCell::new(Vec::new()));
2549
2550 // initially, the buffer isn't dirty.
2551 buffer1.update(cx, |buffer, cx| {
2552 cx.subscribe(&buffer1, {
2553 let events = events.clone();
2554 move |_, _, event, _| match event {
2555 BufferEvent::Operation(_) => {}
2556 _ => events.borrow_mut().push(event.clone()),
2557 }
2558 })
2559 .detach();
2560
2561 assert!(!buffer.is_dirty());
2562 assert!(events.borrow().is_empty());
2563
2564 buffer.edit([(1..2, "")], None, cx);
2565 });
2566
2567 // after the first edit, the buffer is dirty, and emits a dirtied event.
2568 buffer1.update(cx, |buffer, cx| {
2569 assert!(buffer.text() == "ac");
2570 assert!(buffer.is_dirty());
2571 assert_eq!(
2572 *events.borrow(),
2573 &[language::Event::Edited, language::Event::DirtyChanged]
2574 );
2575 events.borrow_mut().clear();
2576 buffer.did_save(
2577 buffer.version(),
2578 buffer.as_rope().fingerprint(),
2579 buffer.file().unwrap().mtime(),
2580 cx,
2581 );
2582 });
2583
2584 // after saving, the buffer is not dirty, and emits a saved event.
2585 buffer1.update(cx, |buffer, cx| {
2586 assert!(!buffer.is_dirty());
2587 assert_eq!(*events.borrow(), &[language::Event::Saved]);
2588 events.borrow_mut().clear();
2589
2590 buffer.edit([(1..1, "B")], None, cx);
2591 buffer.edit([(2..2, "D")], None, cx);
2592 });
2593
2594 // after editing again, the buffer is dirty, and emits another dirty event.
2595 buffer1.update(cx, |buffer, cx| {
2596 assert!(buffer.text() == "aBDc");
2597 assert!(buffer.is_dirty());
2598 assert_eq!(
2599 *events.borrow(),
2600 &[
2601 language::Event::Edited,
2602 language::Event::DirtyChanged,
2603 language::Event::Edited,
2604 ],
2605 );
2606 events.borrow_mut().clear();
2607
2608 // After restoring the buffer to its previously-saved state,
2609 // the buffer is not considered dirty anymore.
2610 buffer.edit([(1..3, "")], None, cx);
2611 assert!(buffer.text() == "ac");
2612 assert!(!buffer.is_dirty());
2613 });
2614
2615 assert_eq!(
2616 *events.borrow(),
2617 &[language::Event::Edited, language::Event::DirtyChanged]
2618 );
2619
2620 // When a file is deleted, the buffer is considered dirty.
2621 let events = Rc::new(RefCell::new(Vec::new()));
2622 let buffer2 = project
2623 .update(cx, |p, cx| p.open_local_buffer("/dir/file2", cx))
2624 .await
2625 .unwrap();
2626 buffer2.update(cx, |_, cx| {
2627 cx.subscribe(&buffer2, {
2628 let events = events.clone();
2629 move |_, _, event, _| events.borrow_mut().push(event.clone())
2630 })
2631 .detach();
2632 });
2633
2634 fs.remove_file("/dir/file2".as_ref(), Default::default())
2635 .await
2636 .unwrap();
2637 cx.foreground().run_until_parked();
2638 buffer2.read_with(cx, |buffer, _| assert!(buffer.is_dirty()));
2639 assert_eq!(
2640 *events.borrow(),
2641 &[
2642 language::Event::DirtyChanged,
2643 language::Event::FileHandleChanged
2644 ]
2645 );
2646
2647 // When a file is already dirty when deleted, we don't emit a Dirtied event.
2648 let events = Rc::new(RefCell::new(Vec::new()));
2649 let buffer3 = project
2650 .update(cx, |p, cx| p.open_local_buffer("/dir/file3", cx))
2651 .await
2652 .unwrap();
2653 buffer3.update(cx, |_, cx| {
2654 cx.subscribe(&buffer3, {
2655 let events = events.clone();
2656 move |_, _, event, _| events.borrow_mut().push(event.clone())
2657 })
2658 .detach();
2659 });
2660
2661 buffer3.update(cx, |buffer, cx| {
2662 buffer.edit([(0..0, "x")], None, cx);
2663 });
2664 events.borrow_mut().clear();
2665 fs.remove_file("/dir/file3".as_ref(), Default::default())
2666 .await
2667 .unwrap();
2668 cx.foreground().run_until_parked();
2669 assert_eq!(*events.borrow(), &[language::Event::FileHandleChanged]);
2670 cx.read(|cx| assert!(buffer3.read(cx).is_dirty()));
2671}
2672
2673#[gpui::test]
2674async fn test_buffer_file_changes_on_disk(cx: &mut gpui::TestAppContext) {
2675 let initial_contents = "aaa\nbbbbb\nc\n";
2676 let fs = FakeFs::new(cx.background());
2677 fs.insert_tree(
2678 "/dir",
2679 json!({
2680 "the-file": initial_contents,
2681 }),
2682 )
2683 .await;
2684 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
2685 let buffer = project
2686 .update(cx, |p, cx| p.open_local_buffer("/dir/the-file", cx))
2687 .await
2688 .unwrap();
2689
2690 let anchors = (0..3)
2691 .map(|row| buffer.read_with(cx, |b, _| b.anchor_before(Point::new(row, 1))))
2692 .collect::<Vec<_>>();
2693
2694 // Change the file on disk, adding two new lines of text, and removing
2695 // one line.
2696 buffer.read_with(cx, |buffer, _| {
2697 assert!(!buffer.is_dirty());
2698 assert!(!buffer.has_conflict());
2699 });
2700 let new_contents = "AAAA\naaa\nBB\nbbbbb\n";
2701 fs.save(
2702 "/dir/the-file".as_ref(),
2703 &new_contents.into(),
2704 LineEnding::Unix,
2705 )
2706 .await
2707 .unwrap();
2708
2709 // Because the buffer was not modified, it is reloaded from disk. Its
2710 // contents are edited according to the diff between the old and new
2711 // file contents.
2712 cx.foreground().run_until_parked();
2713 buffer.update(cx, |buffer, _| {
2714 assert_eq!(buffer.text(), new_contents);
2715 assert!(!buffer.is_dirty());
2716 assert!(!buffer.has_conflict());
2717
2718 let anchor_positions = anchors
2719 .iter()
2720 .map(|anchor| anchor.to_point(&*buffer))
2721 .collect::<Vec<_>>();
2722 assert_eq!(
2723 anchor_positions,
2724 [Point::new(1, 1), Point::new(3, 1), Point::new(3, 5)]
2725 );
2726 });
2727
2728 // Modify the buffer
2729 buffer.update(cx, |buffer, cx| {
2730 buffer.edit([(0..0, " ")], None, cx);
2731 assert!(buffer.is_dirty());
2732 assert!(!buffer.has_conflict());
2733 });
2734
2735 // Change the file on disk again, adding blank lines to the beginning.
2736 fs.save(
2737 "/dir/the-file".as_ref(),
2738 &"\n\n\nAAAA\naaa\nBB\nbbbbb\n".into(),
2739 LineEnding::Unix,
2740 )
2741 .await
2742 .unwrap();
2743
2744 // Because the buffer is modified, it doesn't reload from disk, but is
2745 // marked as having a conflict.
2746 cx.foreground().run_until_parked();
2747 buffer.read_with(cx, |buffer, _| {
2748 assert!(buffer.has_conflict());
2749 });
2750}
2751
2752#[gpui::test]
2753async fn test_buffer_line_endings(cx: &mut gpui::TestAppContext) {
2754 let fs = FakeFs::new(cx.background());
2755 fs.insert_tree(
2756 "/dir",
2757 json!({
2758 "file1": "a\nb\nc\n",
2759 "file2": "one\r\ntwo\r\nthree\r\n",
2760 }),
2761 )
2762 .await;
2763
2764 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
2765 let buffer1 = project
2766 .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
2767 .await
2768 .unwrap();
2769 let buffer2 = project
2770 .update(cx, |p, cx| p.open_local_buffer("/dir/file2", cx))
2771 .await
2772 .unwrap();
2773
2774 buffer1.read_with(cx, |buffer, _| {
2775 assert_eq!(buffer.text(), "a\nb\nc\n");
2776 assert_eq!(buffer.line_ending(), LineEnding::Unix);
2777 });
2778 buffer2.read_with(cx, |buffer, _| {
2779 assert_eq!(buffer.text(), "one\ntwo\nthree\n");
2780 assert_eq!(buffer.line_ending(), LineEnding::Windows);
2781 });
2782
2783 // Change a file's line endings on disk from unix to windows. The buffer's
2784 // state updates correctly.
2785 fs.save(
2786 "/dir/file1".as_ref(),
2787 &"aaa\nb\nc\n".into(),
2788 LineEnding::Windows,
2789 )
2790 .await
2791 .unwrap();
2792 cx.foreground().run_until_parked();
2793 buffer1.read_with(cx, |buffer, _| {
2794 assert_eq!(buffer.text(), "aaa\nb\nc\n");
2795 assert_eq!(buffer.line_ending(), LineEnding::Windows);
2796 });
2797
2798 // Save a file with windows line endings. The file is written correctly.
2799 buffer2.update(cx, |buffer, cx| {
2800 buffer.set_text("one\ntwo\nthree\nfour\n", cx);
2801 });
2802 project
2803 .update(cx, |project, cx| project.save_buffer(buffer2, cx))
2804 .await
2805 .unwrap();
2806 assert_eq!(
2807 fs.load("/dir/file2".as_ref()).await.unwrap(),
2808 "one\r\ntwo\r\nthree\r\nfour\r\n",
2809 );
2810}
2811
2812#[gpui::test]
2813async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
2814 cx.foreground().forbid_parking();
2815
2816 let fs = FakeFs::new(cx.background());
2817 fs.insert_tree(
2818 "/the-dir",
2819 json!({
2820 "a.rs": "
2821 fn foo(mut v: Vec<usize>) {
2822 for x in &v {
2823 v.push(1);
2824 }
2825 }
2826 "
2827 .unindent(),
2828 }),
2829 )
2830 .await;
2831
2832 let project = Project::test(fs.clone(), ["/the-dir".as_ref()], cx).await;
2833 let buffer = project
2834 .update(cx, |p, cx| p.open_local_buffer("/the-dir/a.rs", cx))
2835 .await
2836 .unwrap();
2837
2838 let buffer_uri = Url::from_file_path("/the-dir/a.rs").unwrap();
2839 let message = lsp::PublishDiagnosticsParams {
2840 uri: buffer_uri.clone(),
2841 diagnostics: vec![
2842 lsp::Diagnostic {
2843 range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
2844 severity: Some(DiagnosticSeverity::WARNING),
2845 message: "error 1".to_string(),
2846 related_information: Some(vec![lsp::DiagnosticRelatedInformation {
2847 location: lsp::Location {
2848 uri: buffer_uri.clone(),
2849 range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
2850 },
2851 message: "error 1 hint 1".to_string(),
2852 }]),
2853 ..Default::default()
2854 },
2855 lsp::Diagnostic {
2856 range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
2857 severity: Some(DiagnosticSeverity::HINT),
2858 message: "error 1 hint 1".to_string(),
2859 related_information: Some(vec![lsp::DiagnosticRelatedInformation {
2860 location: lsp::Location {
2861 uri: buffer_uri.clone(),
2862 range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
2863 },
2864 message: "original diagnostic".to_string(),
2865 }]),
2866 ..Default::default()
2867 },
2868 lsp::Diagnostic {
2869 range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)),
2870 severity: Some(DiagnosticSeverity::ERROR),
2871 message: "error 2".to_string(),
2872 related_information: Some(vec![
2873 lsp::DiagnosticRelatedInformation {
2874 location: lsp::Location {
2875 uri: buffer_uri.clone(),
2876 range: lsp::Range::new(
2877 lsp::Position::new(1, 13),
2878 lsp::Position::new(1, 15),
2879 ),
2880 },
2881 message: "error 2 hint 1".to_string(),
2882 },
2883 lsp::DiagnosticRelatedInformation {
2884 location: lsp::Location {
2885 uri: buffer_uri.clone(),
2886 range: lsp::Range::new(
2887 lsp::Position::new(1, 13),
2888 lsp::Position::new(1, 15),
2889 ),
2890 },
2891 message: "error 2 hint 2".to_string(),
2892 },
2893 ]),
2894 ..Default::default()
2895 },
2896 lsp::Diagnostic {
2897 range: lsp::Range::new(lsp::Position::new(1, 13), lsp::Position::new(1, 15)),
2898 severity: Some(DiagnosticSeverity::HINT),
2899 message: "error 2 hint 1".to_string(),
2900 related_information: Some(vec![lsp::DiagnosticRelatedInformation {
2901 location: lsp::Location {
2902 uri: buffer_uri.clone(),
2903 range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)),
2904 },
2905 message: "original diagnostic".to_string(),
2906 }]),
2907 ..Default::default()
2908 },
2909 lsp::Diagnostic {
2910 range: lsp::Range::new(lsp::Position::new(1, 13), lsp::Position::new(1, 15)),
2911 severity: Some(DiagnosticSeverity::HINT),
2912 message: "error 2 hint 2".to_string(),
2913 related_information: Some(vec![lsp::DiagnosticRelatedInformation {
2914 location: lsp::Location {
2915 uri: buffer_uri,
2916 range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)),
2917 },
2918 message: "original diagnostic".to_string(),
2919 }]),
2920 ..Default::default()
2921 },
2922 ],
2923 version: None,
2924 };
2925
2926 project
2927 .update(cx, |p, cx| p.update_diagnostics(0, message, &[], cx))
2928 .unwrap();
2929 let buffer = buffer.read_with(cx, |buffer, _| buffer.snapshot());
2930
2931 assert_eq!(
2932 buffer
2933 .diagnostics_in_range::<_, Point>(0..buffer.len(), false)
2934 .collect::<Vec<_>>(),
2935 &[
2936 DiagnosticEntry {
2937 range: Point::new(1, 8)..Point::new(1, 9),
2938 diagnostic: Diagnostic {
2939 severity: DiagnosticSeverity::WARNING,
2940 message: "error 1".to_string(),
2941 group_id: 1,
2942 is_primary: true,
2943 ..Default::default()
2944 }
2945 },
2946 DiagnosticEntry {
2947 range: Point::new(1, 8)..Point::new(1, 9),
2948 diagnostic: Diagnostic {
2949 severity: DiagnosticSeverity::HINT,
2950 message: "error 1 hint 1".to_string(),
2951 group_id: 1,
2952 is_primary: false,
2953 ..Default::default()
2954 }
2955 },
2956 DiagnosticEntry {
2957 range: Point::new(1, 13)..Point::new(1, 15),
2958 diagnostic: Diagnostic {
2959 severity: DiagnosticSeverity::HINT,
2960 message: "error 2 hint 1".to_string(),
2961 group_id: 0,
2962 is_primary: false,
2963 ..Default::default()
2964 }
2965 },
2966 DiagnosticEntry {
2967 range: Point::new(1, 13)..Point::new(1, 15),
2968 diagnostic: Diagnostic {
2969 severity: DiagnosticSeverity::HINT,
2970 message: "error 2 hint 2".to_string(),
2971 group_id: 0,
2972 is_primary: false,
2973 ..Default::default()
2974 }
2975 },
2976 DiagnosticEntry {
2977 range: Point::new(2, 8)..Point::new(2, 17),
2978 diagnostic: Diagnostic {
2979 severity: DiagnosticSeverity::ERROR,
2980 message: "error 2".to_string(),
2981 group_id: 0,
2982 is_primary: true,
2983 ..Default::default()
2984 }
2985 }
2986 ]
2987 );
2988
2989 assert_eq!(
2990 buffer.diagnostic_group::<Point>(0).collect::<Vec<_>>(),
2991 &[
2992 DiagnosticEntry {
2993 range: Point::new(1, 13)..Point::new(1, 15),
2994 diagnostic: Diagnostic {
2995 severity: DiagnosticSeverity::HINT,
2996 message: "error 2 hint 1".to_string(),
2997 group_id: 0,
2998 is_primary: false,
2999 ..Default::default()
3000 }
3001 },
3002 DiagnosticEntry {
3003 range: Point::new(1, 13)..Point::new(1, 15),
3004 diagnostic: Diagnostic {
3005 severity: DiagnosticSeverity::HINT,
3006 message: "error 2 hint 2".to_string(),
3007 group_id: 0,
3008 is_primary: false,
3009 ..Default::default()
3010 }
3011 },
3012 DiagnosticEntry {
3013 range: Point::new(2, 8)..Point::new(2, 17),
3014 diagnostic: Diagnostic {
3015 severity: DiagnosticSeverity::ERROR,
3016 message: "error 2".to_string(),
3017 group_id: 0,
3018 is_primary: true,
3019 ..Default::default()
3020 }
3021 }
3022 ]
3023 );
3024
3025 assert_eq!(
3026 buffer.diagnostic_group::<Point>(1).collect::<Vec<_>>(),
3027 &[
3028 DiagnosticEntry {
3029 range: Point::new(1, 8)..Point::new(1, 9),
3030 diagnostic: Diagnostic {
3031 severity: DiagnosticSeverity::WARNING,
3032 message: "error 1".to_string(),
3033 group_id: 1,
3034 is_primary: true,
3035 ..Default::default()
3036 }
3037 },
3038 DiagnosticEntry {
3039 range: Point::new(1, 8)..Point::new(1, 9),
3040 diagnostic: Diagnostic {
3041 severity: DiagnosticSeverity::HINT,
3042 message: "error 1 hint 1".to_string(),
3043 group_id: 1,
3044 is_primary: false,
3045 ..Default::default()
3046 }
3047 },
3048 ]
3049 );
3050}
3051
3052#[gpui::test]
3053async fn test_rename(cx: &mut gpui::TestAppContext) {
3054 cx.foreground().forbid_parking();
3055
3056 let mut language = Language::new(
3057 LanguageConfig {
3058 name: "Rust".into(),
3059 path_suffixes: vec!["rs".to_string()],
3060 ..Default::default()
3061 },
3062 Some(tree_sitter_rust::language()),
3063 );
3064 let mut fake_servers = language
3065 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
3066 capabilities: lsp::ServerCapabilities {
3067 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
3068 prepare_provider: Some(true),
3069 work_done_progress_options: Default::default(),
3070 })),
3071 ..Default::default()
3072 },
3073 ..Default::default()
3074 }))
3075 .await;
3076
3077 let fs = FakeFs::new(cx.background());
3078 fs.insert_tree(
3079 "/dir",
3080 json!({
3081 "one.rs": "const ONE: usize = 1;",
3082 "two.rs": "const TWO: usize = one::ONE + one::ONE;"
3083 }),
3084 )
3085 .await;
3086
3087 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
3088 project.update(cx, |project, _| project.languages.add(Arc::new(language)));
3089 let buffer = project
3090 .update(cx, |project, cx| {
3091 project.open_local_buffer("/dir/one.rs", cx)
3092 })
3093 .await
3094 .unwrap();
3095
3096 let fake_server = fake_servers.next().await.unwrap();
3097
3098 let response = project.update(cx, |project, cx| {
3099 project.prepare_rename(buffer.clone(), 7, cx)
3100 });
3101 fake_server
3102 .handle_request::<lsp::request::PrepareRenameRequest, _, _>(|params, _| async move {
3103 assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs");
3104 assert_eq!(params.position, lsp::Position::new(0, 7));
3105 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
3106 lsp::Position::new(0, 6),
3107 lsp::Position::new(0, 9),
3108 ))))
3109 })
3110 .next()
3111 .await
3112 .unwrap();
3113 let range = response.await.unwrap().unwrap();
3114 let range = buffer.read_with(cx, |buffer, _| range.to_offset(buffer));
3115 assert_eq!(range, 6..9);
3116
3117 let response = project.update(cx, |project, cx| {
3118 project.perform_rename(buffer.clone(), 7, "THREE".to_string(), true, cx)
3119 });
3120 fake_server
3121 .handle_request::<lsp::request::Rename, _, _>(|params, _| async move {
3122 assert_eq!(
3123 params.text_document_position.text_document.uri.as_str(),
3124 "file:///dir/one.rs"
3125 );
3126 assert_eq!(
3127 params.text_document_position.position,
3128 lsp::Position::new(0, 7)
3129 );
3130 assert_eq!(params.new_name, "THREE");
3131 Ok(Some(lsp::WorkspaceEdit {
3132 changes: Some(
3133 [
3134 (
3135 lsp::Url::from_file_path("/dir/one.rs").unwrap(),
3136 vec![lsp::TextEdit::new(
3137 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
3138 "THREE".to_string(),
3139 )],
3140 ),
3141 (
3142 lsp::Url::from_file_path("/dir/two.rs").unwrap(),
3143 vec![
3144 lsp::TextEdit::new(
3145 lsp::Range::new(
3146 lsp::Position::new(0, 24),
3147 lsp::Position::new(0, 27),
3148 ),
3149 "THREE".to_string(),
3150 ),
3151 lsp::TextEdit::new(
3152 lsp::Range::new(
3153 lsp::Position::new(0, 35),
3154 lsp::Position::new(0, 38),
3155 ),
3156 "THREE".to_string(),
3157 ),
3158 ],
3159 ),
3160 ]
3161 .into_iter()
3162 .collect(),
3163 ),
3164 ..Default::default()
3165 }))
3166 })
3167 .next()
3168 .await
3169 .unwrap();
3170 let mut transaction = response.await.unwrap().0;
3171 assert_eq!(transaction.len(), 2);
3172 assert_eq!(
3173 transaction
3174 .remove_entry(&buffer)
3175 .unwrap()
3176 .0
3177 .read_with(cx, |buffer, _| buffer.text()),
3178 "const THREE: usize = 1;"
3179 );
3180 assert_eq!(
3181 transaction
3182 .into_keys()
3183 .next()
3184 .unwrap()
3185 .read_with(cx, |buffer, _| buffer.text()),
3186 "const TWO: usize = one::THREE + one::THREE;"
3187 );
3188}
3189
3190#[gpui::test]
3191async fn test_search(cx: &mut gpui::TestAppContext) {
3192 let fs = FakeFs::new(cx.background());
3193 fs.insert_tree(
3194 "/dir",
3195 json!({
3196 "one.rs": "const ONE: usize = 1;",
3197 "two.rs": "const TWO: usize = one::ONE + one::ONE;",
3198 "three.rs": "const THREE: usize = one::ONE + two::TWO;",
3199 "four.rs": "const FOUR: usize = one::ONE + three::THREE;",
3200 }),
3201 )
3202 .await;
3203 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
3204 assert_eq!(
3205 search(&project, SearchQuery::text("TWO", false, true), cx)
3206 .await
3207 .unwrap(),
3208 HashMap::from_iter([
3209 ("two.rs".to_string(), vec![6..9]),
3210 ("three.rs".to_string(), vec![37..40])
3211 ])
3212 );
3213
3214 let buffer_4 = project
3215 .update(cx, |project, cx| {
3216 project.open_local_buffer("/dir/four.rs", cx)
3217 })
3218 .await
3219 .unwrap();
3220 buffer_4.update(cx, |buffer, cx| {
3221 let text = "two::TWO";
3222 buffer.edit([(20..28, text), (31..43, text)], None, cx);
3223 });
3224
3225 assert_eq!(
3226 search(&project, SearchQuery::text("TWO", false, true), cx)
3227 .await
3228 .unwrap(),
3229 HashMap::from_iter([
3230 ("two.rs".to_string(), vec![6..9]),
3231 ("three.rs".to_string(), vec![37..40]),
3232 ("four.rs".to_string(), vec![25..28, 36..39])
3233 ])
3234 );
3235
3236 async fn search(
3237 project: &ModelHandle<Project>,
3238 query: SearchQuery,
3239 cx: &mut gpui::TestAppContext,
3240 ) -> Result<HashMap<String, Vec<Range<usize>>>> {
3241 let results = project
3242 .update(cx, |project, cx| project.search(query, cx))
3243 .await?;
3244
3245 Ok(results
3246 .into_iter()
3247 .map(|(buffer, ranges)| {
3248 buffer.read_with(cx, |buffer, _| {
3249 let path = buffer.file().unwrap().path().to_string_lossy().to_string();
3250 let ranges = ranges
3251 .into_iter()
3252 .map(|range| range.to_offset(buffer))
3253 .collect::<Vec<_>>();
3254 (path, ranges)
3255 })
3256 })
3257 .collect())
3258 }
3259}