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