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