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