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