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 LanguageServerId(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 LanguageServerId(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 LanguageServerId(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 LanguageServerId(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: LanguageServerId(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: LanguageServerId(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: LanguageServerId(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: LanguageServerId(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: LanguageServerId(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 [LanguageServerId(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: LanguageServerId(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 [LanguageServerId(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 LanguageServerId(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_diagnostics_from_multiple_language_servers(cx: &mut gpui::TestAppContext) {
1454 println!("hello from stdout");
1455 eprintln!("hello from stderr");
1456 cx.foreground().forbid_parking();
1457
1458 let fs = FakeFs::new(cx.background());
1459 fs.insert_tree("/dir", json!({ "a.rs": "one two three" }))
1460 .await;
1461
1462 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
1463
1464 project.update(cx, |project, cx| {
1465 project
1466 .update_diagnostic_entries(
1467 LanguageServerId(0),
1468 Path::new("/dir/a.rs").to_owned(),
1469 None,
1470 vec![DiagnosticEntry {
1471 range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 3)),
1472 diagnostic: Diagnostic {
1473 severity: DiagnosticSeverity::ERROR,
1474 is_primary: true,
1475 message: "syntax error a1".to_string(),
1476 ..Default::default()
1477 },
1478 }],
1479 cx,
1480 )
1481 .unwrap();
1482 project
1483 .update_diagnostic_entries(
1484 LanguageServerId(1),
1485 Path::new("/dir/a.rs").to_owned(),
1486 None,
1487 vec![DiagnosticEntry {
1488 range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 3)),
1489 diagnostic: Diagnostic {
1490 severity: DiagnosticSeverity::ERROR,
1491 is_primary: true,
1492 message: "syntax error b1".to_string(),
1493 ..Default::default()
1494 },
1495 }],
1496 cx,
1497 )
1498 .unwrap();
1499
1500 assert_eq!(
1501 project.diagnostic_summary(cx),
1502 DiagnosticSummary {
1503 error_count: 2,
1504 warning_count: 0,
1505 }
1506 );
1507 });
1508}
1509
1510#[gpui::test]
1511async fn test_edits_from_lsp_with_past_version(cx: &mut gpui::TestAppContext) {
1512 cx.foreground().forbid_parking();
1513
1514 let mut language = Language::new(
1515 LanguageConfig {
1516 name: "Rust".into(),
1517 path_suffixes: vec!["rs".to_string()],
1518 ..Default::default()
1519 },
1520 Some(tree_sitter_rust::language()),
1521 );
1522 let mut fake_servers = language.set_fake_lsp_adapter(Default::default()).await;
1523
1524 let text = "
1525 fn a() {
1526 f1();
1527 }
1528 fn b() {
1529 f2();
1530 }
1531 fn c() {
1532 f3();
1533 }
1534 "
1535 .unindent();
1536
1537 let fs = FakeFs::new(cx.background());
1538 fs.insert_tree(
1539 "/dir",
1540 json!({
1541 "a.rs": text.clone(),
1542 }),
1543 )
1544 .await;
1545
1546 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
1547 project.update(cx, |project, _| project.languages.add(Arc::new(language)));
1548 let buffer = project
1549 .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
1550 .await
1551 .unwrap();
1552
1553 let mut fake_server = fake_servers.next().await.unwrap();
1554 let lsp_document_version = fake_server
1555 .receive_notification::<lsp::notification::DidOpenTextDocument>()
1556 .await
1557 .text_document
1558 .version;
1559
1560 // Simulate editing the buffer after the language server computes some edits.
1561 buffer.update(cx, |buffer, cx| {
1562 buffer.edit(
1563 [(
1564 Point::new(0, 0)..Point::new(0, 0),
1565 "// above first function\n",
1566 )],
1567 None,
1568 cx,
1569 );
1570 buffer.edit(
1571 [(
1572 Point::new(2, 0)..Point::new(2, 0),
1573 " // inside first function\n",
1574 )],
1575 None,
1576 cx,
1577 );
1578 buffer.edit(
1579 [(
1580 Point::new(6, 4)..Point::new(6, 4),
1581 "// inside second function ",
1582 )],
1583 None,
1584 cx,
1585 );
1586
1587 assert_eq!(
1588 buffer.text(),
1589 "
1590 // above first function
1591 fn a() {
1592 // inside first function
1593 f1();
1594 }
1595 fn b() {
1596 // inside second function f2();
1597 }
1598 fn c() {
1599 f3();
1600 }
1601 "
1602 .unindent()
1603 );
1604 });
1605
1606 let edits = project
1607 .update(cx, |project, cx| {
1608 project.edits_from_lsp(
1609 &buffer,
1610 vec![
1611 // replace body of first function
1612 lsp::TextEdit {
1613 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(3, 0)),
1614 new_text: "
1615 fn a() {
1616 f10();
1617 }
1618 "
1619 .unindent(),
1620 },
1621 // edit inside second function
1622 lsp::TextEdit {
1623 range: lsp::Range::new(lsp::Position::new(4, 6), lsp::Position::new(4, 6)),
1624 new_text: "00".into(),
1625 },
1626 // edit inside third function via two distinct edits
1627 lsp::TextEdit {
1628 range: lsp::Range::new(lsp::Position::new(7, 5), lsp::Position::new(7, 5)),
1629 new_text: "4000".into(),
1630 },
1631 lsp::TextEdit {
1632 range: lsp::Range::new(lsp::Position::new(7, 5), lsp::Position::new(7, 6)),
1633 new_text: "".into(),
1634 },
1635 ],
1636 LanguageServerId(0),
1637 Some(lsp_document_version),
1638 cx,
1639 )
1640 })
1641 .await
1642 .unwrap();
1643
1644 buffer.update(cx, |buffer, cx| {
1645 for (range, new_text) in edits {
1646 buffer.edit([(range, new_text)], None, cx);
1647 }
1648 assert_eq!(
1649 buffer.text(),
1650 "
1651 // above first function
1652 fn a() {
1653 // inside first function
1654 f10();
1655 }
1656 fn b() {
1657 // inside second function f200();
1658 }
1659 fn c() {
1660 f4000();
1661 }
1662 "
1663 .unindent()
1664 );
1665 });
1666}
1667
1668#[gpui::test]
1669async fn test_edits_from_lsp_with_edits_on_adjacent_lines(cx: &mut gpui::TestAppContext) {
1670 cx.foreground().forbid_parking();
1671
1672 let text = "
1673 use a::b;
1674 use a::c;
1675
1676 fn f() {
1677 b();
1678 c();
1679 }
1680 "
1681 .unindent();
1682
1683 let fs = FakeFs::new(cx.background());
1684 fs.insert_tree(
1685 "/dir",
1686 json!({
1687 "a.rs": text.clone(),
1688 }),
1689 )
1690 .await;
1691
1692 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
1693 let buffer = project
1694 .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
1695 .await
1696 .unwrap();
1697
1698 // Simulate the language server sending us a small edit in the form of a very large diff.
1699 // Rust-analyzer does this when performing a merge-imports code action.
1700 let edits = project
1701 .update(cx, |project, cx| {
1702 project.edits_from_lsp(
1703 &buffer,
1704 [
1705 // Replace the first use statement without editing the semicolon.
1706 lsp::TextEdit {
1707 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 8)),
1708 new_text: "a::{b, c}".into(),
1709 },
1710 // Reinsert the remainder of the file between the semicolon and the final
1711 // newline of the file.
1712 lsp::TextEdit {
1713 range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 9)),
1714 new_text: "\n\n".into(),
1715 },
1716 lsp::TextEdit {
1717 range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 9)),
1718 new_text: "
1719 fn f() {
1720 b();
1721 c();
1722 }"
1723 .unindent(),
1724 },
1725 // Delete everything after the first newline of the file.
1726 lsp::TextEdit {
1727 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(7, 0)),
1728 new_text: "".into(),
1729 },
1730 ],
1731 LanguageServerId(0),
1732 None,
1733 cx,
1734 )
1735 })
1736 .await
1737 .unwrap();
1738
1739 buffer.update(cx, |buffer, cx| {
1740 let edits = edits
1741 .into_iter()
1742 .map(|(range, text)| {
1743 (
1744 range.start.to_point(buffer)..range.end.to_point(buffer),
1745 text,
1746 )
1747 })
1748 .collect::<Vec<_>>();
1749
1750 assert_eq!(
1751 edits,
1752 [
1753 (Point::new(0, 4)..Point::new(0, 8), "a::{b, c}".into()),
1754 (Point::new(1, 0)..Point::new(2, 0), "".into())
1755 ]
1756 );
1757
1758 for (range, new_text) in edits {
1759 buffer.edit([(range, new_text)], None, cx);
1760 }
1761 assert_eq!(
1762 buffer.text(),
1763 "
1764 use a::{b, c};
1765
1766 fn f() {
1767 b();
1768 c();
1769 }
1770 "
1771 .unindent()
1772 );
1773 });
1774}
1775
1776#[gpui::test]
1777async fn test_invalid_edits_from_lsp(cx: &mut gpui::TestAppContext) {
1778 cx.foreground().forbid_parking();
1779
1780 let text = "
1781 use a::b;
1782 use a::c;
1783
1784 fn f() {
1785 b();
1786 c();
1787 }
1788 "
1789 .unindent();
1790
1791 let fs = FakeFs::new(cx.background());
1792 fs.insert_tree(
1793 "/dir",
1794 json!({
1795 "a.rs": text.clone(),
1796 }),
1797 )
1798 .await;
1799
1800 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
1801 let buffer = project
1802 .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
1803 .await
1804 .unwrap();
1805
1806 // Simulate the language server sending us edits in a non-ordered fashion,
1807 // with ranges sometimes being inverted or pointing to invalid locations.
1808 let edits = project
1809 .update(cx, |project, cx| {
1810 project.edits_from_lsp(
1811 &buffer,
1812 [
1813 lsp::TextEdit {
1814 range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 9)),
1815 new_text: "\n\n".into(),
1816 },
1817 lsp::TextEdit {
1818 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 4)),
1819 new_text: "a::{b, c}".into(),
1820 },
1821 lsp::TextEdit {
1822 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(99, 0)),
1823 new_text: "".into(),
1824 },
1825 lsp::TextEdit {
1826 range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 9)),
1827 new_text: "
1828 fn f() {
1829 b();
1830 c();
1831 }"
1832 .unindent(),
1833 },
1834 ],
1835 LanguageServerId(0),
1836 None,
1837 cx,
1838 )
1839 })
1840 .await
1841 .unwrap();
1842
1843 buffer.update(cx, |buffer, cx| {
1844 let edits = edits
1845 .into_iter()
1846 .map(|(range, text)| {
1847 (
1848 range.start.to_point(buffer)..range.end.to_point(buffer),
1849 text,
1850 )
1851 })
1852 .collect::<Vec<_>>();
1853
1854 assert_eq!(
1855 edits,
1856 [
1857 (Point::new(0, 4)..Point::new(0, 8), "a::{b, c}".into()),
1858 (Point::new(1, 0)..Point::new(2, 0), "".into())
1859 ]
1860 );
1861
1862 for (range, new_text) in edits {
1863 buffer.edit([(range, new_text)], None, cx);
1864 }
1865 assert_eq!(
1866 buffer.text(),
1867 "
1868 use a::{b, c};
1869
1870 fn f() {
1871 b();
1872 c();
1873 }
1874 "
1875 .unindent()
1876 );
1877 });
1878}
1879
1880fn chunks_with_diagnostics<T: ToOffset + ToPoint>(
1881 buffer: &Buffer,
1882 range: Range<T>,
1883) -> Vec<(String, Option<DiagnosticSeverity>)> {
1884 let mut chunks: Vec<(String, Option<DiagnosticSeverity>)> = Vec::new();
1885 for chunk in buffer.snapshot().chunks(range, true) {
1886 if chunks.last().map_or(false, |prev_chunk| {
1887 prev_chunk.1 == chunk.diagnostic_severity
1888 }) {
1889 chunks.last_mut().unwrap().0.push_str(chunk.text);
1890 } else {
1891 chunks.push((chunk.text.to_string(), chunk.diagnostic_severity));
1892 }
1893 }
1894 chunks
1895}
1896
1897#[gpui::test(iterations = 10)]
1898async fn test_definition(cx: &mut gpui::TestAppContext) {
1899 let mut language = Language::new(
1900 LanguageConfig {
1901 name: "Rust".into(),
1902 path_suffixes: vec!["rs".to_string()],
1903 ..Default::default()
1904 },
1905 Some(tree_sitter_rust::language()),
1906 );
1907 let mut fake_servers = language.set_fake_lsp_adapter(Default::default()).await;
1908
1909 let fs = FakeFs::new(cx.background());
1910 fs.insert_tree(
1911 "/dir",
1912 json!({
1913 "a.rs": "const fn a() { A }",
1914 "b.rs": "const y: i32 = crate::a()",
1915 }),
1916 )
1917 .await;
1918
1919 let project = Project::test(fs, ["/dir/b.rs".as_ref()], cx).await;
1920 project.update(cx, |project, _| project.languages.add(Arc::new(language)));
1921
1922 let buffer = project
1923 .update(cx, |project, cx| project.open_local_buffer("/dir/b.rs", cx))
1924 .await
1925 .unwrap();
1926
1927 let fake_server = fake_servers.next().await.unwrap();
1928 fake_server.handle_request::<lsp::request::GotoDefinition, _, _>(|params, _| async move {
1929 let params = params.text_document_position_params;
1930 assert_eq!(
1931 params.text_document.uri.to_file_path().unwrap(),
1932 Path::new("/dir/b.rs"),
1933 );
1934 assert_eq!(params.position, lsp::Position::new(0, 22));
1935
1936 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
1937 lsp::Location::new(
1938 lsp::Url::from_file_path("/dir/a.rs").unwrap(),
1939 lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
1940 ),
1941 )))
1942 });
1943
1944 let mut definitions = project
1945 .update(cx, |project, cx| project.definition(&buffer, 22, cx))
1946 .await
1947 .unwrap();
1948
1949 // Assert no new language server started
1950 cx.foreground().run_until_parked();
1951 assert!(fake_servers.try_next().is_err());
1952
1953 assert_eq!(definitions.len(), 1);
1954 let definition = definitions.pop().unwrap();
1955 cx.update(|cx| {
1956 let target_buffer = definition.target.buffer.read(cx);
1957 assert_eq!(
1958 target_buffer
1959 .file()
1960 .unwrap()
1961 .as_local()
1962 .unwrap()
1963 .abs_path(cx),
1964 Path::new("/dir/a.rs"),
1965 );
1966 assert_eq!(definition.target.range.to_offset(target_buffer), 9..10);
1967 assert_eq!(
1968 list_worktrees(&project, cx),
1969 [("/dir/b.rs".as_ref(), true), ("/dir/a.rs".as_ref(), false)]
1970 );
1971
1972 drop(definition);
1973 });
1974 cx.read(|cx| {
1975 assert_eq!(list_worktrees(&project, cx), [("/dir/b.rs".as_ref(), true)]);
1976 });
1977
1978 fn list_worktrees<'a>(
1979 project: &'a ModelHandle<Project>,
1980 cx: &'a AppContext,
1981 ) -> Vec<(&'a Path, bool)> {
1982 project
1983 .read(cx)
1984 .worktrees(cx)
1985 .map(|worktree| {
1986 let worktree = worktree.read(cx);
1987 (
1988 worktree.as_local().unwrap().abs_path().as_ref(),
1989 worktree.is_visible(),
1990 )
1991 })
1992 .collect::<Vec<_>>()
1993 }
1994}
1995
1996#[gpui::test]
1997async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) {
1998 let mut language = Language::new(
1999 LanguageConfig {
2000 name: "TypeScript".into(),
2001 path_suffixes: vec!["ts".to_string()],
2002 ..Default::default()
2003 },
2004 Some(tree_sitter_typescript::language_typescript()),
2005 );
2006 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
2007
2008 let fs = FakeFs::new(cx.background());
2009 fs.insert_tree(
2010 "/dir",
2011 json!({
2012 "a.ts": "",
2013 }),
2014 )
2015 .await;
2016
2017 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
2018 project.update(cx, |project, _| project.languages.add(Arc::new(language)));
2019 let buffer = project
2020 .update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
2021 .await
2022 .unwrap();
2023
2024 let fake_server = fake_language_servers.next().await.unwrap();
2025
2026 let text = "let a = b.fqn";
2027 buffer.update(cx, |buffer, cx| buffer.set_text(text, cx));
2028 let completions = project.update(cx, |project, cx| {
2029 project.completions(&buffer, text.len(), cx)
2030 });
2031
2032 fake_server
2033 .handle_request::<lsp::request::Completion, _, _>(|_, _| async move {
2034 Ok(Some(lsp::CompletionResponse::Array(vec![
2035 lsp::CompletionItem {
2036 label: "fullyQualifiedName?".into(),
2037 insert_text: Some("fullyQualifiedName".into()),
2038 ..Default::default()
2039 },
2040 ])))
2041 })
2042 .next()
2043 .await;
2044 let completions = completions.await.unwrap();
2045 let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
2046 assert_eq!(completions.len(), 1);
2047 assert_eq!(completions[0].new_text, "fullyQualifiedName");
2048 assert_eq!(
2049 completions[0].old_range.to_offset(&snapshot),
2050 text.len() - 3..text.len()
2051 );
2052
2053 let text = "let a = \"atoms/cmp\"";
2054 buffer.update(cx, |buffer, cx| buffer.set_text(text, cx));
2055 let completions = project.update(cx, |project, cx| {
2056 project.completions(&buffer, text.len() - 1, cx)
2057 });
2058
2059 fake_server
2060 .handle_request::<lsp::request::Completion, _, _>(|_, _| async move {
2061 Ok(Some(lsp::CompletionResponse::Array(vec![
2062 lsp::CompletionItem {
2063 label: "component".into(),
2064 ..Default::default()
2065 },
2066 ])))
2067 })
2068 .next()
2069 .await;
2070 let completions = completions.await.unwrap();
2071 let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
2072 assert_eq!(completions.len(), 1);
2073 assert_eq!(completions[0].new_text, "component");
2074 assert_eq!(
2075 completions[0].old_range.to_offset(&snapshot),
2076 text.len() - 4..text.len() - 1
2077 );
2078}
2079
2080#[gpui::test]
2081async fn test_completions_with_carriage_returns(cx: &mut gpui::TestAppContext) {
2082 let mut language = Language::new(
2083 LanguageConfig {
2084 name: "TypeScript".into(),
2085 path_suffixes: vec!["ts".to_string()],
2086 ..Default::default()
2087 },
2088 Some(tree_sitter_typescript::language_typescript()),
2089 );
2090 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
2091
2092 let fs = FakeFs::new(cx.background());
2093 fs.insert_tree(
2094 "/dir",
2095 json!({
2096 "a.ts": "",
2097 }),
2098 )
2099 .await;
2100
2101 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
2102 project.update(cx, |project, _| project.languages.add(Arc::new(language)));
2103 let buffer = project
2104 .update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
2105 .await
2106 .unwrap();
2107
2108 let fake_server = fake_language_servers.next().await.unwrap();
2109
2110 let text = "let a = b.fqn";
2111 buffer.update(cx, |buffer, cx| buffer.set_text(text, cx));
2112 let completions = project.update(cx, |project, cx| {
2113 project.completions(&buffer, text.len(), cx)
2114 });
2115
2116 fake_server
2117 .handle_request::<lsp::request::Completion, _, _>(|_, _| async move {
2118 Ok(Some(lsp::CompletionResponse::Array(vec![
2119 lsp::CompletionItem {
2120 label: "fullyQualifiedName?".into(),
2121 insert_text: Some("fully\rQualified\r\nName".into()),
2122 ..Default::default()
2123 },
2124 ])))
2125 })
2126 .next()
2127 .await;
2128 let completions = completions.await.unwrap();
2129 assert_eq!(completions.len(), 1);
2130 assert_eq!(completions[0].new_text, "fully\nQualified\nName");
2131}
2132
2133#[gpui::test(iterations = 10)]
2134async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) {
2135 let mut language = Language::new(
2136 LanguageConfig {
2137 name: "TypeScript".into(),
2138 path_suffixes: vec!["ts".to_string()],
2139 ..Default::default()
2140 },
2141 None,
2142 );
2143 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
2144
2145 let fs = FakeFs::new(cx.background());
2146 fs.insert_tree(
2147 "/dir",
2148 json!({
2149 "a.ts": "a",
2150 }),
2151 )
2152 .await;
2153
2154 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
2155 project.update(cx, |project, _| project.languages.add(Arc::new(language)));
2156 let buffer = project
2157 .update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
2158 .await
2159 .unwrap();
2160
2161 let fake_server = fake_language_servers.next().await.unwrap();
2162
2163 // Language server returns code actions that contain commands, and not edits.
2164 let actions = project.update(cx, |project, cx| project.code_actions(&buffer, 0..0, cx));
2165 fake_server
2166 .handle_request::<lsp::request::CodeActionRequest, _, _>(|_, _| async move {
2167 Ok(Some(vec![
2168 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
2169 title: "The code action".into(),
2170 command: Some(lsp::Command {
2171 title: "The command".into(),
2172 command: "_the/command".into(),
2173 arguments: Some(vec![json!("the-argument")]),
2174 }),
2175 ..Default::default()
2176 }),
2177 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
2178 title: "two".into(),
2179 ..Default::default()
2180 }),
2181 ]))
2182 })
2183 .next()
2184 .await;
2185
2186 let action = actions.await.unwrap()[0].clone();
2187 let apply = project.update(cx, |project, cx| {
2188 project.apply_code_action(buffer.clone(), action, true, cx)
2189 });
2190
2191 // Resolving the code action does not populate its edits. In absence of
2192 // edits, we must execute the given command.
2193 fake_server.handle_request::<lsp::request::CodeActionResolveRequest, _, _>(
2194 |action, _| async move { Ok(action) },
2195 );
2196
2197 // While executing the command, the language server sends the editor
2198 // a `workspaceEdit` request.
2199 fake_server
2200 .handle_request::<lsp::request::ExecuteCommand, _, _>({
2201 let fake = fake_server.clone();
2202 move |params, _| {
2203 assert_eq!(params.command, "_the/command");
2204 let fake = fake.clone();
2205 async move {
2206 fake.server
2207 .request::<lsp::request::ApplyWorkspaceEdit>(
2208 lsp::ApplyWorkspaceEditParams {
2209 label: None,
2210 edit: lsp::WorkspaceEdit {
2211 changes: Some(
2212 [(
2213 lsp::Url::from_file_path("/dir/a.ts").unwrap(),
2214 vec![lsp::TextEdit {
2215 range: lsp::Range::new(
2216 lsp::Position::new(0, 0),
2217 lsp::Position::new(0, 0),
2218 ),
2219 new_text: "X".into(),
2220 }],
2221 )]
2222 .into_iter()
2223 .collect(),
2224 ),
2225 ..Default::default()
2226 },
2227 },
2228 )
2229 .await
2230 .unwrap();
2231 Ok(Some(json!(null)))
2232 }
2233 }
2234 })
2235 .next()
2236 .await;
2237
2238 // Applying the code action returns a project transaction containing the edits
2239 // sent by the language server in its `workspaceEdit` request.
2240 let transaction = apply.await.unwrap();
2241 assert!(transaction.0.contains_key(&buffer));
2242 buffer.update(cx, |buffer, cx| {
2243 assert_eq!(buffer.text(), "Xa");
2244 buffer.undo(cx);
2245 assert_eq!(buffer.text(), "a");
2246 });
2247}
2248
2249#[gpui::test(iterations = 10)]
2250async fn test_save_file(cx: &mut gpui::TestAppContext) {
2251 let fs = FakeFs::new(cx.background());
2252 fs.insert_tree(
2253 "/dir",
2254 json!({
2255 "file1": "the old contents",
2256 }),
2257 )
2258 .await;
2259
2260 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
2261 let buffer = project
2262 .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
2263 .await
2264 .unwrap();
2265 buffer.update(cx, |buffer, cx| {
2266 assert_eq!(buffer.text(), "the old contents");
2267 buffer.edit([(0..0, "a line of text.\n".repeat(10 * 1024))], None, cx);
2268 });
2269
2270 project
2271 .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
2272 .await
2273 .unwrap();
2274
2275 let new_text = fs.load(Path::new("/dir/file1")).await.unwrap();
2276 assert_eq!(new_text, buffer.read_with(cx, |buffer, _| buffer.text()));
2277}
2278
2279#[gpui::test]
2280async fn test_save_in_single_file_worktree(cx: &mut gpui::TestAppContext) {
2281 let fs = FakeFs::new(cx.background());
2282 fs.insert_tree(
2283 "/dir",
2284 json!({
2285 "file1": "the old contents",
2286 }),
2287 )
2288 .await;
2289
2290 let project = Project::test(fs.clone(), ["/dir/file1".as_ref()], cx).await;
2291 let buffer = project
2292 .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
2293 .await
2294 .unwrap();
2295 buffer.update(cx, |buffer, cx| {
2296 buffer.edit([(0..0, "a line of text.\n".repeat(10 * 1024))], None, cx);
2297 });
2298
2299 project
2300 .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
2301 .await
2302 .unwrap();
2303
2304 let new_text = fs.load(Path::new("/dir/file1")).await.unwrap();
2305 assert_eq!(new_text, buffer.read_with(cx, |buffer, _| buffer.text()));
2306}
2307
2308#[gpui::test]
2309async fn test_save_as(cx: &mut gpui::TestAppContext) {
2310 let fs = FakeFs::new(cx.background());
2311 fs.insert_tree("/dir", json!({})).await;
2312
2313 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
2314
2315 let languages = project.read_with(cx, |project, _| project.languages().clone());
2316 languages.register(
2317 "/some/path",
2318 LanguageConfig {
2319 name: "Rust".into(),
2320 path_suffixes: vec!["rs".into()],
2321 ..Default::default()
2322 },
2323 tree_sitter_rust::language(),
2324 vec![],
2325 |_| Default::default(),
2326 );
2327
2328 let buffer = project.update(cx, |project, cx| {
2329 project.create_buffer("", None, cx).unwrap()
2330 });
2331 buffer.update(cx, |buffer, cx| {
2332 buffer.edit([(0..0, "abc")], None, cx);
2333 assert!(buffer.is_dirty());
2334 assert!(!buffer.has_conflict());
2335 assert_eq!(buffer.language().unwrap().name().as_ref(), "Plain Text");
2336 });
2337 project
2338 .update(cx, |project, cx| {
2339 project.save_buffer_as(buffer.clone(), "/dir/file1.rs".into(), cx)
2340 })
2341 .await
2342 .unwrap();
2343 assert_eq!(fs.load(Path::new("/dir/file1.rs")).await.unwrap(), "abc");
2344
2345 cx.foreground().run_until_parked();
2346 buffer.read_with(cx, |buffer, cx| {
2347 assert_eq!(
2348 buffer.file().unwrap().full_path(cx),
2349 Path::new("dir/file1.rs")
2350 );
2351 assert!(!buffer.is_dirty());
2352 assert!(!buffer.has_conflict());
2353 assert_eq!(buffer.language().unwrap().name().as_ref(), "Rust");
2354 });
2355
2356 let opened_buffer = project
2357 .update(cx, |project, cx| {
2358 project.open_local_buffer("/dir/file1.rs", cx)
2359 })
2360 .await
2361 .unwrap();
2362 assert_eq!(opened_buffer, buffer);
2363}
2364
2365#[gpui::test(retries = 5)]
2366async fn test_rescan_and_remote_updates(
2367 deterministic: Arc<Deterministic>,
2368 cx: &mut gpui::TestAppContext,
2369) {
2370 let dir = temp_tree(json!({
2371 "a": {
2372 "file1": "",
2373 "file2": "",
2374 "file3": "",
2375 },
2376 "b": {
2377 "c": {
2378 "file4": "",
2379 "file5": "",
2380 }
2381 }
2382 }));
2383
2384 let project = Project::test(Arc::new(RealFs), [dir.path()], cx).await;
2385 let rpc = project.read_with(cx, |p, _| p.client.clone());
2386
2387 let buffer_for_path = |path: &'static str, cx: &mut gpui::TestAppContext| {
2388 let buffer = project.update(cx, |p, cx| p.open_local_buffer(dir.path().join(path), cx));
2389 async move { buffer.await.unwrap() }
2390 };
2391 let id_for_path = |path: &'static str, cx: &gpui::TestAppContext| {
2392 project.read_with(cx, |project, cx| {
2393 let tree = project.worktrees(cx).next().unwrap();
2394 tree.read(cx)
2395 .entry_for_path(path)
2396 .unwrap_or_else(|| panic!("no entry for path {}", path))
2397 .id
2398 })
2399 };
2400
2401 let buffer2 = buffer_for_path("a/file2", cx).await;
2402 let buffer3 = buffer_for_path("a/file3", cx).await;
2403 let buffer4 = buffer_for_path("b/c/file4", cx).await;
2404 let buffer5 = buffer_for_path("b/c/file5", cx).await;
2405
2406 let file2_id = id_for_path("a/file2", cx);
2407 let file3_id = id_for_path("a/file3", cx);
2408 let file4_id = id_for_path("b/c/file4", cx);
2409
2410 // Create a remote copy of this worktree.
2411 let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
2412 let initial_snapshot = tree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
2413 let remote = cx.update(|cx| {
2414 Worktree::remote(
2415 1,
2416 1,
2417 proto::WorktreeMetadata {
2418 id: initial_snapshot.id().to_proto(),
2419 root_name: initial_snapshot.root_name().into(),
2420 abs_path: initial_snapshot
2421 .abs_path()
2422 .as_os_str()
2423 .to_string_lossy()
2424 .into(),
2425 visible: true,
2426 },
2427 rpc.clone(),
2428 cx,
2429 )
2430 });
2431 remote.update(cx, |remote, _| {
2432 let update = initial_snapshot.build_initial_update(1);
2433 remote.as_remote_mut().unwrap().update_from_remote(update);
2434 });
2435 deterministic.run_until_parked();
2436
2437 cx.read(|cx| {
2438 assert!(!buffer2.read(cx).is_dirty());
2439 assert!(!buffer3.read(cx).is_dirty());
2440 assert!(!buffer4.read(cx).is_dirty());
2441 assert!(!buffer5.read(cx).is_dirty());
2442 });
2443
2444 // Rename and delete files and directories.
2445 tree.flush_fs_events(cx).await;
2446 std::fs::rename(dir.path().join("a/file3"), dir.path().join("b/c/file3")).unwrap();
2447 std::fs::remove_file(dir.path().join("b/c/file5")).unwrap();
2448 std::fs::rename(dir.path().join("b/c"), dir.path().join("d")).unwrap();
2449 std::fs::rename(dir.path().join("a/file2"), dir.path().join("a/file2.new")).unwrap();
2450 tree.flush_fs_events(cx).await;
2451
2452 let expected_paths = vec![
2453 "a",
2454 "a/file1",
2455 "a/file2.new",
2456 "b",
2457 "d",
2458 "d/file3",
2459 "d/file4",
2460 ];
2461
2462 cx.read(|app| {
2463 assert_eq!(
2464 tree.read(app)
2465 .paths()
2466 .map(|p| p.to_str().unwrap())
2467 .collect::<Vec<_>>(),
2468 expected_paths
2469 );
2470
2471 assert_eq!(id_for_path("a/file2.new", cx), file2_id);
2472 assert_eq!(id_for_path("d/file3", cx), file3_id);
2473 assert_eq!(id_for_path("d/file4", cx), file4_id);
2474
2475 assert_eq!(
2476 buffer2.read(app).file().unwrap().path().as_ref(),
2477 Path::new("a/file2.new")
2478 );
2479 assert_eq!(
2480 buffer3.read(app).file().unwrap().path().as_ref(),
2481 Path::new("d/file3")
2482 );
2483 assert_eq!(
2484 buffer4.read(app).file().unwrap().path().as_ref(),
2485 Path::new("d/file4")
2486 );
2487 assert_eq!(
2488 buffer5.read(app).file().unwrap().path().as_ref(),
2489 Path::new("b/c/file5")
2490 );
2491
2492 assert!(!buffer2.read(app).file().unwrap().is_deleted());
2493 assert!(!buffer3.read(app).file().unwrap().is_deleted());
2494 assert!(!buffer4.read(app).file().unwrap().is_deleted());
2495 assert!(buffer5.read(app).file().unwrap().is_deleted());
2496 });
2497
2498 // Update the remote worktree. Check that it becomes consistent with the
2499 // local worktree.
2500 remote.update(cx, |remote, cx| {
2501 let update = tree.read(cx).as_local().unwrap().snapshot().build_update(
2502 &initial_snapshot,
2503 1,
2504 1,
2505 true,
2506 );
2507 remote.as_remote_mut().unwrap().update_from_remote(update);
2508 });
2509 deterministic.run_until_parked();
2510 remote.read_with(cx, |remote, _| {
2511 assert_eq!(
2512 remote
2513 .paths()
2514 .map(|p| p.to_str().unwrap())
2515 .collect::<Vec<_>>(),
2516 expected_paths
2517 );
2518 });
2519}
2520
2521#[gpui::test(iterations = 10)]
2522async fn test_buffer_identity_across_renames(
2523 deterministic: Arc<Deterministic>,
2524 cx: &mut gpui::TestAppContext,
2525) {
2526 let fs = FakeFs::new(cx.background());
2527 fs.insert_tree(
2528 "/dir",
2529 json!({
2530 "a": {
2531 "file1": "",
2532 }
2533 }),
2534 )
2535 .await;
2536
2537 let project = Project::test(fs, [Path::new("/dir")], cx).await;
2538 let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
2539 let tree_id = tree.read_with(cx, |tree, _| tree.id());
2540
2541 let id_for_path = |path: &'static str, cx: &gpui::TestAppContext| {
2542 project.read_with(cx, |project, cx| {
2543 let tree = project.worktrees(cx).next().unwrap();
2544 tree.read(cx)
2545 .entry_for_path(path)
2546 .unwrap_or_else(|| panic!("no entry for path {}", path))
2547 .id
2548 })
2549 };
2550
2551 let dir_id = id_for_path("a", cx);
2552 let file_id = id_for_path("a/file1", cx);
2553 let buffer = project
2554 .update(cx, |p, cx| p.open_buffer((tree_id, "a/file1"), cx))
2555 .await
2556 .unwrap();
2557 buffer.read_with(cx, |buffer, _| assert!(!buffer.is_dirty()));
2558
2559 project
2560 .update(cx, |project, cx| {
2561 project.rename_entry(dir_id, Path::new("b"), cx)
2562 })
2563 .unwrap()
2564 .await
2565 .unwrap();
2566 deterministic.run_until_parked();
2567 assert_eq!(id_for_path("b", cx), dir_id);
2568 assert_eq!(id_for_path("b/file1", cx), file_id);
2569 buffer.read_with(cx, |buffer, _| assert!(!buffer.is_dirty()));
2570}
2571
2572#[gpui::test]
2573async fn test_buffer_deduping(cx: &mut gpui::TestAppContext) {
2574 let fs = FakeFs::new(cx.background());
2575 fs.insert_tree(
2576 "/dir",
2577 json!({
2578 "a.txt": "a-contents",
2579 "b.txt": "b-contents",
2580 }),
2581 )
2582 .await;
2583
2584 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
2585
2586 // Spawn multiple tasks to open paths, repeating some paths.
2587 let (buffer_a_1, buffer_b, buffer_a_2) = project.update(cx, |p, cx| {
2588 (
2589 p.open_local_buffer("/dir/a.txt", cx),
2590 p.open_local_buffer("/dir/b.txt", cx),
2591 p.open_local_buffer("/dir/a.txt", cx),
2592 )
2593 });
2594
2595 let buffer_a_1 = buffer_a_1.await.unwrap();
2596 let buffer_a_2 = buffer_a_2.await.unwrap();
2597 let buffer_b = buffer_b.await.unwrap();
2598 assert_eq!(buffer_a_1.read_with(cx, |b, _| b.text()), "a-contents");
2599 assert_eq!(buffer_b.read_with(cx, |b, _| b.text()), "b-contents");
2600
2601 // There is only one buffer per path.
2602 let buffer_a_id = buffer_a_1.id();
2603 assert_eq!(buffer_a_2.id(), buffer_a_id);
2604
2605 // Open the same path again while it is still open.
2606 drop(buffer_a_1);
2607 let buffer_a_3 = project
2608 .update(cx, |p, cx| p.open_local_buffer("/dir/a.txt", cx))
2609 .await
2610 .unwrap();
2611
2612 // There's still only one buffer per path.
2613 assert_eq!(buffer_a_3.id(), buffer_a_id);
2614}
2615
2616#[gpui::test]
2617async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) {
2618 let fs = FakeFs::new(cx.background());
2619 fs.insert_tree(
2620 "/dir",
2621 json!({
2622 "file1": "abc",
2623 "file2": "def",
2624 "file3": "ghi",
2625 }),
2626 )
2627 .await;
2628
2629 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
2630
2631 let buffer1 = project
2632 .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
2633 .await
2634 .unwrap();
2635 let events = Rc::new(RefCell::new(Vec::new()));
2636
2637 // initially, the buffer isn't dirty.
2638 buffer1.update(cx, |buffer, cx| {
2639 cx.subscribe(&buffer1, {
2640 let events = events.clone();
2641 move |_, _, event, _| match event {
2642 BufferEvent::Operation(_) => {}
2643 _ => events.borrow_mut().push(event.clone()),
2644 }
2645 })
2646 .detach();
2647
2648 assert!(!buffer.is_dirty());
2649 assert!(events.borrow().is_empty());
2650
2651 buffer.edit([(1..2, "")], None, cx);
2652 });
2653
2654 // after the first edit, the buffer is dirty, and emits a dirtied event.
2655 buffer1.update(cx, |buffer, cx| {
2656 assert!(buffer.text() == "ac");
2657 assert!(buffer.is_dirty());
2658 assert_eq!(
2659 *events.borrow(),
2660 &[language::Event::Edited, language::Event::DirtyChanged]
2661 );
2662 events.borrow_mut().clear();
2663 buffer.did_save(
2664 buffer.version(),
2665 buffer.as_rope().fingerprint(),
2666 buffer.file().unwrap().mtime(),
2667 cx,
2668 );
2669 });
2670
2671 // after saving, the buffer is not dirty, and emits a saved event.
2672 buffer1.update(cx, |buffer, cx| {
2673 assert!(!buffer.is_dirty());
2674 assert_eq!(*events.borrow(), &[language::Event::Saved]);
2675 events.borrow_mut().clear();
2676
2677 buffer.edit([(1..1, "B")], None, cx);
2678 buffer.edit([(2..2, "D")], None, cx);
2679 });
2680
2681 // after editing again, the buffer is dirty, and emits another dirty event.
2682 buffer1.update(cx, |buffer, cx| {
2683 assert!(buffer.text() == "aBDc");
2684 assert!(buffer.is_dirty());
2685 assert_eq!(
2686 *events.borrow(),
2687 &[
2688 language::Event::Edited,
2689 language::Event::DirtyChanged,
2690 language::Event::Edited,
2691 ],
2692 );
2693 events.borrow_mut().clear();
2694
2695 // After restoring the buffer to its previously-saved state,
2696 // the buffer is not considered dirty anymore.
2697 buffer.edit([(1..3, "")], None, cx);
2698 assert!(buffer.text() == "ac");
2699 assert!(!buffer.is_dirty());
2700 });
2701
2702 assert_eq!(
2703 *events.borrow(),
2704 &[language::Event::Edited, language::Event::DirtyChanged]
2705 );
2706
2707 // When a file is deleted, the buffer is considered dirty.
2708 let events = Rc::new(RefCell::new(Vec::new()));
2709 let buffer2 = project
2710 .update(cx, |p, cx| p.open_local_buffer("/dir/file2", cx))
2711 .await
2712 .unwrap();
2713 buffer2.update(cx, |_, cx| {
2714 cx.subscribe(&buffer2, {
2715 let events = events.clone();
2716 move |_, _, event, _| events.borrow_mut().push(event.clone())
2717 })
2718 .detach();
2719 });
2720
2721 fs.remove_file("/dir/file2".as_ref(), Default::default())
2722 .await
2723 .unwrap();
2724 cx.foreground().run_until_parked();
2725 buffer2.read_with(cx, |buffer, _| assert!(buffer.is_dirty()));
2726 assert_eq!(
2727 *events.borrow(),
2728 &[
2729 language::Event::DirtyChanged,
2730 language::Event::FileHandleChanged
2731 ]
2732 );
2733
2734 // When a file is already dirty when deleted, we don't emit a Dirtied event.
2735 let events = Rc::new(RefCell::new(Vec::new()));
2736 let buffer3 = project
2737 .update(cx, |p, cx| p.open_local_buffer("/dir/file3", cx))
2738 .await
2739 .unwrap();
2740 buffer3.update(cx, |_, cx| {
2741 cx.subscribe(&buffer3, {
2742 let events = events.clone();
2743 move |_, _, event, _| events.borrow_mut().push(event.clone())
2744 })
2745 .detach();
2746 });
2747
2748 buffer3.update(cx, |buffer, cx| {
2749 buffer.edit([(0..0, "x")], None, cx);
2750 });
2751 events.borrow_mut().clear();
2752 fs.remove_file("/dir/file3".as_ref(), Default::default())
2753 .await
2754 .unwrap();
2755 cx.foreground().run_until_parked();
2756 assert_eq!(*events.borrow(), &[language::Event::FileHandleChanged]);
2757 cx.read(|cx| assert!(buffer3.read(cx).is_dirty()));
2758}
2759
2760#[gpui::test]
2761async fn test_buffer_file_changes_on_disk(cx: &mut gpui::TestAppContext) {
2762 let initial_contents = "aaa\nbbbbb\nc\n";
2763 let fs = FakeFs::new(cx.background());
2764 fs.insert_tree(
2765 "/dir",
2766 json!({
2767 "the-file": initial_contents,
2768 }),
2769 )
2770 .await;
2771 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
2772 let buffer = project
2773 .update(cx, |p, cx| p.open_local_buffer("/dir/the-file", cx))
2774 .await
2775 .unwrap();
2776
2777 let anchors = (0..3)
2778 .map(|row| buffer.read_with(cx, |b, _| b.anchor_before(Point::new(row, 1))))
2779 .collect::<Vec<_>>();
2780
2781 // Change the file on disk, adding two new lines of text, and removing
2782 // one line.
2783 buffer.read_with(cx, |buffer, _| {
2784 assert!(!buffer.is_dirty());
2785 assert!(!buffer.has_conflict());
2786 });
2787 let new_contents = "AAAA\naaa\nBB\nbbbbb\n";
2788 fs.save(
2789 "/dir/the-file".as_ref(),
2790 &new_contents.into(),
2791 LineEnding::Unix,
2792 )
2793 .await
2794 .unwrap();
2795
2796 // Because the buffer was not modified, it is reloaded from disk. Its
2797 // contents are edited according to the diff between the old and new
2798 // file contents.
2799 cx.foreground().run_until_parked();
2800 buffer.update(cx, |buffer, _| {
2801 assert_eq!(buffer.text(), new_contents);
2802 assert!(!buffer.is_dirty());
2803 assert!(!buffer.has_conflict());
2804
2805 let anchor_positions = anchors
2806 .iter()
2807 .map(|anchor| anchor.to_point(&*buffer))
2808 .collect::<Vec<_>>();
2809 assert_eq!(
2810 anchor_positions,
2811 [Point::new(1, 1), Point::new(3, 1), Point::new(3, 5)]
2812 );
2813 });
2814
2815 // Modify the buffer
2816 buffer.update(cx, |buffer, cx| {
2817 buffer.edit([(0..0, " ")], None, cx);
2818 assert!(buffer.is_dirty());
2819 assert!(!buffer.has_conflict());
2820 });
2821
2822 // Change the file on disk again, adding blank lines to the beginning.
2823 fs.save(
2824 "/dir/the-file".as_ref(),
2825 &"\n\n\nAAAA\naaa\nBB\nbbbbb\n".into(),
2826 LineEnding::Unix,
2827 )
2828 .await
2829 .unwrap();
2830
2831 // Because the buffer is modified, it doesn't reload from disk, but is
2832 // marked as having a conflict.
2833 cx.foreground().run_until_parked();
2834 buffer.read_with(cx, |buffer, _| {
2835 assert!(buffer.has_conflict());
2836 });
2837}
2838
2839#[gpui::test]
2840async fn test_buffer_line_endings(cx: &mut gpui::TestAppContext) {
2841 let fs = FakeFs::new(cx.background());
2842 fs.insert_tree(
2843 "/dir",
2844 json!({
2845 "file1": "a\nb\nc\n",
2846 "file2": "one\r\ntwo\r\nthree\r\n",
2847 }),
2848 )
2849 .await;
2850
2851 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
2852 let buffer1 = project
2853 .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
2854 .await
2855 .unwrap();
2856 let buffer2 = project
2857 .update(cx, |p, cx| p.open_local_buffer("/dir/file2", cx))
2858 .await
2859 .unwrap();
2860
2861 buffer1.read_with(cx, |buffer, _| {
2862 assert_eq!(buffer.text(), "a\nb\nc\n");
2863 assert_eq!(buffer.line_ending(), LineEnding::Unix);
2864 });
2865 buffer2.read_with(cx, |buffer, _| {
2866 assert_eq!(buffer.text(), "one\ntwo\nthree\n");
2867 assert_eq!(buffer.line_ending(), LineEnding::Windows);
2868 });
2869
2870 // Change a file's line endings on disk from unix to windows. The buffer's
2871 // state updates correctly.
2872 fs.save(
2873 "/dir/file1".as_ref(),
2874 &"aaa\nb\nc\n".into(),
2875 LineEnding::Windows,
2876 )
2877 .await
2878 .unwrap();
2879 cx.foreground().run_until_parked();
2880 buffer1.read_with(cx, |buffer, _| {
2881 assert_eq!(buffer.text(), "aaa\nb\nc\n");
2882 assert_eq!(buffer.line_ending(), LineEnding::Windows);
2883 });
2884
2885 // Save a file with windows line endings. The file is written correctly.
2886 buffer2.update(cx, |buffer, cx| {
2887 buffer.set_text("one\ntwo\nthree\nfour\n", cx);
2888 });
2889 project
2890 .update(cx, |project, cx| project.save_buffer(buffer2, cx))
2891 .await
2892 .unwrap();
2893 assert_eq!(
2894 fs.load("/dir/file2".as_ref()).await.unwrap(),
2895 "one\r\ntwo\r\nthree\r\nfour\r\n",
2896 );
2897}
2898
2899#[gpui::test]
2900async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
2901 cx.foreground().forbid_parking();
2902
2903 let fs = FakeFs::new(cx.background());
2904 fs.insert_tree(
2905 "/the-dir",
2906 json!({
2907 "a.rs": "
2908 fn foo(mut v: Vec<usize>) {
2909 for x in &v {
2910 v.push(1);
2911 }
2912 }
2913 "
2914 .unindent(),
2915 }),
2916 )
2917 .await;
2918
2919 let project = Project::test(fs.clone(), ["/the-dir".as_ref()], cx).await;
2920 let buffer = project
2921 .update(cx, |p, cx| p.open_local_buffer("/the-dir/a.rs", cx))
2922 .await
2923 .unwrap();
2924
2925 let buffer_uri = Url::from_file_path("/the-dir/a.rs").unwrap();
2926 let message = lsp::PublishDiagnosticsParams {
2927 uri: buffer_uri.clone(),
2928 diagnostics: vec![
2929 lsp::Diagnostic {
2930 range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
2931 severity: Some(DiagnosticSeverity::WARNING),
2932 message: "error 1".to_string(),
2933 related_information: Some(vec![lsp::DiagnosticRelatedInformation {
2934 location: lsp::Location {
2935 uri: buffer_uri.clone(),
2936 range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
2937 },
2938 message: "error 1 hint 1".to_string(),
2939 }]),
2940 ..Default::default()
2941 },
2942 lsp::Diagnostic {
2943 range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
2944 severity: Some(DiagnosticSeverity::HINT),
2945 message: "error 1 hint 1".to_string(),
2946 related_information: Some(vec![lsp::DiagnosticRelatedInformation {
2947 location: lsp::Location {
2948 uri: buffer_uri.clone(),
2949 range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
2950 },
2951 message: "original diagnostic".to_string(),
2952 }]),
2953 ..Default::default()
2954 },
2955 lsp::Diagnostic {
2956 range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)),
2957 severity: Some(DiagnosticSeverity::ERROR),
2958 message: "error 2".to_string(),
2959 related_information: Some(vec![
2960 lsp::DiagnosticRelatedInformation {
2961 location: lsp::Location {
2962 uri: buffer_uri.clone(),
2963 range: lsp::Range::new(
2964 lsp::Position::new(1, 13),
2965 lsp::Position::new(1, 15),
2966 ),
2967 },
2968 message: "error 2 hint 1".to_string(),
2969 },
2970 lsp::DiagnosticRelatedInformation {
2971 location: lsp::Location {
2972 uri: buffer_uri.clone(),
2973 range: lsp::Range::new(
2974 lsp::Position::new(1, 13),
2975 lsp::Position::new(1, 15),
2976 ),
2977 },
2978 message: "error 2 hint 2".to_string(),
2979 },
2980 ]),
2981 ..Default::default()
2982 },
2983 lsp::Diagnostic {
2984 range: lsp::Range::new(lsp::Position::new(1, 13), lsp::Position::new(1, 15)),
2985 severity: Some(DiagnosticSeverity::HINT),
2986 message: "error 2 hint 1".to_string(),
2987 related_information: Some(vec![lsp::DiagnosticRelatedInformation {
2988 location: lsp::Location {
2989 uri: buffer_uri.clone(),
2990 range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)),
2991 },
2992 message: "original diagnostic".to_string(),
2993 }]),
2994 ..Default::default()
2995 },
2996 lsp::Diagnostic {
2997 range: lsp::Range::new(lsp::Position::new(1, 13), lsp::Position::new(1, 15)),
2998 severity: Some(DiagnosticSeverity::HINT),
2999 message: "error 2 hint 2".to_string(),
3000 related_information: Some(vec![lsp::DiagnosticRelatedInformation {
3001 location: lsp::Location {
3002 uri: buffer_uri,
3003 range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)),
3004 },
3005 message: "original diagnostic".to_string(),
3006 }]),
3007 ..Default::default()
3008 },
3009 ],
3010 version: None,
3011 };
3012
3013 project
3014 .update(cx, |p, cx| {
3015 p.update_diagnostics(LanguageServerId(0), message, &[], cx)
3016 })
3017 .unwrap();
3018 let buffer = buffer.read_with(cx, |buffer, _| buffer.snapshot());
3019
3020 assert_eq!(
3021 buffer
3022 .diagnostics_in_range::<_, Point>(0..buffer.len(), false)
3023 .collect::<Vec<_>>(),
3024 &[
3025 DiagnosticEntry {
3026 range: Point::new(1, 8)..Point::new(1, 9),
3027 diagnostic: Diagnostic {
3028 severity: DiagnosticSeverity::WARNING,
3029 message: "error 1".to_string(),
3030 group_id: 1,
3031 is_primary: true,
3032 ..Default::default()
3033 }
3034 },
3035 DiagnosticEntry {
3036 range: Point::new(1, 8)..Point::new(1, 9),
3037 diagnostic: Diagnostic {
3038 severity: DiagnosticSeverity::HINT,
3039 message: "error 1 hint 1".to_string(),
3040 group_id: 1,
3041 is_primary: false,
3042 ..Default::default()
3043 }
3044 },
3045 DiagnosticEntry {
3046 range: Point::new(1, 13)..Point::new(1, 15),
3047 diagnostic: Diagnostic {
3048 severity: DiagnosticSeverity::HINT,
3049 message: "error 2 hint 1".to_string(),
3050 group_id: 0,
3051 is_primary: false,
3052 ..Default::default()
3053 }
3054 },
3055 DiagnosticEntry {
3056 range: Point::new(1, 13)..Point::new(1, 15),
3057 diagnostic: Diagnostic {
3058 severity: DiagnosticSeverity::HINT,
3059 message: "error 2 hint 2".to_string(),
3060 group_id: 0,
3061 is_primary: false,
3062 ..Default::default()
3063 }
3064 },
3065 DiagnosticEntry {
3066 range: Point::new(2, 8)..Point::new(2, 17),
3067 diagnostic: Diagnostic {
3068 severity: DiagnosticSeverity::ERROR,
3069 message: "error 2".to_string(),
3070 group_id: 0,
3071 is_primary: true,
3072 ..Default::default()
3073 }
3074 }
3075 ]
3076 );
3077
3078 assert_eq!(
3079 buffer.diagnostic_group::<Point>(0).collect::<Vec<_>>(),
3080 &[
3081 DiagnosticEntry {
3082 range: Point::new(1, 13)..Point::new(1, 15),
3083 diagnostic: Diagnostic {
3084 severity: DiagnosticSeverity::HINT,
3085 message: "error 2 hint 1".to_string(),
3086 group_id: 0,
3087 is_primary: false,
3088 ..Default::default()
3089 }
3090 },
3091 DiagnosticEntry {
3092 range: Point::new(1, 13)..Point::new(1, 15),
3093 diagnostic: Diagnostic {
3094 severity: DiagnosticSeverity::HINT,
3095 message: "error 2 hint 2".to_string(),
3096 group_id: 0,
3097 is_primary: false,
3098 ..Default::default()
3099 }
3100 },
3101 DiagnosticEntry {
3102 range: Point::new(2, 8)..Point::new(2, 17),
3103 diagnostic: Diagnostic {
3104 severity: DiagnosticSeverity::ERROR,
3105 message: "error 2".to_string(),
3106 group_id: 0,
3107 is_primary: true,
3108 ..Default::default()
3109 }
3110 }
3111 ]
3112 );
3113
3114 assert_eq!(
3115 buffer.diagnostic_group::<Point>(1).collect::<Vec<_>>(),
3116 &[
3117 DiagnosticEntry {
3118 range: Point::new(1, 8)..Point::new(1, 9),
3119 diagnostic: Diagnostic {
3120 severity: DiagnosticSeverity::WARNING,
3121 message: "error 1".to_string(),
3122 group_id: 1,
3123 is_primary: true,
3124 ..Default::default()
3125 }
3126 },
3127 DiagnosticEntry {
3128 range: Point::new(1, 8)..Point::new(1, 9),
3129 diagnostic: Diagnostic {
3130 severity: DiagnosticSeverity::HINT,
3131 message: "error 1 hint 1".to_string(),
3132 group_id: 1,
3133 is_primary: false,
3134 ..Default::default()
3135 }
3136 },
3137 ]
3138 );
3139}
3140
3141#[gpui::test]
3142async fn test_rename(cx: &mut gpui::TestAppContext) {
3143 cx.foreground().forbid_parking();
3144
3145 let mut language = Language::new(
3146 LanguageConfig {
3147 name: "Rust".into(),
3148 path_suffixes: vec!["rs".to_string()],
3149 ..Default::default()
3150 },
3151 Some(tree_sitter_rust::language()),
3152 );
3153 let mut fake_servers = language
3154 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
3155 capabilities: lsp::ServerCapabilities {
3156 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
3157 prepare_provider: Some(true),
3158 work_done_progress_options: Default::default(),
3159 })),
3160 ..Default::default()
3161 },
3162 ..Default::default()
3163 }))
3164 .await;
3165
3166 let fs = FakeFs::new(cx.background());
3167 fs.insert_tree(
3168 "/dir",
3169 json!({
3170 "one.rs": "const ONE: usize = 1;",
3171 "two.rs": "const TWO: usize = one::ONE + one::ONE;"
3172 }),
3173 )
3174 .await;
3175
3176 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
3177 project.update(cx, |project, _| project.languages.add(Arc::new(language)));
3178 let buffer = project
3179 .update(cx, |project, cx| {
3180 project.open_local_buffer("/dir/one.rs", cx)
3181 })
3182 .await
3183 .unwrap();
3184
3185 let fake_server = fake_servers.next().await.unwrap();
3186
3187 let response = project.update(cx, |project, cx| {
3188 project.prepare_rename(buffer.clone(), 7, cx)
3189 });
3190 fake_server
3191 .handle_request::<lsp::request::PrepareRenameRequest, _, _>(|params, _| async move {
3192 assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs");
3193 assert_eq!(params.position, lsp::Position::new(0, 7));
3194 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
3195 lsp::Position::new(0, 6),
3196 lsp::Position::new(0, 9),
3197 ))))
3198 })
3199 .next()
3200 .await
3201 .unwrap();
3202 let range = response.await.unwrap().unwrap();
3203 let range = buffer.read_with(cx, |buffer, _| range.to_offset(buffer));
3204 assert_eq!(range, 6..9);
3205
3206 let response = project.update(cx, |project, cx| {
3207 project.perform_rename(buffer.clone(), 7, "THREE".to_string(), true, cx)
3208 });
3209 fake_server
3210 .handle_request::<lsp::request::Rename, _, _>(|params, _| async move {
3211 assert_eq!(
3212 params.text_document_position.text_document.uri.as_str(),
3213 "file:///dir/one.rs"
3214 );
3215 assert_eq!(
3216 params.text_document_position.position,
3217 lsp::Position::new(0, 7)
3218 );
3219 assert_eq!(params.new_name, "THREE");
3220 Ok(Some(lsp::WorkspaceEdit {
3221 changes: Some(
3222 [
3223 (
3224 lsp::Url::from_file_path("/dir/one.rs").unwrap(),
3225 vec![lsp::TextEdit::new(
3226 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
3227 "THREE".to_string(),
3228 )],
3229 ),
3230 (
3231 lsp::Url::from_file_path("/dir/two.rs").unwrap(),
3232 vec![
3233 lsp::TextEdit::new(
3234 lsp::Range::new(
3235 lsp::Position::new(0, 24),
3236 lsp::Position::new(0, 27),
3237 ),
3238 "THREE".to_string(),
3239 ),
3240 lsp::TextEdit::new(
3241 lsp::Range::new(
3242 lsp::Position::new(0, 35),
3243 lsp::Position::new(0, 38),
3244 ),
3245 "THREE".to_string(),
3246 ),
3247 ],
3248 ),
3249 ]
3250 .into_iter()
3251 .collect(),
3252 ),
3253 ..Default::default()
3254 }))
3255 })
3256 .next()
3257 .await
3258 .unwrap();
3259 let mut transaction = response.await.unwrap().0;
3260 assert_eq!(transaction.len(), 2);
3261 assert_eq!(
3262 transaction
3263 .remove_entry(&buffer)
3264 .unwrap()
3265 .0
3266 .read_with(cx, |buffer, _| buffer.text()),
3267 "const THREE: usize = 1;"
3268 );
3269 assert_eq!(
3270 transaction
3271 .into_keys()
3272 .next()
3273 .unwrap()
3274 .read_with(cx, |buffer, _| buffer.text()),
3275 "const TWO: usize = one::THREE + one::THREE;"
3276 );
3277}
3278
3279#[gpui::test]
3280async fn test_search(cx: &mut gpui::TestAppContext) {
3281 let fs = FakeFs::new(cx.background());
3282 fs.insert_tree(
3283 "/dir",
3284 json!({
3285 "one.rs": "const ONE: usize = 1;",
3286 "two.rs": "const TWO: usize = one::ONE + one::ONE;",
3287 "three.rs": "const THREE: usize = one::ONE + two::TWO;",
3288 "four.rs": "const FOUR: usize = one::ONE + three::THREE;",
3289 }),
3290 )
3291 .await;
3292 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
3293 assert_eq!(
3294 search(&project, SearchQuery::text("TWO", false, true), cx)
3295 .await
3296 .unwrap(),
3297 HashMap::from_iter([
3298 ("two.rs".to_string(), vec![6..9]),
3299 ("three.rs".to_string(), vec![37..40])
3300 ])
3301 );
3302
3303 let buffer_4 = project
3304 .update(cx, |project, cx| {
3305 project.open_local_buffer("/dir/four.rs", cx)
3306 })
3307 .await
3308 .unwrap();
3309 buffer_4.update(cx, |buffer, cx| {
3310 let text = "two::TWO";
3311 buffer.edit([(20..28, text), (31..43, text)], None, cx);
3312 });
3313
3314 assert_eq!(
3315 search(&project, SearchQuery::text("TWO", false, true), cx)
3316 .await
3317 .unwrap(),
3318 HashMap::from_iter([
3319 ("two.rs".to_string(), vec![6..9]),
3320 ("three.rs".to_string(), vec![37..40]),
3321 ("four.rs".to_string(), vec![25..28, 36..39])
3322 ])
3323 );
3324
3325 async fn search(
3326 project: &ModelHandle<Project>,
3327 query: SearchQuery,
3328 cx: &mut gpui::TestAppContext,
3329 ) -> Result<HashMap<String, Vec<Range<usize>>>> {
3330 let results = project
3331 .update(cx, |project, cx| project.search(query, cx))
3332 .await?;
3333
3334 Ok(results
3335 .into_iter()
3336 .map(|(buffer, ranges)| {
3337 buffer.read_with(cx, |buffer, _| {
3338 let path = buffer.file().unwrap().path().to_string_lossy().to_string();
3339 let ranges = ranges
3340 .into_iter()
3341 .map(|range| range.to_offset(buffer))
3342 .collect::<Vec<_>>();
3343 (path, ranges)
3344 })
3345 })
3346 .collect())
3347 }
3348}