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.read_with(cx, |buffer, _| {
1277 assert_eq!(
1278 buffer
1279 .snapshot()
1280 .diagnostics_in_range::<_, Point>(Point::new(3, 0)..Point::new(5, 0), false)
1281 .collect::<Vec<_>>(),
1282 &[
1283 DiagnosticEntry {
1284 range: Point::new(3, 9)..Point::new(3, 11),
1285 diagnostic: Diagnostic {
1286 source: Some("disk".into()),
1287 severity: DiagnosticSeverity::ERROR,
1288 message: "undefined variable 'BB'".to_string(),
1289 is_disk_based: true,
1290 group_id: 1,
1291 is_primary: true,
1292 ..Default::default()
1293 },
1294 },
1295 DiagnosticEntry {
1296 range: Point::new(4, 9)..Point::new(4, 12),
1297 diagnostic: Diagnostic {
1298 source: Some("disk".into()),
1299 severity: DiagnosticSeverity::ERROR,
1300 message: "undefined variable 'CCC'".to_string(),
1301 is_disk_based: true,
1302 group_id: 2,
1303 is_primary: true,
1304 ..Default::default()
1305 }
1306 }
1307 ]
1308 );
1309 assert_eq!(
1310 chunks_with_diagnostics(buffer, 0..buffer.len()),
1311 [
1312 ("\n\nfn a() { ".to_string(), None),
1313 ("A".to_string(), Some(DiagnosticSeverity::ERROR)),
1314 (" }\nfn b() { ".to_string(), None),
1315 ("BB".to_string(), Some(DiagnosticSeverity::ERROR)),
1316 (" }\nfn c() { ".to_string(), None),
1317 ("CCC".to_string(), Some(DiagnosticSeverity::ERROR)),
1318 (" }\n".to_string(), None),
1319 ]
1320 );
1321 assert_eq!(
1322 chunks_with_diagnostics(buffer, Point::new(3, 10)..Point::new(4, 11)),
1323 [
1324 ("B".to_string(), Some(DiagnosticSeverity::ERROR)),
1325 (" }\nfn c() { ".to_string(), None),
1326 ("CC".to_string(), Some(DiagnosticSeverity::ERROR)),
1327 ]
1328 );
1329 });
1330
1331 // Ensure overlapping diagnostics are highlighted correctly.
1332 fake_server.notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
1333 uri: lsp::Url::from_file_path("/dir/a.rs").unwrap(),
1334 version: Some(open_notification.text_document.version),
1335 diagnostics: vec![
1336 lsp::Diagnostic {
1337 range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
1338 severity: Some(DiagnosticSeverity::ERROR),
1339 message: "undefined variable 'A'".to_string(),
1340 source: Some("disk".to_string()),
1341 ..Default::default()
1342 },
1343 lsp::Diagnostic {
1344 range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 12)),
1345 severity: Some(DiagnosticSeverity::WARNING),
1346 message: "unreachable statement".to_string(),
1347 source: Some("disk".to_string()),
1348 ..Default::default()
1349 },
1350 ],
1351 });
1352
1353 buffer.next_notification(cx).await;
1354 buffer.read_with(cx, |buffer, _| {
1355 assert_eq!(
1356 buffer
1357 .snapshot()
1358 .diagnostics_in_range::<_, Point>(Point::new(2, 0)..Point::new(3, 0), false)
1359 .collect::<Vec<_>>(),
1360 &[
1361 DiagnosticEntry {
1362 range: Point::new(2, 9)..Point::new(2, 12),
1363 diagnostic: Diagnostic {
1364 source: Some("disk".into()),
1365 severity: DiagnosticSeverity::WARNING,
1366 message: "unreachable statement".to_string(),
1367 is_disk_based: true,
1368 group_id: 4,
1369 is_primary: true,
1370 ..Default::default()
1371 }
1372 },
1373 DiagnosticEntry {
1374 range: Point::new(2, 9)..Point::new(2, 10),
1375 diagnostic: Diagnostic {
1376 source: Some("disk".into()),
1377 severity: DiagnosticSeverity::ERROR,
1378 message: "undefined variable 'A'".to_string(),
1379 is_disk_based: true,
1380 group_id: 3,
1381 is_primary: true,
1382 ..Default::default()
1383 },
1384 }
1385 ]
1386 );
1387 assert_eq!(
1388 chunks_with_diagnostics(buffer, Point::new(2, 0)..Point::new(3, 0)),
1389 [
1390 ("fn a() { ".to_string(), None),
1391 ("A".to_string(), Some(DiagnosticSeverity::ERROR)),
1392 (" }".to_string(), Some(DiagnosticSeverity::WARNING)),
1393 ("\n".to_string(), None),
1394 ]
1395 );
1396 assert_eq!(
1397 chunks_with_diagnostics(buffer, Point::new(2, 10)..Point::new(3, 0)),
1398 [
1399 (" }".to_string(), Some(DiagnosticSeverity::WARNING)),
1400 ("\n".to_string(), None),
1401 ]
1402 );
1403 });
1404
1405 // Keep editing the buffer and ensure disk-based diagnostics get translated according to the
1406 // changes since the last save.
1407 buffer.update(cx, |buffer, cx| {
1408 buffer.edit([(Point::new(2, 0)..Point::new(2, 0), " ")], None, cx);
1409 buffer.edit(
1410 [(Point::new(2, 8)..Point::new(2, 10), "(x: usize)")],
1411 None,
1412 cx,
1413 );
1414 buffer.edit([(Point::new(3, 10)..Point::new(3, 10), "xxx")], None, cx);
1415 });
1416 let change_notification_2 = fake_server
1417 .receive_notification::<lsp::notification::DidChangeTextDocument>()
1418 .await;
1419 assert!(
1420 change_notification_2.text_document.version > change_notification_1.text_document.version
1421 );
1422
1423 // Handle out-of-order diagnostics
1424 fake_server.notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
1425 uri: lsp::Url::from_file_path("/dir/a.rs").unwrap(),
1426 version: Some(change_notification_2.text_document.version),
1427 diagnostics: vec![
1428 lsp::Diagnostic {
1429 range: lsp::Range::new(lsp::Position::new(1, 9), lsp::Position::new(1, 11)),
1430 severity: Some(DiagnosticSeverity::ERROR),
1431 message: "undefined variable 'BB'".to_string(),
1432 source: Some("disk".to_string()),
1433 ..Default::default()
1434 },
1435 lsp::Diagnostic {
1436 range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
1437 severity: Some(DiagnosticSeverity::WARNING),
1438 message: "undefined variable 'A'".to_string(),
1439 source: Some("disk".to_string()),
1440 ..Default::default()
1441 },
1442 ],
1443 });
1444
1445 buffer.next_notification(cx).await;
1446 buffer.read_with(cx, |buffer, _| {
1447 assert_eq!(
1448 buffer
1449 .snapshot()
1450 .diagnostics_in_range::<_, Point>(0..buffer.len(), false)
1451 .collect::<Vec<_>>(),
1452 &[
1453 DiagnosticEntry {
1454 range: Point::new(2, 21)..Point::new(2, 22),
1455 diagnostic: Diagnostic {
1456 source: Some("disk".into()),
1457 severity: DiagnosticSeverity::WARNING,
1458 message: "undefined variable 'A'".to_string(),
1459 is_disk_based: true,
1460 group_id: 6,
1461 is_primary: true,
1462 ..Default::default()
1463 }
1464 },
1465 DiagnosticEntry {
1466 range: Point::new(3, 9)..Point::new(3, 14),
1467 diagnostic: Diagnostic {
1468 source: Some("disk".into()),
1469 severity: DiagnosticSeverity::ERROR,
1470 message: "undefined variable 'BB'".to_string(),
1471 is_disk_based: true,
1472 group_id: 5,
1473 is_primary: true,
1474 ..Default::default()
1475 },
1476 }
1477 ]
1478 );
1479 });
1480}
1481
1482#[gpui::test]
1483async fn test_empty_diagnostic_ranges(cx: &mut gpui::TestAppContext) {
1484 init_test(cx);
1485
1486 let text = concat!(
1487 "let one = ;\n", //
1488 "let two = \n",
1489 "let three = 3;\n",
1490 );
1491
1492 let fs = FakeFs::new(cx.background());
1493 fs.insert_tree("/dir", json!({ "a.rs": text })).await;
1494
1495 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
1496 let buffer = project
1497 .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
1498 .await
1499 .unwrap();
1500
1501 project.update(cx, |project, cx| {
1502 project
1503 .update_buffer_diagnostics(
1504 &buffer,
1505 LanguageServerId(0),
1506 None,
1507 vec![
1508 DiagnosticEntry {
1509 range: Unclipped(PointUtf16::new(0, 10))..Unclipped(PointUtf16::new(0, 10)),
1510 diagnostic: Diagnostic {
1511 severity: DiagnosticSeverity::ERROR,
1512 message: "syntax error 1".to_string(),
1513 ..Default::default()
1514 },
1515 },
1516 DiagnosticEntry {
1517 range: Unclipped(PointUtf16::new(1, 10))..Unclipped(PointUtf16::new(1, 10)),
1518 diagnostic: Diagnostic {
1519 severity: DiagnosticSeverity::ERROR,
1520 message: "syntax error 2".to_string(),
1521 ..Default::default()
1522 },
1523 },
1524 ],
1525 cx,
1526 )
1527 .unwrap();
1528 });
1529
1530 // An empty range is extended forward to include the following character.
1531 // At the end of a line, an empty range is extended backward to include
1532 // the preceding character.
1533 buffer.read_with(cx, |buffer, _| {
1534 let chunks = chunks_with_diagnostics(buffer, 0..buffer.len());
1535 assert_eq!(
1536 chunks
1537 .iter()
1538 .map(|(s, d)| (s.as_str(), *d))
1539 .collect::<Vec<_>>(),
1540 &[
1541 ("let one = ", None),
1542 (";", Some(DiagnosticSeverity::ERROR)),
1543 ("\nlet two =", None),
1544 (" ", Some(DiagnosticSeverity::ERROR)),
1545 ("\nlet three = 3;\n", None)
1546 ]
1547 );
1548 });
1549}
1550
1551#[gpui::test]
1552async fn test_diagnostics_from_multiple_language_servers(cx: &mut gpui::TestAppContext) {
1553 init_test(cx);
1554
1555 let fs = FakeFs::new(cx.background());
1556 fs.insert_tree("/dir", json!({ "a.rs": "one two three" }))
1557 .await;
1558
1559 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
1560
1561 project.update(cx, |project, cx| {
1562 project
1563 .update_diagnostic_entries(
1564 LanguageServerId(0),
1565 Path::new("/dir/a.rs").to_owned(),
1566 None,
1567 vec![DiagnosticEntry {
1568 range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 3)),
1569 diagnostic: Diagnostic {
1570 severity: DiagnosticSeverity::ERROR,
1571 is_primary: true,
1572 message: "syntax error a1".to_string(),
1573 ..Default::default()
1574 },
1575 }],
1576 cx,
1577 )
1578 .unwrap();
1579 project
1580 .update_diagnostic_entries(
1581 LanguageServerId(1),
1582 Path::new("/dir/a.rs").to_owned(),
1583 None,
1584 vec![DiagnosticEntry {
1585 range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 3)),
1586 diagnostic: Diagnostic {
1587 severity: DiagnosticSeverity::ERROR,
1588 is_primary: true,
1589 message: "syntax error b1".to_string(),
1590 ..Default::default()
1591 },
1592 }],
1593 cx,
1594 )
1595 .unwrap();
1596
1597 assert_eq!(
1598 project.diagnostic_summary(cx),
1599 DiagnosticSummary {
1600 error_count: 2,
1601 warning_count: 0,
1602 }
1603 );
1604 });
1605}
1606
1607#[gpui::test]
1608async fn test_edits_from_lsp_with_past_version(cx: &mut gpui::TestAppContext) {
1609 init_test(cx);
1610
1611 let mut language = Language::new(
1612 LanguageConfig {
1613 name: "Rust".into(),
1614 path_suffixes: vec!["rs".to_string()],
1615 ..Default::default()
1616 },
1617 Some(tree_sitter_rust::language()),
1618 );
1619 let mut fake_servers = language.set_fake_lsp_adapter(Default::default()).await;
1620
1621 let text = "
1622 fn a() {
1623 f1();
1624 }
1625 fn b() {
1626 f2();
1627 }
1628 fn c() {
1629 f3();
1630 }
1631 "
1632 .unindent();
1633
1634 let fs = FakeFs::new(cx.background());
1635 fs.insert_tree(
1636 "/dir",
1637 json!({
1638 "a.rs": text.clone(),
1639 }),
1640 )
1641 .await;
1642
1643 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
1644 project.update(cx, |project, _| project.languages.add(Arc::new(language)));
1645 let buffer = project
1646 .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
1647 .await
1648 .unwrap();
1649
1650 let mut fake_server = fake_servers.next().await.unwrap();
1651 let lsp_document_version = fake_server
1652 .receive_notification::<lsp::notification::DidOpenTextDocument>()
1653 .await
1654 .text_document
1655 .version;
1656
1657 // Simulate editing the buffer after the language server computes some edits.
1658 buffer.update(cx, |buffer, cx| {
1659 buffer.edit(
1660 [(
1661 Point::new(0, 0)..Point::new(0, 0),
1662 "// above first function\n",
1663 )],
1664 None,
1665 cx,
1666 );
1667 buffer.edit(
1668 [(
1669 Point::new(2, 0)..Point::new(2, 0),
1670 " // inside first function\n",
1671 )],
1672 None,
1673 cx,
1674 );
1675 buffer.edit(
1676 [(
1677 Point::new(6, 4)..Point::new(6, 4),
1678 "// inside second function ",
1679 )],
1680 None,
1681 cx,
1682 );
1683
1684 assert_eq!(
1685 buffer.text(),
1686 "
1687 // above first function
1688 fn a() {
1689 // inside first function
1690 f1();
1691 }
1692 fn b() {
1693 // inside second function f2();
1694 }
1695 fn c() {
1696 f3();
1697 }
1698 "
1699 .unindent()
1700 );
1701 });
1702
1703 let edits = project
1704 .update(cx, |project, cx| {
1705 project.edits_from_lsp(
1706 &buffer,
1707 vec![
1708 // replace body of first function
1709 lsp::TextEdit {
1710 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(3, 0)),
1711 new_text: "
1712 fn a() {
1713 f10();
1714 }
1715 "
1716 .unindent(),
1717 },
1718 // edit inside second function
1719 lsp::TextEdit {
1720 range: lsp::Range::new(lsp::Position::new(4, 6), lsp::Position::new(4, 6)),
1721 new_text: "00".into(),
1722 },
1723 // edit inside third function via two distinct edits
1724 lsp::TextEdit {
1725 range: lsp::Range::new(lsp::Position::new(7, 5), lsp::Position::new(7, 5)),
1726 new_text: "4000".into(),
1727 },
1728 lsp::TextEdit {
1729 range: lsp::Range::new(lsp::Position::new(7, 5), lsp::Position::new(7, 6)),
1730 new_text: "".into(),
1731 },
1732 ],
1733 LanguageServerId(0),
1734 Some(lsp_document_version),
1735 cx,
1736 )
1737 })
1738 .await
1739 .unwrap();
1740
1741 buffer.update(cx, |buffer, cx| {
1742 for (range, new_text) in edits {
1743 buffer.edit([(range, new_text)], None, cx);
1744 }
1745 assert_eq!(
1746 buffer.text(),
1747 "
1748 // above first function
1749 fn a() {
1750 // inside first function
1751 f10();
1752 }
1753 fn b() {
1754 // inside second function f200();
1755 }
1756 fn c() {
1757 f4000();
1758 }
1759 "
1760 .unindent()
1761 );
1762 });
1763}
1764
1765#[gpui::test]
1766async fn test_edits_from_lsp_with_edits_on_adjacent_lines(cx: &mut gpui::TestAppContext) {
1767 init_test(cx);
1768
1769 let text = "
1770 use a::b;
1771 use a::c;
1772
1773 fn f() {
1774 b();
1775 c();
1776 }
1777 "
1778 .unindent();
1779
1780 let fs = FakeFs::new(cx.background());
1781 fs.insert_tree(
1782 "/dir",
1783 json!({
1784 "a.rs": text.clone(),
1785 }),
1786 )
1787 .await;
1788
1789 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
1790 let buffer = project
1791 .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
1792 .await
1793 .unwrap();
1794
1795 // Simulate the language server sending us a small edit in the form of a very large diff.
1796 // Rust-analyzer does this when performing a merge-imports code action.
1797 let edits = project
1798 .update(cx, |project, cx| {
1799 project.edits_from_lsp(
1800 &buffer,
1801 [
1802 // Replace the first use statement without editing the semicolon.
1803 lsp::TextEdit {
1804 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 8)),
1805 new_text: "a::{b, c}".into(),
1806 },
1807 // Reinsert the remainder of the file between the semicolon and the final
1808 // newline of the file.
1809 lsp::TextEdit {
1810 range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 9)),
1811 new_text: "\n\n".into(),
1812 },
1813 lsp::TextEdit {
1814 range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 9)),
1815 new_text: "
1816 fn f() {
1817 b();
1818 c();
1819 }"
1820 .unindent(),
1821 },
1822 // Delete everything after the first newline of the file.
1823 lsp::TextEdit {
1824 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(7, 0)),
1825 new_text: "".into(),
1826 },
1827 ],
1828 LanguageServerId(0),
1829 None,
1830 cx,
1831 )
1832 })
1833 .await
1834 .unwrap();
1835
1836 buffer.update(cx, |buffer, cx| {
1837 let edits = edits
1838 .into_iter()
1839 .map(|(range, text)| {
1840 (
1841 range.start.to_point(buffer)..range.end.to_point(buffer),
1842 text,
1843 )
1844 })
1845 .collect::<Vec<_>>();
1846
1847 assert_eq!(
1848 edits,
1849 [
1850 (Point::new(0, 4)..Point::new(0, 8), "a::{b, c}".into()),
1851 (Point::new(1, 0)..Point::new(2, 0), "".into())
1852 ]
1853 );
1854
1855 for (range, new_text) in edits {
1856 buffer.edit([(range, new_text)], None, cx);
1857 }
1858 assert_eq!(
1859 buffer.text(),
1860 "
1861 use a::{b, c};
1862
1863 fn f() {
1864 b();
1865 c();
1866 }
1867 "
1868 .unindent()
1869 );
1870 });
1871}
1872
1873#[gpui::test]
1874async fn test_invalid_edits_from_lsp(cx: &mut gpui::TestAppContext) {
1875 init_test(cx);
1876
1877 let text = "
1878 use a::b;
1879 use a::c;
1880
1881 fn f() {
1882 b();
1883 c();
1884 }
1885 "
1886 .unindent();
1887
1888 let fs = FakeFs::new(cx.background());
1889 fs.insert_tree(
1890 "/dir",
1891 json!({
1892 "a.rs": text.clone(),
1893 }),
1894 )
1895 .await;
1896
1897 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
1898 let buffer = project
1899 .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
1900 .await
1901 .unwrap();
1902
1903 // Simulate the language server sending us edits in a non-ordered fashion,
1904 // with ranges sometimes being inverted or pointing to invalid locations.
1905 let edits = project
1906 .update(cx, |project, cx| {
1907 project.edits_from_lsp(
1908 &buffer,
1909 [
1910 lsp::TextEdit {
1911 range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 9)),
1912 new_text: "\n\n".into(),
1913 },
1914 lsp::TextEdit {
1915 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 4)),
1916 new_text: "a::{b, c}".into(),
1917 },
1918 lsp::TextEdit {
1919 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(99, 0)),
1920 new_text: "".into(),
1921 },
1922 lsp::TextEdit {
1923 range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 9)),
1924 new_text: "
1925 fn f() {
1926 b();
1927 c();
1928 }"
1929 .unindent(),
1930 },
1931 ],
1932 LanguageServerId(0),
1933 None,
1934 cx,
1935 )
1936 })
1937 .await
1938 .unwrap();
1939
1940 buffer.update(cx, |buffer, cx| {
1941 let edits = edits
1942 .into_iter()
1943 .map(|(range, text)| {
1944 (
1945 range.start.to_point(buffer)..range.end.to_point(buffer),
1946 text,
1947 )
1948 })
1949 .collect::<Vec<_>>();
1950
1951 assert_eq!(
1952 edits,
1953 [
1954 (Point::new(0, 4)..Point::new(0, 8), "a::{b, c}".into()),
1955 (Point::new(1, 0)..Point::new(2, 0), "".into())
1956 ]
1957 );
1958
1959 for (range, new_text) in edits {
1960 buffer.edit([(range, new_text)], None, cx);
1961 }
1962 assert_eq!(
1963 buffer.text(),
1964 "
1965 use a::{b, c};
1966
1967 fn f() {
1968 b();
1969 c();
1970 }
1971 "
1972 .unindent()
1973 );
1974 });
1975}
1976
1977fn chunks_with_diagnostics<T: ToOffset + ToPoint>(
1978 buffer: &Buffer,
1979 range: Range<T>,
1980) -> Vec<(String, Option<DiagnosticSeverity>)> {
1981 let mut chunks: Vec<(String, Option<DiagnosticSeverity>)> = Vec::new();
1982 for chunk in buffer.snapshot().chunks(range, true) {
1983 if chunks.last().map_or(false, |prev_chunk| {
1984 prev_chunk.1 == chunk.diagnostic_severity
1985 }) {
1986 chunks.last_mut().unwrap().0.push_str(chunk.text);
1987 } else {
1988 chunks.push((chunk.text.to_string(), chunk.diagnostic_severity));
1989 }
1990 }
1991 chunks
1992}
1993
1994#[gpui::test(iterations = 10)]
1995async fn test_definition(cx: &mut gpui::TestAppContext) {
1996 init_test(cx);
1997
1998 let mut language = Language::new(
1999 LanguageConfig {
2000 name: "Rust".into(),
2001 path_suffixes: vec!["rs".to_string()],
2002 ..Default::default()
2003 },
2004 Some(tree_sitter_rust::language()),
2005 );
2006 let mut fake_servers = language.set_fake_lsp_adapter(Default::default()).await;
2007
2008 let fs = FakeFs::new(cx.background());
2009 fs.insert_tree(
2010 "/dir",
2011 json!({
2012 "a.rs": "const fn a() { A }",
2013 "b.rs": "const y: i32 = crate::a()",
2014 }),
2015 )
2016 .await;
2017
2018 let project = Project::test(fs, ["/dir/b.rs".as_ref()], cx).await;
2019 project.update(cx, |project, _| project.languages.add(Arc::new(language)));
2020
2021 let buffer = project
2022 .update(cx, |project, cx| project.open_local_buffer("/dir/b.rs", cx))
2023 .await
2024 .unwrap();
2025
2026 let fake_server = fake_servers.next().await.unwrap();
2027 fake_server.handle_request::<lsp::request::GotoDefinition, _, _>(|params, _| async move {
2028 let params = params.text_document_position_params;
2029 assert_eq!(
2030 params.text_document.uri.to_file_path().unwrap(),
2031 Path::new("/dir/b.rs"),
2032 );
2033 assert_eq!(params.position, lsp::Position::new(0, 22));
2034
2035 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
2036 lsp::Location::new(
2037 lsp::Url::from_file_path("/dir/a.rs").unwrap(),
2038 lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
2039 ),
2040 )))
2041 });
2042
2043 let mut definitions = project
2044 .update(cx, |project, cx| project.definition(&buffer, 22, cx))
2045 .await
2046 .unwrap();
2047
2048 // Assert no new language server started
2049 cx.foreground().run_until_parked();
2050 assert!(fake_servers.try_next().is_err());
2051
2052 assert_eq!(definitions.len(), 1);
2053 let definition = definitions.pop().unwrap();
2054 cx.update(|cx| {
2055 let target_buffer = definition.target.buffer.read(cx);
2056 assert_eq!(
2057 target_buffer
2058 .file()
2059 .unwrap()
2060 .as_local()
2061 .unwrap()
2062 .abs_path(cx),
2063 Path::new("/dir/a.rs"),
2064 );
2065 assert_eq!(definition.target.range.to_offset(target_buffer), 9..10);
2066 assert_eq!(
2067 list_worktrees(&project, cx),
2068 [("/dir/b.rs".as_ref(), true), ("/dir/a.rs".as_ref(), false)]
2069 );
2070
2071 drop(definition);
2072 });
2073 cx.read(|cx| {
2074 assert_eq!(list_worktrees(&project, cx), [("/dir/b.rs".as_ref(), true)]);
2075 });
2076
2077 fn list_worktrees<'a>(
2078 project: &'a ModelHandle<Project>,
2079 cx: &'a AppContext,
2080 ) -> Vec<(&'a Path, bool)> {
2081 project
2082 .read(cx)
2083 .worktrees(cx)
2084 .map(|worktree| {
2085 let worktree = worktree.read(cx);
2086 (
2087 worktree.as_local().unwrap().abs_path().as_ref(),
2088 worktree.is_visible(),
2089 )
2090 })
2091 .collect::<Vec<_>>()
2092 }
2093}
2094
2095#[gpui::test]
2096async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) {
2097 init_test(cx);
2098
2099 let mut language = Language::new(
2100 LanguageConfig {
2101 name: "TypeScript".into(),
2102 path_suffixes: vec!["ts".to_string()],
2103 ..Default::default()
2104 },
2105 Some(tree_sitter_typescript::language_typescript()),
2106 );
2107 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
2108
2109 let fs = FakeFs::new(cx.background());
2110 fs.insert_tree(
2111 "/dir",
2112 json!({
2113 "a.ts": "",
2114 }),
2115 )
2116 .await;
2117
2118 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
2119 project.update(cx, |project, _| project.languages.add(Arc::new(language)));
2120 let buffer = project
2121 .update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
2122 .await
2123 .unwrap();
2124
2125 let fake_server = fake_language_servers.next().await.unwrap();
2126
2127 let text = "let a = b.fqn";
2128 buffer.update(cx, |buffer, cx| buffer.set_text(text, cx));
2129 let completions = project.update(cx, |project, cx| {
2130 project.completions(&buffer, text.len(), cx)
2131 });
2132
2133 fake_server
2134 .handle_request::<lsp::request::Completion, _, _>(|_, _| async move {
2135 Ok(Some(lsp::CompletionResponse::Array(vec![
2136 lsp::CompletionItem {
2137 label: "fullyQualifiedName?".into(),
2138 insert_text: Some("fullyQualifiedName".into()),
2139 ..Default::default()
2140 },
2141 ])))
2142 })
2143 .next()
2144 .await;
2145 let completions = completions.await.unwrap();
2146 let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
2147 assert_eq!(completions.len(), 1);
2148 assert_eq!(completions[0].new_text, "fullyQualifiedName");
2149 assert_eq!(
2150 completions[0].old_range.to_offset(&snapshot),
2151 text.len() - 3..text.len()
2152 );
2153
2154 let text = "let a = \"atoms/cmp\"";
2155 buffer.update(cx, |buffer, cx| buffer.set_text(text, cx));
2156 let completions = project.update(cx, |project, cx| {
2157 project.completions(&buffer, text.len() - 1, cx)
2158 });
2159
2160 fake_server
2161 .handle_request::<lsp::request::Completion, _, _>(|_, _| async move {
2162 Ok(Some(lsp::CompletionResponse::Array(vec![
2163 lsp::CompletionItem {
2164 label: "component".into(),
2165 ..Default::default()
2166 },
2167 ])))
2168 })
2169 .next()
2170 .await;
2171 let completions = completions.await.unwrap();
2172 let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
2173 assert_eq!(completions.len(), 1);
2174 assert_eq!(completions[0].new_text, "component");
2175 assert_eq!(
2176 completions[0].old_range.to_offset(&snapshot),
2177 text.len() - 4..text.len() - 1
2178 );
2179}
2180
2181#[gpui::test]
2182async fn test_completions_with_carriage_returns(cx: &mut gpui::TestAppContext) {
2183 init_test(cx);
2184
2185 let mut language = Language::new(
2186 LanguageConfig {
2187 name: "TypeScript".into(),
2188 path_suffixes: vec!["ts".to_string()],
2189 ..Default::default()
2190 },
2191 Some(tree_sitter_typescript::language_typescript()),
2192 );
2193 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
2194
2195 let fs = FakeFs::new(cx.background());
2196 fs.insert_tree(
2197 "/dir",
2198 json!({
2199 "a.ts": "",
2200 }),
2201 )
2202 .await;
2203
2204 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
2205 project.update(cx, |project, _| project.languages.add(Arc::new(language)));
2206 let buffer = project
2207 .update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
2208 .await
2209 .unwrap();
2210
2211 let fake_server = fake_language_servers.next().await.unwrap();
2212
2213 let text = "let a = b.fqn";
2214 buffer.update(cx, |buffer, cx| buffer.set_text(text, cx));
2215 let completions = project.update(cx, |project, cx| {
2216 project.completions(&buffer, text.len(), cx)
2217 });
2218
2219 fake_server
2220 .handle_request::<lsp::request::Completion, _, _>(|_, _| async move {
2221 Ok(Some(lsp::CompletionResponse::Array(vec![
2222 lsp::CompletionItem {
2223 label: "fullyQualifiedName?".into(),
2224 insert_text: Some("fully\rQualified\r\nName".into()),
2225 ..Default::default()
2226 },
2227 ])))
2228 })
2229 .next()
2230 .await;
2231 let completions = completions.await.unwrap();
2232 assert_eq!(completions.len(), 1);
2233 assert_eq!(completions[0].new_text, "fully\nQualified\nName");
2234}
2235
2236#[gpui::test(iterations = 10)]
2237async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) {
2238 init_test(cx);
2239
2240 let mut language = Language::new(
2241 LanguageConfig {
2242 name: "TypeScript".into(),
2243 path_suffixes: vec!["ts".to_string()],
2244 ..Default::default()
2245 },
2246 None,
2247 );
2248 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
2249
2250 let fs = FakeFs::new(cx.background());
2251 fs.insert_tree(
2252 "/dir",
2253 json!({
2254 "a.ts": "a",
2255 }),
2256 )
2257 .await;
2258
2259 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
2260 project.update(cx, |project, _| project.languages.add(Arc::new(language)));
2261 let buffer = project
2262 .update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
2263 .await
2264 .unwrap();
2265
2266 let fake_server = fake_language_servers.next().await.unwrap();
2267
2268 // Language server returns code actions that contain commands, and not edits.
2269 let actions = project.update(cx, |project, cx| project.code_actions(&buffer, 0..0, cx));
2270 fake_server
2271 .handle_request::<lsp::request::CodeActionRequest, _, _>(|_, _| async move {
2272 Ok(Some(vec![
2273 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
2274 title: "The code action".into(),
2275 command: Some(lsp::Command {
2276 title: "The command".into(),
2277 command: "_the/command".into(),
2278 arguments: Some(vec![json!("the-argument")]),
2279 }),
2280 ..Default::default()
2281 }),
2282 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
2283 title: "two".into(),
2284 ..Default::default()
2285 }),
2286 ]))
2287 })
2288 .next()
2289 .await;
2290
2291 let action = actions.await.unwrap()[0].clone();
2292 let apply = project.update(cx, |project, cx| {
2293 project.apply_code_action(buffer.clone(), action, true, cx)
2294 });
2295
2296 // Resolving the code action does not populate its edits. In absence of
2297 // edits, we must execute the given command.
2298 fake_server.handle_request::<lsp::request::CodeActionResolveRequest, _, _>(
2299 |action, _| async move { Ok(action) },
2300 );
2301
2302 // While executing the command, the language server sends the editor
2303 // a `workspaceEdit` request.
2304 fake_server
2305 .handle_request::<lsp::request::ExecuteCommand, _, _>({
2306 let fake = fake_server.clone();
2307 move |params, _| {
2308 assert_eq!(params.command, "_the/command");
2309 let fake = fake.clone();
2310 async move {
2311 fake.server
2312 .request::<lsp::request::ApplyWorkspaceEdit>(
2313 lsp::ApplyWorkspaceEditParams {
2314 label: None,
2315 edit: lsp::WorkspaceEdit {
2316 changes: Some(
2317 [(
2318 lsp::Url::from_file_path("/dir/a.ts").unwrap(),
2319 vec![lsp::TextEdit {
2320 range: lsp::Range::new(
2321 lsp::Position::new(0, 0),
2322 lsp::Position::new(0, 0),
2323 ),
2324 new_text: "X".into(),
2325 }],
2326 )]
2327 .into_iter()
2328 .collect(),
2329 ),
2330 ..Default::default()
2331 },
2332 },
2333 )
2334 .await
2335 .unwrap();
2336 Ok(Some(json!(null)))
2337 }
2338 }
2339 })
2340 .next()
2341 .await;
2342
2343 // Applying the code action returns a project transaction containing the edits
2344 // sent by the language server in its `workspaceEdit` request.
2345 let transaction = apply.await.unwrap();
2346 assert!(transaction.0.contains_key(&buffer));
2347 buffer.update(cx, |buffer, cx| {
2348 assert_eq!(buffer.text(), "Xa");
2349 buffer.undo(cx);
2350 assert_eq!(buffer.text(), "a");
2351 });
2352}
2353
2354#[gpui::test(iterations = 10)]
2355async fn test_save_file(cx: &mut gpui::TestAppContext) {
2356 init_test(cx);
2357
2358 let fs = FakeFs::new(cx.background());
2359 fs.insert_tree(
2360 "/dir",
2361 json!({
2362 "file1": "the old contents",
2363 }),
2364 )
2365 .await;
2366
2367 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
2368 let buffer = project
2369 .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
2370 .await
2371 .unwrap();
2372 buffer.update(cx, |buffer, cx| {
2373 assert_eq!(buffer.text(), "the old contents");
2374 buffer.edit([(0..0, "a line of text.\n".repeat(10 * 1024))], None, cx);
2375 });
2376
2377 project
2378 .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
2379 .await
2380 .unwrap();
2381
2382 let new_text = fs.load(Path::new("/dir/file1")).await.unwrap();
2383 assert_eq!(new_text, buffer.read_with(cx, |buffer, _| buffer.text()));
2384}
2385
2386#[gpui::test]
2387async fn test_save_in_single_file_worktree(cx: &mut gpui::TestAppContext) {
2388 init_test(cx);
2389
2390 let fs = FakeFs::new(cx.background());
2391 fs.insert_tree(
2392 "/dir",
2393 json!({
2394 "file1": "the old contents",
2395 }),
2396 )
2397 .await;
2398
2399 let project = Project::test(fs.clone(), ["/dir/file1".as_ref()], cx).await;
2400 let buffer = project
2401 .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
2402 .await
2403 .unwrap();
2404 buffer.update(cx, |buffer, cx| {
2405 buffer.edit([(0..0, "a line of text.\n".repeat(10 * 1024))], None, cx);
2406 });
2407
2408 project
2409 .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
2410 .await
2411 .unwrap();
2412
2413 let new_text = fs.load(Path::new("/dir/file1")).await.unwrap();
2414 assert_eq!(new_text, buffer.read_with(cx, |buffer, _| buffer.text()));
2415}
2416
2417#[gpui::test]
2418async fn test_save_as(cx: &mut gpui::TestAppContext) {
2419 init_test(cx);
2420
2421 let fs = FakeFs::new(cx.background());
2422 fs.insert_tree("/dir", json!({})).await;
2423
2424 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
2425
2426 let languages = project.read_with(cx, |project, _| project.languages().clone());
2427 languages.register(
2428 "/some/path",
2429 LanguageConfig {
2430 name: "Rust".into(),
2431 path_suffixes: vec!["rs".into()],
2432 ..Default::default()
2433 },
2434 tree_sitter_rust::language(),
2435 vec![],
2436 |_| Default::default(),
2437 );
2438
2439 let buffer = project.update(cx, |project, cx| {
2440 project.create_buffer("", None, cx).unwrap()
2441 });
2442 buffer.update(cx, |buffer, cx| {
2443 buffer.edit([(0..0, "abc")], None, cx);
2444 assert!(buffer.is_dirty());
2445 assert!(!buffer.has_conflict());
2446 assert_eq!(buffer.language().unwrap().name().as_ref(), "Plain Text");
2447 });
2448 project
2449 .update(cx, |project, cx| {
2450 project.save_buffer_as(buffer.clone(), "/dir/file1.rs".into(), cx)
2451 })
2452 .await
2453 .unwrap();
2454 assert_eq!(fs.load(Path::new("/dir/file1.rs")).await.unwrap(), "abc");
2455
2456 cx.foreground().run_until_parked();
2457 buffer.read_with(cx, |buffer, cx| {
2458 assert_eq!(
2459 buffer.file().unwrap().full_path(cx),
2460 Path::new("dir/file1.rs")
2461 );
2462 assert!(!buffer.is_dirty());
2463 assert!(!buffer.has_conflict());
2464 assert_eq!(buffer.language().unwrap().name().as_ref(), "Rust");
2465 });
2466
2467 let opened_buffer = project
2468 .update(cx, |project, cx| {
2469 project.open_local_buffer("/dir/file1.rs", cx)
2470 })
2471 .await
2472 .unwrap();
2473 assert_eq!(opened_buffer, buffer);
2474}
2475
2476#[gpui::test(retries = 5)]
2477async fn test_rescan_and_remote_updates(
2478 deterministic: Arc<Deterministic>,
2479 cx: &mut gpui::TestAppContext,
2480) {
2481 init_test(cx);
2482 cx.foreground().allow_parking();
2483
2484 let dir = temp_tree(json!({
2485 "a": {
2486 "file1": "",
2487 "file2": "",
2488 "file3": "",
2489 },
2490 "b": {
2491 "c": {
2492 "file4": "",
2493 "file5": "",
2494 }
2495 }
2496 }));
2497
2498 let project = Project::test(Arc::new(RealFs), [dir.path()], cx).await;
2499 let rpc = project.read_with(cx, |p, _| p.client.clone());
2500
2501 let buffer_for_path = |path: &'static str, cx: &mut gpui::TestAppContext| {
2502 let buffer = project.update(cx, |p, cx| p.open_local_buffer(dir.path().join(path), cx));
2503 async move { buffer.await.unwrap() }
2504 };
2505 let id_for_path = |path: &'static str, cx: &gpui::TestAppContext| {
2506 project.read_with(cx, |project, cx| {
2507 let tree = project.worktrees(cx).next().unwrap();
2508 tree.read(cx)
2509 .entry_for_path(path)
2510 .unwrap_or_else(|| panic!("no entry for path {}", path))
2511 .id
2512 })
2513 };
2514
2515 let buffer2 = buffer_for_path("a/file2", cx).await;
2516 let buffer3 = buffer_for_path("a/file3", cx).await;
2517 let buffer4 = buffer_for_path("b/c/file4", cx).await;
2518 let buffer5 = buffer_for_path("b/c/file5", cx).await;
2519
2520 let file2_id = id_for_path("a/file2", cx);
2521 let file3_id = id_for_path("a/file3", cx);
2522 let file4_id = id_for_path("b/c/file4", cx);
2523
2524 // Create a remote copy of this worktree.
2525 let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
2526 let initial_snapshot = tree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
2527 let remote = cx.update(|cx| {
2528 Worktree::remote(
2529 1,
2530 1,
2531 proto::WorktreeMetadata {
2532 id: initial_snapshot.id().to_proto(),
2533 root_name: initial_snapshot.root_name().into(),
2534 abs_path: initial_snapshot
2535 .abs_path()
2536 .as_os_str()
2537 .to_string_lossy()
2538 .into(),
2539 visible: true,
2540 },
2541 rpc.clone(),
2542 cx,
2543 )
2544 });
2545 remote.update(cx, |remote, _| {
2546 let update = initial_snapshot.build_initial_update(1);
2547 remote.as_remote_mut().unwrap().update_from_remote(update);
2548 });
2549 deterministic.run_until_parked();
2550
2551 cx.read(|cx| {
2552 assert!(!buffer2.read(cx).is_dirty());
2553 assert!(!buffer3.read(cx).is_dirty());
2554 assert!(!buffer4.read(cx).is_dirty());
2555 assert!(!buffer5.read(cx).is_dirty());
2556 });
2557
2558 // Rename and delete files and directories.
2559 tree.flush_fs_events(cx).await;
2560 std::fs::rename(dir.path().join("a/file3"), dir.path().join("b/c/file3")).unwrap();
2561 std::fs::remove_file(dir.path().join("b/c/file5")).unwrap();
2562 std::fs::rename(dir.path().join("b/c"), dir.path().join("d")).unwrap();
2563 std::fs::rename(dir.path().join("a/file2"), dir.path().join("a/file2.new")).unwrap();
2564 tree.flush_fs_events(cx).await;
2565
2566 let expected_paths = vec![
2567 "a",
2568 "a/file1",
2569 "a/file2.new",
2570 "b",
2571 "d",
2572 "d/file3",
2573 "d/file4",
2574 ];
2575
2576 cx.read(|app| {
2577 assert_eq!(
2578 tree.read(app)
2579 .paths()
2580 .map(|p| p.to_str().unwrap())
2581 .collect::<Vec<_>>(),
2582 expected_paths
2583 );
2584
2585 assert_eq!(id_for_path("a/file2.new", cx), file2_id);
2586 assert_eq!(id_for_path("d/file3", cx), file3_id);
2587 assert_eq!(id_for_path("d/file4", cx), file4_id);
2588
2589 assert_eq!(
2590 buffer2.read(app).file().unwrap().path().as_ref(),
2591 Path::new("a/file2.new")
2592 );
2593 assert_eq!(
2594 buffer3.read(app).file().unwrap().path().as_ref(),
2595 Path::new("d/file3")
2596 );
2597 assert_eq!(
2598 buffer4.read(app).file().unwrap().path().as_ref(),
2599 Path::new("d/file4")
2600 );
2601 assert_eq!(
2602 buffer5.read(app).file().unwrap().path().as_ref(),
2603 Path::new("b/c/file5")
2604 );
2605
2606 assert!(!buffer2.read(app).file().unwrap().is_deleted());
2607 assert!(!buffer3.read(app).file().unwrap().is_deleted());
2608 assert!(!buffer4.read(app).file().unwrap().is_deleted());
2609 assert!(buffer5.read(app).file().unwrap().is_deleted());
2610 });
2611
2612 // Update the remote worktree. Check that it becomes consistent with the
2613 // local worktree.
2614 remote.update(cx, |remote, cx| {
2615 let update = tree.read(cx).as_local().unwrap().snapshot().build_update(
2616 &initial_snapshot,
2617 1,
2618 1,
2619 true,
2620 );
2621 remote.as_remote_mut().unwrap().update_from_remote(update);
2622 });
2623 deterministic.run_until_parked();
2624 remote.read_with(cx, |remote, _| {
2625 assert_eq!(
2626 remote
2627 .paths()
2628 .map(|p| p.to_str().unwrap())
2629 .collect::<Vec<_>>(),
2630 expected_paths
2631 );
2632 });
2633}
2634
2635#[gpui::test(iterations = 10)]
2636async fn test_buffer_identity_across_renames(
2637 deterministic: Arc<Deterministic>,
2638 cx: &mut gpui::TestAppContext,
2639) {
2640 init_test(cx);
2641
2642 let fs = FakeFs::new(cx.background());
2643 fs.insert_tree(
2644 "/dir",
2645 json!({
2646 "a": {
2647 "file1": "",
2648 }
2649 }),
2650 )
2651 .await;
2652
2653 let project = Project::test(fs, [Path::new("/dir")], cx).await;
2654 let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
2655 let tree_id = tree.read_with(cx, |tree, _| tree.id());
2656
2657 let id_for_path = |path: &'static str, cx: &gpui::TestAppContext| {
2658 project.read_with(cx, |project, cx| {
2659 let tree = project.worktrees(cx).next().unwrap();
2660 tree.read(cx)
2661 .entry_for_path(path)
2662 .unwrap_or_else(|| panic!("no entry for path {}", path))
2663 .id
2664 })
2665 };
2666
2667 let dir_id = id_for_path("a", cx);
2668 let file_id = id_for_path("a/file1", cx);
2669 let buffer = project
2670 .update(cx, |p, cx| p.open_buffer((tree_id, "a/file1"), cx))
2671 .await
2672 .unwrap();
2673 buffer.read_with(cx, |buffer, _| assert!(!buffer.is_dirty()));
2674
2675 project
2676 .update(cx, |project, cx| {
2677 project.rename_entry(dir_id, Path::new("b"), cx)
2678 })
2679 .unwrap()
2680 .await
2681 .unwrap();
2682 deterministic.run_until_parked();
2683 assert_eq!(id_for_path("b", cx), dir_id);
2684 assert_eq!(id_for_path("b/file1", cx), file_id);
2685 buffer.read_with(cx, |buffer, _| assert!(!buffer.is_dirty()));
2686}
2687
2688#[gpui::test]
2689async fn test_buffer_deduping(cx: &mut gpui::TestAppContext) {
2690 init_test(cx);
2691
2692 let fs = FakeFs::new(cx.background());
2693 fs.insert_tree(
2694 "/dir",
2695 json!({
2696 "a.txt": "a-contents",
2697 "b.txt": "b-contents",
2698 }),
2699 )
2700 .await;
2701
2702 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
2703
2704 // Spawn multiple tasks to open paths, repeating some paths.
2705 let (buffer_a_1, buffer_b, buffer_a_2) = project.update(cx, |p, cx| {
2706 (
2707 p.open_local_buffer("/dir/a.txt", cx),
2708 p.open_local_buffer("/dir/b.txt", cx),
2709 p.open_local_buffer("/dir/a.txt", cx),
2710 )
2711 });
2712
2713 let buffer_a_1 = buffer_a_1.await.unwrap();
2714 let buffer_a_2 = buffer_a_2.await.unwrap();
2715 let buffer_b = buffer_b.await.unwrap();
2716 assert_eq!(buffer_a_1.read_with(cx, |b, _| b.text()), "a-contents");
2717 assert_eq!(buffer_b.read_with(cx, |b, _| b.text()), "b-contents");
2718
2719 // There is only one buffer per path.
2720 let buffer_a_id = buffer_a_1.id();
2721 assert_eq!(buffer_a_2.id(), buffer_a_id);
2722
2723 // Open the same path again while it is still open.
2724 drop(buffer_a_1);
2725 let buffer_a_3 = project
2726 .update(cx, |p, cx| p.open_local_buffer("/dir/a.txt", cx))
2727 .await
2728 .unwrap();
2729
2730 // There's still only one buffer per path.
2731 assert_eq!(buffer_a_3.id(), buffer_a_id);
2732}
2733
2734#[gpui::test]
2735async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) {
2736 init_test(cx);
2737
2738 let fs = FakeFs::new(cx.background());
2739 fs.insert_tree(
2740 "/dir",
2741 json!({
2742 "file1": "abc",
2743 "file2": "def",
2744 "file3": "ghi",
2745 }),
2746 )
2747 .await;
2748
2749 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
2750
2751 let buffer1 = project
2752 .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
2753 .await
2754 .unwrap();
2755 let events = Rc::new(RefCell::new(Vec::new()));
2756
2757 // initially, the buffer isn't dirty.
2758 buffer1.update(cx, |buffer, cx| {
2759 cx.subscribe(&buffer1, {
2760 let events = events.clone();
2761 move |_, _, event, _| match event {
2762 BufferEvent::Operation(_) => {}
2763 _ => events.borrow_mut().push(event.clone()),
2764 }
2765 })
2766 .detach();
2767
2768 assert!(!buffer.is_dirty());
2769 assert!(events.borrow().is_empty());
2770
2771 buffer.edit([(1..2, "")], None, cx);
2772 });
2773
2774 // after the first edit, the buffer is dirty, and emits a dirtied event.
2775 buffer1.update(cx, |buffer, cx| {
2776 assert!(buffer.text() == "ac");
2777 assert!(buffer.is_dirty());
2778 assert_eq!(
2779 *events.borrow(),
2780 &[language::Event::Edited, language::Event::DirtyChanged]
2781 );
2782 events.borrow_mut().clear();
2783 buffer.did_save(
2784 buffer.version(),
2785 buffer.as_rope().fingerprint(),
2786 buffer.file().unwrap().mtime(),
2787 cx,
2788 );
2789 });
2790
2791 // after saving, the buffer is not dirty, and emits a saved event.
2792 buffer1.update(cx, |buffer, cx| {
2793 assert!(!buffer.is_dirty());
2794 assert_eq!(*events.borrow(), &[language::Event::Saved]);
2795 events.borrow_mut().clear();
2796
2797 buffer.edit([(1..1, "B")], None, cx);
2798 buffer.edit([(2..2, "D")], None, cx);
2799 });
2800
2801 // after editing again, the buffer is dirty, and emits another dirty event.
2802 buffer1.update(cx, |buffer, cx| {
2803 assert!(buffer.text() == "aBDc");
2804 assert!(buffer.is_dirty());
2805 assert_eq!(
2806 *events.borrow(),
2807 &[
2808 language::Event::Edited,
2809 language::Event::DirtyChanged,
2810 language::Event::Edited,
2811 ],
2812 );
2813 events.borrow_mut().clear();
2814
2815 // After restoring the buffer to its previously-saved state,
2816 // the buffer is not considered dirty anymore.
2817 buffer.edit([(1..3, "")], None, cx);
2818 assert!(buffer.text() == "ac");
2819 assert!(!buffer.is_dirty());
2820 });
2821
2822 assert_eq!(
2823 *events.borrow(),
2824 &[language::Event::Edited, language::Event::DirtyChanged]
2825 );
2826
2827 // When a file is deleted, the buffer is considered dirty.
2828 let events = Rc::new(RefCell::new(Vec::new()));
2829 let buffer2 = project
2830 .update(cx, |p, cx| p.open_local_buffer("/dir/file2", cx))
2831 .await
2832 .unwrap();
2833 buffer2.update(cx, |_, cx| {
2834 cx.subscribe(&buffer2, {
2835 let events = events.clone();
2836 move |_, _, event, _| events.borrow_mut().push(event.clone())
2837 })
2838 .detach();
2839 });
2840
2841 fs.remove_file("/dir/file2".as_ref(), Default::default())
2842 .await
2843 .unwrap();
2844 cx.foreground().run_until_parked();
2845 buffer2.read_with(cx, |buffer, _| assert!(buffer.is_dirty()));
2846 assert_eq!(
2847 *events.borrow(),
2848 &[
2849 language::Event::DirtyChanged,
2850 language::Event::FileHandleChanged
2851 ]
2852 );
2853
2854 // When a file is already dirty when deleted, we don't emit a Dirtied event.
2855 let events = Rc::new(RefCell::new(Vec::new()));
2856 let buffer3 = project
2857 .update(cx, |p, cx| p.open_local_buffer("/dir/file3", cx))
2858 .await
2859 .unwrap();
2860 buffer3.update(cx, |_, cx| {
2861 cx.subscribe(&buffer3, {
2862 let events = events.clone();
2863 move |_, _, event, _| events.borrow_mut().push(event.clone())
2864 })
2865 .detach();
2866 });
2867
2868 buffer3.update(cx, |buffer, cx| {
2869 buffer.edit([(0..0, "x")], None, cx);
2870 });
2871 events.borrow_mut().clear();
2872 fs.remove_file("/dir/file3".as_ref(), Default::default())
2873 .await
2874 .unwrap();
2875 cx.foreground().run_until_parked();
2876 assert_eq!(*events.borrow(), &[language::Event::FileHandleChanged]);
2877 cx.read(|cx| assert!(buffer3.read(cx).is_dirty()));
2878}
2879
2880#[gpui::test]
2881async fn test_buffer_file_changes_on_disk(cx: &mut gpui::TestAppContext) {
2882 init_test(cx);
2883
2884 let initial_contents = "aaa\nbbbbb\nc\n";
2885 let fs = FakeFs::new(cx.background());
2886 fs.insert_tree(
2887 "/dir",
2888 json!({
2889 "the-file": initial_contents,
2890 }),
2891 )
2892 .await;
2893 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
2894 let buffer = project
2895 .update(cx, |p, cx| p.open_local_buffer("/dir/the-file", cx))
2896 .await
2897 .unwrap();
2898
2899 let anchors = (0..3)
2900 .map(|row| buffer.read_with(cx, |b, _| b.anchor_before(Point::new(row, 1))))
2901 .collect::<Vec<_>>();
2902
2903 // Change the file on disk, adding two new lines of text, and removing
2904 // one line.
2905 buffer.read_with(cx, |buffer, _| {
2906 assert!(!buffer.is_dirty());
2907 assert!(!buffer.has_conflict());
2908 });
2909 let new_contents = "AAAA\naaa\nBB\nbbbbb\n";
2910 fs.save(
2911 "/dir/the-file".as_ref(),
2912 &new_contents.into(),
2913 LineEnding::Unix,
2914 )
2915 .await
2916 .unwrap();
2917
2918 // Because the buffer was not modified, it is reloaded from disk. Its
2919 // contents are edited according to the diff between the old and new
2920 // file contents.
2921 cx.foreground().run_until_parked();
2922 buffer.update(cx, |buffer, _| {
2923 assert_eq!(buffer.text(), new_contents);
2924 assert!(!buffer.is_dirty());
2925 assert!(!buffer.has_conflict());
2926
2927 let anchor_positions = anchors
2928 .iter()
2929 .map(|anchor| anchor.to_point(&*buffer))
2930 .collect::<Vec<_>>();
2931 assert_eq!(
2932 anchor_positions,
2933 [Point::new(1, 1), Point::new(3, 1), Point::new(3, 5)]
2934 );
2935 });
2936
2937 // Modify the buffer
2938 buffer.update(cx, |buffer, cx| {
2939 buffer.edit([(0..0, " ")], None, cx);
2940 assert!(buffer.is_dirty());
2941 assert!(!buffer.has_conflict());
2942 });
2943
2944 // Change the file on disk again, adding blank lines to the beginning.
2945 fs.save(
2946 "/dir/the-file".as_ref(),
2947 &"\n\n\nAAAA\naaa\nBB\nbbbbb\n".into(),
2948 LineEnding::Unix,
2949 )
2950 .await
2951 .unwrap();
2952
2953 // Because the buffer is modified, it doesn't reload from disk, but is
2954 // marked as having a conflict.
2955 cx.foreground().run_until_parked();
2956 buffer.read_with(cx, |buffer, _| {
2957 assert!(buffer.has_conflict());
2958 });
2959}
2960
2961#[gpui::test]
2962async fn test_buffer_line_endings(cx: &mut gpui::TestAppContext) {
2963 init_test(cx);
2964
2965 let fs = FakeFs::new(cx.background());
2966 fs.insert_tree(
2967 "/dir",
2968 json!({
2969 "file1": "a\nb\nc\n",
2970 "file2": "one\r\ntwo\r\nthree\r\n",
2971 }),
2972 )
2973 .await;
2974
2975 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
2976 let buffer1 = project
2977 .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
2978 .await
2979 .unwrap();
2980 let buffer2 = project
2981 .update(cx, |p, cx| p.open_local_buffer("/dir/file2", cx))
2982 .await
2983 .unwrap();
2984
2985 buffer1.read_with(cx, |buffer, _| {
2986 assert_eq!(buffer.text(), "a\nb\nc\n");
2987 assert_eq!(buffer.line_ending(), LineEnding::Unix);
2988 });
2989 buffer2.read_with(cx, |buffer, _| {
2990 assert_eq!(buffer.text(), "one\ntwo\nthree\n");
2991 assert_eq!(buffer.line_ending(), LineEnding::Windows);
2992 });
2993
2994 // Change a file's line endings on disk from unix to windows. The buffer's
2995 // state updates correctly.
2996 fs.save(
2997 "/dir/file1".as_ref(),
2998 &"aaa\nb\nc\n".into(),
2999 LineEnding::Windows,
3000 )
3001 .await
3002 .unwrap();
3003 cx.foreground().run_until_parked();
3004 buffer1.read_with(cx, |buffer, _| {
3005 assert_eq!(buffer.text(), "aaa\nb\nc\n");
3006 assert_eq!(buffer.line_ending(), LineEnding::Windows);
3007 });
3008
3009 // Save a file with windows line endings. The file is written correctly.
3010 buffer2.update(cx, |buffer, cx| {
3011 buffer.set_text("one\ntwo\nthree\nfour\n", cx);
3012 });
3013 project
3014 .update(cx, |project, cx| project.save_buffer(buffer2, cx))
3015 .await
3016 .unwrap();
3017 assert_eq!(
3018 fs.load("/dir/file2".as_ref()).await.unwrap(),
3019 "one\r\ntwo\r\nthree\r\nfour\r\n",
3020 );
3021}
3022
3023#[gpui::test]
3024async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
3025 init_test(cx);
3026
3027 let fs = FakeFs::new(cx.background());
3028 fs.insert_tree(
3029 "/the-dir",
3030 json!({
3031 "a.rs": "
3032 fn foo(mut v: Vec<usize>) {
3033 for x in &v {
3034 v.push(1);
3035 }
3036 }
3037 "
3038 .unindent(),
3039 }),
3040 )
3041 .await;
3042
3043 let project = Project::test(fs.clone(), ["/the-dir".as_ref()], cx).await;
3044 let buffer = project
3045 .update(cx, |p, cx| p.open_local_buffer("/the-dir/a.rs", cx))
3046 .await
3047 .unwrap();
3048
3049 let buffer_uri = Url::from_file_path("/the-dir/a.rs").unwrap();
3050 let message = lsp::PublishDiagnosticsParams {
3051 uri: buffer_uri.clone(),
3052 diagnostics: vec![
3053 lsp::Diagnostic {
3054 range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
3055 severity: Some(DiagnosticSeverity::WARNING),
3056 message: "error 1".to_string(),
3057 related_information: Some(vec![lsp::DiagnosticRelatedInformation {
3058 location: lsp::Location {
3059 uri: buffer_uri.clone(),
3060 range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
3061 },
3062 message: "error 1 hint 1".to_string(),
3063 }]),
3064 ..Default::default()
3065 },
3066 lsp::Diagnostic {
3067 range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
3068 severity: Some(DiagnosticSeverity::HINT),
3069 message: "error 1 hint 1".to_string(),
3070 related_information: Some(vec![lsp::DiagnosticRelatedInformation {
3071 location: lsp::Location {
3072 uri: buffer_uri.clone(),
3073 range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
3074 },
3075 message: "original diagnostic".to_string(),
3076 }]),
3077 ..Default::default()
3078 },
3079 lsp::Diagnostic {
3080 range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)),
3081 severity: Some(DiagnosticSeverity::ERROR),
3082 message: "error 2".to_string(),
3083 related_information: Some(vec![
3084 lsp::DiagnosticRelatedInformation {
3085 location: lsp::Location {
3086 uri: buffer_uri.clone(),
3087 range: lsp::Range::new(
3088 lsp::Position::new(1, 13),
3089 lsp::Position::new(1, 15),
3090 ),
3091 },
3092 message: "error 2 hint 1".to_string(),
3093 },
3094 lsp::DiagnosticRelatedInformation {
3095 location: lsp::Location {
3096 uri: buffer_uri.clone(),
3097 range: lsp::Range::new(
3098 lsp::Position::new(1, 13),
3099 lsp::Position::new(1, 15),
3100 ),
3101 },
3102 message: "error 2 hint 2".to_string(),
3103 },
3104 ]),
3105 ..Default::default()
3106 },
3107 lsp::Diagnostic {
3108 range: lsp::Range::new(lsp::Position::new(1, 13), lsp::Position::new(1, 15)),
3109 severity: Some(DiagnosticSeverity::HINT),
3110 message: "error 2 hint 1".to_string(),
3111 related_information: Some(vec![lsp::DiagnosticRelatedInformation {
3112 location: lsp::Location {
3113 uri: buffer_uri.clone(),
3114 range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)),
3115 },
3116 message: "original diagnostic".to_string(),
3117 }]),
3118 ..Default::default()
3119 },
3120 lsp::Diagnostic {
3121 range: lsp::Range::new(lsp::Position::new(1, 13), lsp::Position::new(1, 15)),
3122 severity: Some(DiagnosticSeverity::HINT),
3123 message: "error 2 hint 2".to_string(),
3124 related_information: Some(vec![lsp::DiagnosticRelatedInformation {
3125 location: lsp::Location {
3126 uri: buffer_uri,
3127 range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)),
3128 },
3129 message: "original diagnostic".to_string(),
3130 }]),
3131 ..Default::default()
3132 },
3133 ],
3134 version: None,
3135 };
3136
3137 project
3138 .update(cx, |p, cx| {
3139 p.update_diagnostics(LanguageServerId(0), message, &[], cx)
3140 })
3141 .unwrap();
3142 let buffer = buffer.read_with(cx, |buffer, _| buffer.snapshot());
3143
3144 assert_eq!(
3145 buffer
3146 .diagnostics_in_range::<_, Point>(0..buffer.len(), false)
3147 .collect::<Vec<_>>(),
3148 &[
3149 DiagnosticEntry {
3150 range: Point::new(1, 8)..Point::new(1, 9),
3151 diagnostic: Diagnostic {
3152 severity: DiagnosticSeverity::WARNING,
3153 message: "error 1".to_string(),
3154 group_id: 1,
3155 is_primary: true,
3156 ..Default::default()
3157 }
3158 },
3159 DiagnosticEntry {
3160 range: Point::new(1, 8)..Point::new(1, 9),
3161 diagnostic: Diagnostic {
3162 severity: DiagnosticSeverity::HINT,
3163 message: "error 1 hint 1".to_string(),
3164 group_id: 1,
3165 is_primary: false,
3166 ..Default::default()
3167 }
3168 },
3169 DiagnosticEntry {
3170 range: Point::new(1, 13)..Point::new(1, 15),
3171 diagnostic: Diagnostic {
3172 severity: DiagnosticSeverity::HINT,
3173 message: "error 2 hint 1".to_string(),
3174 group_id: 0,
3175 is_primary: false,
3176 ..Default::default()
3177 }
3178 },
3179 DiagnosticEntry {
3180 range: Point::new(1, 13)..Point::new(1, 15),
3181 diagnostic: Diagnostic {
3182 severity: DiagnosticSeverity::HINT,
3183 message: "error 2 hint 2".to_string(),
3184 group_id: 0,
3185 is_primary: false,
3186 ..Default::default()
3187 }
3188 },
3189 DiagnosticEntry {
3190 range: Point::new(2, 8)..Point::new(2, 17),
3191 diagnostic: Diagnostic {
3192 severity: DiagnosticSeverity::ERROR,
3193 message: "error 2".to_string(),
3194 group_id: 0,
3195 is_primary: true,
3196 ..Default::default()
3197 }
3198 }
3199 ]
3200 );
3201
3202 assert_eq!(
3203 buffer.diagnostic_group::<Point>(0).collect::<Vec<_>>(),
3204 &[
3205 DiagnosticEntry {
3206 range: Point::new(1, 13)..Point::new(1, 15),
3207 diagnostic: Diagnostic {
3208 severity: DiagnosticSeverity::HINT,
3209 message: "error 2 hint 1".to_string(),
3210 group_id: 0,
3211 is_primary: false,
3212 ..Default::default()
3213 }
3214 },
3215 DiagnosticEntry {
3216 range: Point::new(1, 13)..Point::new(1, 15),
3217 diagnostic: Diagnostic {
3218 severity: DiagnosticSeverity::HINT,
3219 message: "error 2 hint 2".to_string(),
3220 group_id: 0,
3221 is_primary: false,
3222 ..Default::default()
3223 }
3224 },
3225 DiagnosticEntry {
3226 range: Point::new(2, 8)..Point::new(2, 17),
3227 diagnostic: Diagnostic {
3228 severity: DiagnosticSeverity::ERROR,
3229 message: "error 2".to_string(),
3230 group_id: 0,
3231 is_primary: true,
3232 ..Default::default()
3233 }
3234 }
3235 ]
3236 );
3237
3238 assert_eq!(
3239 buffer.diagnostic_group::<Point>(1).collect::<Vec<_>>(),
3240 &[
3241 DiagnosticEntry {
3242 range: Point::new(1, 8)..Point::new(1, 9),
3243 diagnostic: Diagnostic {
3244 severity: DiagnosticSeverity::WARNING,
3245 message: "error 1".to_string(),
3246 group_id: 1,
3247 is_primary: true,
3248 ..Default::default()
3249 }
3250 },
3251 DiagnosticEntry {
3252 range: Point::new(1, 8)..Point::new(1, 9),
3253 diagnostic: Diagnostic {
3254 severity: DiagnosticSeverity::HINT,
3255 message: "error 1 hint 1".to_string(),
3256 group_id: 1,
3257 is_primary: false,
3258 ..Default::default()
3259 }
3260 },
3261 ]
3262 );
3263}
3264
3265#[gpui::test]
3266async fn test_rename(cx: &mut gpui::TestAppContext) {
3267 init_test(cx);
3268
3269 let mut language = Language::new(
3270 LanguageConfig {
3271 name: "Rust".into(),
3272 path_suffixes: vec!["rs".to_string()],
3273 ..Default::default()
3274 },
3275 Some(tree_sitter_rust::language()),
3276 );
3277 let mut fake_servers = language
3278 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
3279 capabilities: lsp::ServerCapabilities {
3280 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
3281 prepare_provider: Some(true),
3282 work_done_progress_options: Default::default(),
3283 })),
3284 ..Default::default()
3285 },
3286 ..Default::default()
3287 }))
3288 .await;
3289
3290 let fs = FakeFs::new(cx.background());
3291 fs.insert_tree(
3292 "/dir",
3293 json!({
3294 "one.rs": "const ONE: usize = 1;",
3295 "two.rs": "const TWO: usize = one::ONE + one::ONE;"
3296 }),
3297 )
3298 .await;
3299
3300 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
3301 project.update(cx, |project, _| project.languages.add(Arc::new(language)));
3302 let buffer = project
3303 .update(cx, |project, cx| {
3304 project.open_local_buffer("/dir/one.rs", cx)
3305 })
3306 .await
3307 .unwrap();
3308
3309 let fake_server = fake_servers.next().await.unwrap();
3310
3311 let response = project.update(cx, |project, cx| {
3312 project.prepare_rename(buffer.clone(), 7, cx)
3313 });
3314 fake_server
3315 .handle_request::<lsp::request::PrepareRenameRequest, _, _>(|params, _| async move {
3316 assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs");
3317 assert_eq!(params.position, lsp::Position::new(0, 7));
3318 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
3319 lsp::Position::new(0, 6),
3320 lsp::Position::new(0, 9),
3321 ))))
3322 })
3323 .next()
3324 .await
3325 .unwrap();
3326 let range = response.await.unwrap().unwrap();
3327 let range = buffer.read_with(cx, |buffer, _| range.to_offset(buffer));
3328 assert_eq!(range, 6..9);
3329
3330 let response = project.update(cx, |project, cx| {
3331 project.perform_rename(buffer.clone(), 7, "THREE".to_string(), true, cx)
3332 });
3333 fake_server
3334 .handle_request::<lsp::request::Rename, _, _>(|params, _| async move {
3335 assert_eq!(
3336 params.text_document_position.text_document.uri.as_str(),
3337 "file:///dir/one.rs"
3338 );
3339 assert_eq!(
3340 params.text_document_position.position,
3341 lsp::Position::new(0, 7)
3342 );
3343 assert_eq!(params.new_name, "THREE");
3344 Ok(Some(lsp::WorkspaceEdit {
3345 changes: Some(
3346 [
3347 (
3348 lsp::Url::from_file_path("/dir/one.rs").unwrap(),
3349 vec![lsp::TextEdit::new(
3350 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
3351 "THREE".to_string(),
3352 )],
3353 ),
3354 (
3355 lsp::Url::from_file_path("/dir/two.rs").unwrap(),
3356 vec![
3357 lsp::TextEdit::new(
3358 lsp::Range::new(
3359 lsp::Position::new(0, 24),
3360 lsp::Position::new(0, 27),
3361 ),
3362 "THREE".to_string(),
3363 ),
3364 lsp::TextEdit::new(
3365 lsp::Range::new(
3366 lsp::Position::new(0, 35),
3367 lsp::Position::new(0, 38),
3368 ),
3369 "THREE".to_string(),
3370 ),
3371 ],
3372 ),
3373 ]
3374 .into_iter()
3375 .collect(),
3376 ),
3377 ..Default::default()
3378 }))
3379 })
3380 .next()
3381 .await
3382 .unwrap();
3383 let mut transaction = response.await.unwrap().0;
3384 assert_eq!(transaction.len(), 2);
3385 assert_eq!(
3386 transaction
3387 .remove_entry(&buffer)
3388 .unwrap()
3389 .0
3390 .read_with(cx, |buffer, _| buffer.text()),
3391 "const THREE: usize = 1;"
3392 );
3393 assert_eq!(
3394 transaction
3395 .into_keys()
3396 .next()
3397 .unwrap()
3398 .read_with(cx, |buffer, _| buffer.text()),
3399 "const TWO: usize = one::THREE + one::THREE;"
3400 );
3401}
3402
3403#[gpui::test]
3404async fn test_search(cx: &mut gpui::TestAppContext) {
3405 init_test(cx);
3406
3407 let fs = FakeFs::new(cx.background());
3408 fs.insert_tree(
3409 "/dir",
3410 json!({
3411 "one.rs": "const ONE: usize = 1;",
3412 "two.rs": "const TWO: usize = one::ONE + one::ONE;",
3413 "three.rs": "const THREE: usize = one::ONE + two::TWO;",
3414 "four.rs": "const FOUR: usize = one::ONE + three::THREE;",
3415 }),
3416 )
3417 .await;
3418 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
3419 assert_eq!(
3420 search(
3421 &project,
3422 SearchQuery::text("TWO", false, true, Vec::new(), Vec::new()),
3423 cx
3424 )
3425 .await
3426 .unwrap(),
3427 HashMap::from_iter([
3428 ("two.rs".to_string(), vec![6..9]),
3429 ("three.rs".to_string(), vec![37..40])
3430 ])
3431 );
3432
3433 let buffer_4 = project
3434 .update(cx, |project, cx| {
3435 project.open_local_buffer("/dir/four.rs", cx)
3436 })
3437 .await
3438 .unwrap();
3439 buffer_4.update(cx, |buffer, cx| {
3440 let text = "two::TWO";
3441 buffer.edit([(20..28, text), (31..43, text)], None, cx);
3442 });
3443
3444 assert_eq!(
3445 search(
3446 &project,
3447 SearchQuery::text("TWO", false, true, Vec::new(), Vec::new()),
3448 cx
3449 )
3450 .await
3451 .unwrap(),
3452 HashMap::from_iter([
3453 ("two.rs".to_string(), vec![6..9]),
3454 ("three.rs".to_string(), vec![37..40]),
3455 ("four.rs".to_string(), vec![25..28, 36..39])
3456 ])
3457 );
3458}
3459
3460#[gpui::test]
3461async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
3462 init_test(cx);
3463
3464 let search_query = "file";
3465
3466 let fs = FakeFs::new(cx.background());
3467 fs.insert_tree(
3468 "/dir",
3469 json!({
3470 "one.rs": r#"// Rust file one"#,
3471 "one.ts": r#"// TypeScript file one"#,
3472 "two.rs": r#"// Rust file two"#,
3473 "two.ts": r#"// TypeScript file two"#,
3474 }),
3475 )
3476 .await;
3477 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
3478
3479 assert!(
3480 search(
3481 &project,
3482 SearchQuery::text(
3483 search_query,
3484 false,
3485 true,
3486 vec![Glob::new("*.odd").unwrap().compile_matcher()],
3487 Vec::new()
3488 ),
3489 cx
3490 )
3491 .await
3492 .unwrap()
3493 .is_empty(),
3494 "If no inclusions match, no files should be returned"
3495 );
3496
3497 assert_eq!(
3498 search(
3499 &project,
3500 SearchQuery::text(
3501 search_query,
3502 false,
3503 true,
3504 vec![Glob::new("*.rs").unwrap().compile_matcher()],
3505 Vec::new()
3506 ),
3507 cx
3508 )
3509 .await
3510 .unwrap(),
3511 HashMap::from_iter([
3512 ("one.rs".to_string(), vec![8..12]),
3513 ("two.rs".to_string(), vec![8..12]),
3514 ]),
3515 "Rust only search should give only Rust files"
3516 );
3517
3518 assert_eq!(
3519 search(
3520 &project,
3521 SearchQuery::text(
3522 search_query,
3523 false,
3524 true,
3525 vec![
3526 Glob::new("*.ts").unwrap().compile_matcher(),
3527 Glob::new("*.odd").unwrap().compile_matcher(),
3528 ],
3529 Vec::new()
3530 ),
3531 cx
3532 )
3533 .await
3534 .unwrap(),
3535 HashMap::from_iter([
3536 ("one.ts".to_string(), vec![14..18]),
3537 ("two.ts".to_string(), vec![14..18]),
3538 ]),
3539 "TypeScript only search should give only TypeScript files, even if other inclusions don't match anything"
3540 );
3541
3542 assert_eq!(
3543 search(
3544 &project,
3545 SearchQuery::text(
3546 search_query,
3547 false,
3548 true,
3549 vec![
3550 Glob::new("*.rs").unwrap().compile_matcher(),
3551 Glob::new("*.ts").unwrap().compile_matcher(),
3552 Glob::new("*.odd").unwrap().compile_matcher(),
3553 ],
3554 Vec::new()
3555 ),
3556 cx
3557 )
3558 .await
3559 .unwrap(),
3560 HashMap::from_iter([
3561 ("one.rs".to_string(), vec![8..12]),
3562 ("one.ts".to_string(), vec![14..18]),
3563 ("two.rs".to_string(), vec![8..12]),
3564 ("two.ts".to_string(), vec![14..18]),
3565 ]),
3566 "Rust and typescript search should give both Rust and TypeScript files, even if other inclusions don't match anything"
3567 );
3568}
3569
3570#[gpui::test]
3571async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
3572 init_test(cx);
3573
3574 let search_query = "file";
3575
3576 let fs = FakeFs::new(cx.background());
3577 fs.insert_tree(
3578 "/dir",
3579 json!({
3580 "one.rs": r#"// Rust file one"#,
3581 "one.ts": r#"// TypeScript file one"#,
3582 "two.rs": r#"// Rust file two"#,
3583 "two.ts": r#"// TypeScript file two"#,
3584 }),
3585 )
3586 .await;
3587 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
3588
3589 assert_eq!(
3590 search(
3591 &project,
3592 SearchQuery::text(
3593 search_query,
3594 false,
3595 true,
3596 Vec::new(),
3597 vec![Glob::new("*.odd").unwrap().compile_matcher()],
3598 ),
3599 cx
3600 )
3601 .await
3602 .unwrap(),
3603 HashMap::from_iter([
3604 ("one.rs".to_string(), vec![8..12]),
3605 ("one.ts".to_string(), vec![14..18]),
3606 ("two.rs".to_string(), vec![8..12]),
3607 ("two.ts".to_string(), vec![14..18]),
3608 ]),
3609 "If no exclusions match, all files should be returned"
3610 );
3611
3612 assert_eq!(
3613 search(
3614 &project,
3615 SearchQuery::text(
3616 search_query,
3617 false,
3618 true,
3619 Vec::new(),
3620 vec![Glob::new("*.rs").unwrap().compile_matcher()],
3621 ),
3622 cx
3623 )
3624 .await
3625 .unwrap(),
3626 HashMap::from_iter([
3627 ("one.ts".to_string(), vec![14..18]),
3628 ("two.ts".to_string(), vec![14..18]),
3629 ]),
3630 "Rust exclusion search should give only TypeScript files"
3631 );
3632
3633 assert_eq!(
3634 search(
3635 &project,
3636 SearchQuery::text(
3637 search_query,
3638 false,
3639 true,
3640 Vec::new(),
3641 vec![
3642 Glob::new("*.ts").unwrap().compile_matcher(),
3643 Glob::new("*.odd").unwrap().compile_matcher(),
3644 ],
3645 ),
3646 cx
3647 )
3648 .await
3649 .unwrap(),
3650 HashMap::from_iter([
3651 ("one.rs".to_string(), vec![8..12]),
3652 ("two.rs".to_string(), vec![8..12]),
3653 ]),
3654 "TypeScript exclusion search should give only Rust files, even if other exclusions don't match anything"
3655 );
3656
3657 assert!(
3658 search(
3659 &project,
3660 SearchQuery::text(
3661 search_query,
3662 false,
3663 true,
3664 Vec::new(),
3665 vec![
3666 Glob::new("*.rs").unwrap().compile_matcher(),
3667 Glob::new("*.ts").unwrap().compile_matcher(),
3668 Glob::new("*.odd").unwrap().compile_matcher(),
3669 ],
3670 ),
3671 cx
3672 )
3673 .await
3674 .unwrap().is_empty(),
3675 "Rust and typescript exclusion should give no files, even if other exclusions don't match anything"
3676 );
3677}
3678
3679#[gpui::test]
3680async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContext) {
3681 init_test(cx);
3682
3683 let search_query = "file";
3684
3685 let fs = FakeFs::new(cx.background());
3686 fs.insert_tree(
3687 "/dir",
3688 json!({
3689 "one.rs": r#"// Rust file one"#,
3690 "one.ts": r#"// TypeScript file one"#,
3691 "two.rs": r#"// Rust file two"#,
3692 "two.ts": r#"// TypeScript file two"#,
3693 }),
3694 )
3695 .await;
3696 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
3697
3698 assert!(
3699 search(
3700 &project,
3701 SearchQuery::text(
3702 search_query,
3703 false,
3704 true,
3705 vec![Glob::new("*.odd").unwrap().compile_matcher()],
3706 vec![Glob::new("*.odd").unwrap().compile_matcher()],
3707 ),
3708 cx
3709 )
3710 .await
3711 .unwrap()
3712 .is_empty(),
3713 "If both no exclusions and inclusions match, exclusions should win and return nothing"
3714 );
3715
3716 assert!(
3717 search(
3718 &project,
3719 SearchQuery::text(
3720 search_query,
3721 false,
3722 true,
3723 vec![Glob::new("*.ts").unwrap().compile_matcher()],
3724 vec![Glob::new("*.ts").unwrap().compile_matcher()],
3725 ),
3726 cx
3727 )
3728 .await
3729 .unwrap()
3730 .is_empty(),
3731 "If both TypeScript exclusions and inclusions match, exclusions should win and return nothing files."
3732 );
3733
3734 assert!(
3735 search(
3736 &project,
3737 SearchQuery::text(
3738 search_query,
3739 false,
3740 true,
3741 vec![
3742 Glob::new("*.ts").unwrap().compile_matcher(),
3743 Glob::new("*.odd").unwrap().compile_matcher()
3744 ],
3745 vec![
3746 Glob::new("*.ts").unwrap().compile_matcher(),
3747 Glob::new("*.odd").unwrap().compile_matcher()
3748 ],
3749 ),
3750 cx
3751 )
3752 .await
3753 .unwrap()
3754 .is_empty(),
3755 "Non-matching inclusions and exclusions should not change that."
3756 );
3757
3758 assert_eq!(
3759 search(
3760 &project,
3761 SearchQuery::text(
3762 search_query,
3763 false,
3764 true,
3765 vec![
3766 Glob::new("*.ts").unwrap().compile_matcher(),
3767 Glob::new("*.odd").unwrap().compile_matcher()
3768 ],
3769 vec![
3770 Glob::new("*.rs").unwrap().compile_matcher(),
3771 Glob::new("*.odd").unwrap().compile_matcher()
3772 ],
3773 ),
3774 cx
3775 )
3776 .await
3777 .unwrap(),
3778 HashMap::from_iter([
3779 ("one.ts".to_string(), vec![14..18]),
3780 ("two.ts".to_string(), vec![14..18]),
3781 ]),
3782 "Non-intersecting TypeScript inclusions and Rust exclusions should return TypeScript files"
3783 );
3784}
3785
3786async fn search(
3787 project: &ModelHandle<Project>,
3788 query: SearchQuery,
3789 cx: &mut gpui::TestAppContext,
3790) -> Result<HashMap<String, Vec<Range<usize>>>> {
3791 let results = project
3792 .update(cx, |project, cx| project.search(query, cx))
3793 .await?;
3794
3795 Ok(results
3796 .into_iter()
3797 .map(|(buffer, ranges)| {
3798 buffer.read_with(cx, |buffer, _| {
3799 let path = buffer.file().unwrap().path().to_string_lossy().to_string();
3800 let ranges = ranges
3801 .into_iter()
3802 .map(|range| range.to_offset(buffer))
3803 .collect::<Vec<_>>();
3804 (path, ranges)
3805 })
3806 })
3807 .collect())
3808}
3809
3810fn init_test(cx: &mut gpui::TestAppContext) {
3811 cx.foreground().forbid_parking();
3812
3813 cx.update(|cx| {
3814 cx.set_global(SettingsStore::test(cx));
3815 language::init(cx);
3816 Project::init_settings(cx);
3817 });
3818}