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