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