1use super::*;
2use crate::assemble_excerpts::assemble_excerpt_ranges;
3use futures::channel::mpsc::UnboundedReceiver;
4use gpui::TestAppContext;
5use indoc::indoc;
6use language::{Point, ToPoint as _, rust_lang};
7use lsp::FakeLanguageServer;
8use project::{FakeFs, LocationLink, Project};
9use serde_json::json;
10use settings::SettingsStore;
11use std::fmt::Write as _;
12use util::{path, test::marked_text_ranges};
13
14#[gpui::test]
15async fn test_edit_prediction_context(cx: &mut TestAppContext) {
16 init_test(cx);
17 let fs = FakeFs::new(cx.executor());
18 fs.insert_tree(path!("/root"), test_project_1()).await;
19
20 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
21 let mut servers = setup_fake_lsp(&project, cx);
22
23 let (buffer, _handle) = project
24 .update(cx, |project, cx| {
25 project.open_local_buffer_with_lsp(path!("/root/src/main.rs"), cx)
26 })
27 .await
28 .unwrap();
29
30 let _server = servers.next().await.unwrap();
31 cx.run_until_parked();
32
33 let related_excerpt_store = cx.new(|cx| RelatedExcerptStore::new(&project, cx));
34 related_excerpt_store.update(cx, |store, cx| {
35 let position = {
36 let buffer = buffer.read(cx);
37 let offset = buffer.text().find("todo").unwrap();
38 buffer.anchor_before(offset)
39 };
40
41 store.set_identifier_line_count(0);
42 store.refresh(buffer.clone(), position, cx);
43 });
44
45 cx.executor().advance_clock(DEBOUNCE_DURATION);
46 related_excerpt_store.update(cx, |store, cx| {
47 let excerpts = store.related_files(cx);
48 assert_related_files(
49 &excerpts,
50 &[
51 (
52 "root/src/company.rs",
53 &[indoc! {"
54 pub struct Company {
55 owner: Arc<Person>,
56 address: Address,
57 }"}],
58 ),
59 (
60 "root/src/main.rs",
61 &[
62 indoc! {"
63 pub struct Session {
64 company: Arc<Company>,
65 }
66
67 impl Session {
68 pub fn set_company(&mut self, company: Arc<Company>) {"},
69 indoc! {"
70 }
71 }"},
72 ],
73 ),
74 (
75 "root/src/person.rs",
76 &[
77 indoc! {"
78 pub struct Person {
79 first_name: String,
80 last_name: String,
81 email: String,
82 age: u32,
83 }
84
85 impl Person {
86 pub fn get_first_name(&self) -> &str {
87 &self.first_name
88 }"},
89 "}",
90 ],
91 ),
92 ],
93 );
94 });
95
96 let company_buffer = related_excerpt_store.update(cx, |store, cx| {
97 store
98 .related_files_with_buffers(cx)
99 .find(|(file, _)| file.path.to_str() == Some("root/src/company.rs"))
100 .map(|(_, buffer)| buffer)
101 .expect("company.rs buffer not found")
102 });
103
104 company_buffer.update(cx, |buffer, cx| {
105 let text = buffer.text();
106 let insert_pos = text.find("address: Address,").unwrap() + "address: Address,".len();
107 buffer.edit([(insert_pos..insert_pos, "\n name: String,")], None, cx);
108 });
109
110 related_excerpt_store.update(cx, |store, cx| {
111 let excerpts = store.related_files(cx);
112 assert_related_files(
113 &excerpts,
114 &[
115 (
116 "root/src/company.rs",
117 &[indoc! {"
118 pub struct Company {
119 owner: Arc<Person>,
120 address: Address,
121 name: String,
122 }"}],
123 ),
124 (
125 "root/src/main.rs",
126 &[
127 indoc! {"
128 pub struct Session {
129 company: Arc<Company>,
130 }
131
132 impl Session {
133 pub fn set_company(&mut self, company: Arc<Company>) {"},
134 indoc! {"
135 }
136 }"},
137 ],
138 ),
139 (
140 "root/src/person.rs",
141 &[
142 indoc! {"
143 pub struct Person {
144 first_name: String,
145 last_name: String,
146 email: String,
147 age: u32,
148 }
149
150 impl Person {
151 pub fn get_first_name(&self) -> &str {
152 &self.first_name
153 }"},
154 "}",
155 ],
156 ),
157 ],
158 );
159 });
160}
161
162#[gpui::test]
163fn test_assemble_excerpts(cx: &mut TestAppContext) {
164 let table = [
165 (
166 indoc! {r#"
167 struct User {
168 first_name: String,
169 «last_name»: String,
170 age: u32,
171 email: String,
172 create_at: Instant,
173 }
174
175 impl User {
176 pub fn first_name(&self) -> String {
177 self.first_name.clone()
178 }
179
180 pub fn full_name(&self) -> String {
181 « format!("{} {}", self.first_name, self.last_name)
182 » }
183 }
184 "#},
185 indoc! {r#"
186 struct User {
187 first_name: String,
188 last_name: String,
189 …
190 }
191
192 impl User {
193 …
194 pub fn full_name(&self) -> String {
195 format!("{} {}", self.first_name, self.last_name)
196 }
197 }
198 "#},
199 ),
200 (
201 indoc! {r#"
202 struct «User» {
203 first_name: String,
204 last_name: String,
205 age: u32,
206 }
207
208 impl User {
209 // methods
210 }
211 "#},
212 indoc! {r#"
213 struct User {
214 first_name: String,
215 last_name: String,
216 age: u32,
217 }
218 …
219 "#},
220 ),
221 (
222 indoc! {r#"
223 trait «FooProvider» {
224 const NAME: &'static str;
225
226 fn provide_foo(&self, id: usize) -> Foo;
227
228 fn provide_foo_batched(&self, ids: &[usize]) -> Vec<Foo> {
229 ids.iter()
230 .map(|id| self.provide_foo(*id))
231 .collect()
232 }
233
234 fn sync(&self);
235 }
236 "#
237 },
238 indoc! {r#"
239 trait FooProvider {
240 const NAME: &'static str;
241
242 fn provide_foo(&self, id: usize) -> Foo;
243
244 fn provide_foo_batched(&self, ids: &[usize]) -> Vec<Foo> {
245 …
246 }
247
248 fn sync(&self);
249 }
250 "#},
251 ),
252 (
253 indoc! {r#"
254 trait «Something» {
255 fn method1(&self, id: usize) -> Foo;
256
257 fn method2(&self, ids: &[usize]) -> Vec<Foo> {
258 struct Helper1 {
259 field1: usize,
260 }
261
262 struct Helper2 {
263 field2: usize,
264 }
265
266 struct Helper3 {
267 filed2: usize,
268 }
269 }
270
271 fn sync(&self);
272 }
273 "#
274 },
275 indoc! {r#"
276 trait Something {
277 fn method1(&self, id: usize) -> Foo;
278
279 fn method2(&self, ids: &[usize]) -> Vec<Foo> {
280 …
281 }
282
283 fn sync(&self);
284 }
285 "#},
286 ),
287 ];
288
289 for (input, expected_output) in table {
290 let (input, ranges) = marked_text_ranges(&input, false);
291 let buffer = cx.new(|cx| Buffer::local(input, cx).with_language(rust_lang(), cx));
292 buffer.read_with(cx, |buffer, _cx| {
293 let ranges: Vec<Range<Point>> = ranges
294 .into_iter()
295 .map(|range| range.to_point(&buffer))
296 .collect();
297
298 let row_ranges = assemble_excerpt_ranges(&buffer.snapshot(), ranges);
299 let excerpts: Vec<RelatedExcerpt> = row_ranges
300 .into_iter()
301 .map(|row_range| {
302 let start = Point::new(row_range.start, 0);
303 let end = Point::new(row_range.end, buffer.line_len(row_range.end));
304 RelatedExcerpt {
305 row_range,
306 text: buffer.text_for_range(start..end).collect::<String>().into(),
307 }
308 })
309 .collect();
310
311 let output = format_excerpts(buffer, &excerpts);
312 assert_eq!(output, expected_output);
313 });
314 }
315}
316
317#[gpui::test]
318async fn test_fake_definition_lsp(cx: &mut TestAppContext) {
319 init_test(cx);
320
321 let fs = FakeFs::new(cx.executor());
322 fs.insert_tree(path!("/root"), test_project_1()).await;
323
324 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
325 let mut servers = setup_fake_lsp(&project, cx);
326
327 let (buffer, _handle) = project
328 .update(cx, |project, cx| {
329 project.open_local_buffer_with_lsp(path!("/root/src/main.rs"), cx)
330 })
331 .await
332 .unwrap();
333
334 let _server = servers.next().await.unwrap();
335 cx.run_until_parked();
336
337 let buffer_text = buffer.read_with(cx, |buffer, _| buffer.text());
338
339 let definitions = project
340 .update(cx, |project, cx| {
341 let offset = buffer_text.find("Address {").unwrap();
342 project.definitions(&buffer, offset, cx)
343 })
344 .await
345 .unwrap()
346 .unwrap();
347 assert_definitions(&definitions, &["pub struct Address {"], cx);
348
349 let definitions = project
350 .update(cx, |project, cx| {
351 let offset = buffer_text.find("State::CA").unwrap();
352 project.definitions(&buffer, offset, cx)
353 })
354 .await
355 .unwrap()
356 .unwrap();
357 assert_definitions(&definitions, &["pub enum State {"], cx);
358
359 let definitions = project
360 .update(cx, |project, cx| {
361 let offset = buffer_text.find("to_string()").unwrap();
362 project.definitions(&buffer, offset, cx)
363 })
364 .await
365 .unwrap()
366 .unwrap();
367 assert_definitions(&definitions, &["pub fn to_string(&self) -> String {"], cx);
368}
369
370#[gpui::test]
371async fn test_fake_type_definition_lsp(cx: &mut TestAppContext) {
372 init_test(cx);
373
374 let fs = FakeFs::new(cx.executor());
375 fs.insert_tree(path!("/root"), test_project_1()).await;
376
377 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
378 let mut servers = setup_fake_lsp(&project, cx);
379
380 let (buffer, _handle) = project
381 .update(cx, |project, cx| {
382 project.open_local_buffer_with_lsp(path!("/root/src/main.rs"), cx)
383 })
384 .await
385 .unwrap();
386
387 let _server = servers.next().await.unwrap();
388 cx.run_until_parked();
389
390 let buffer_text = buffer.read_with(cx, |buffer, _| buffer.text());
391
392 // Type definition on a type name returns its own definition
393 // (same as regular definition)
394 let type_defs = project
395 .update(cx, |project, cx| {
396 let offset = buffer_text.find("Address {").expect("Address { not found");
397 project.type_definitions(&buffer, offset, cx)
398 })
399 .await
400 .unwrap()
401 .unwrap();
402 assert_definitions(&type_defs, &["pub struct Address {"], cx);
403
404 // Type definition on a field resolves through the type annotation.
405 // company.rs has `owner: Arc<Person>`, so type-def of `owner` → Person.
406 let (company_buffer, _handle) = project
407 .update(cx, |project, cx| {
408 project.open_local_buffer_with_lsp(path!("/root/src/company.rs"), cx)
409 })
410 .await
411 .unwrap();
412 cx.run_until_parked();
413
414 let company_text = company_buffer.read_with(cx, |buffer, _| buffer.text());
415 let type_defs = project
416 .update(cx, |project, cx| {
417 let offset = company_text.find("owner").expect("owner not found");
418 project.type_definitions(&company_buffer, offset, cx)
419 })
420 .await
421 .unwrap()
422 .unwrap();
423 assert_definitions(&type_defs, &["pub struct Person {"], cx);
424
425 // Type definition on another field: `address: Address` → Address.
426 let type_defs = project
427 .update(cx, |project, cx| {
428 let offset = company_text.find("address").expect("address not found");
429 project.type_definitions(&company_buffer, offset, cx)
430 })
431 .await
432 .unwrap()
433 .unwrap();
434 assert_definitions(&type_defs, &["pub struct Address {"], cx);
435
436 // Type definition on a lowercase name with no type annotation returns empty.
437 let type_defs = project
438 .update(cx, |project, cx| {
439 let offset = buffer_text.find("main").expect("main not found");
440 project.type_definitions(&buffer, offset, cx)
441 })
442 .await;
443 let is_empty = match &type_defs {
444 Ok(Some(defs)) => defs.is_empty(),
445 Ok(None) => true,
446 Err(_) => false,
447 };
448 assert!(is_empty, "expected no type definitions for `main`");
449}
450
451#[gpui::test]
452async fn test_type_definitions_in_related_files(cx: &mut TestAppContext) {
453 init_test(cx);
454 let fs = FakeFs::new(cx.executor());
455 fs.insert_tree(
456 path!("/root"),
457 json!({
458 "src": {
459 "config.rs": indoc! {r#"
460 pub struct Config {
461 debug: bool,
462 verbose: bool,
463 }
464 "#},
465 "widget.rs": indoc! {r#"
466 use super::config::Config;
467
468 pub struct Widget {
469 config: Config,
470 name: String,
471 }
472
473 impl Widget {
474 pub fn render(&self) {
475 if self.config.debug {
476 println!("debug mode");
477 }
478 }
479 }
480 "#},
481 },
482 }),
483 )
484 .await;
485
486 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
487 let mut servers = setup_fake_lsp(&project, cx);
488
489 let (buffer, _handle) = project
490 .update(cx, |project, cx| {
491 project.open_local_buffer_with_lsp(path!("/root/src/widget.rs"), cx)
492 })
493 .await
494 .unwrap();
495
496 let _server = servers.next().await.unwrap();
497 cx.run_until_parked();
498
499 let related_excerpt_store = cx.new(|cx| RelatedExcerptStore::new(&project, cx));
500 related_excerpt_store.update(cx, |store, cx| {
501 let position = {
502 let buffer = buffer.read(cx);
503 let offset = buffer
504 .text()
505 .find("self.config.debug")
506 .expect("self.config.debug not found");
507 buffer.anchor_before(offset)
508 };
509
510 store.set_identifier_line_count(0);
511 store.refresh(buffer.clone(), position, cx);
512 });
513
514 cx.executor().advance_clock(DEBOUNCE_DURATION);
515 // config.rs appears ONLY because the fake LSP resolves the type annotation
516 // `config: Config` to `pub struct Config` via GotoTypeDefinition.
517 // widget.rs appears from regular definitions of Widget / render.
518 related_excerpt_store.update(cx, |store, cx| {
519 let excerpts = store.related_files(cx);
520 assert_related_files(
521 &excerpts,
522 &[
523 (
524 "root/src/config.rs",
525 &[indoc! {"
526 pub struct Config {
527 debug: bool,
528 verbose: bool,
529 }"}],
530 ),
531 (
532 "root/src/widget.rs",
533 &[
534 indoc! {"
535 pub struct Widget {
536 config: Config,
537 name: String,
538 }
539
540 impl Widget {
541 pub fn render(&self) {"},
542 indoc! {"
543 }
544 }"},
545 ],
546 ),
547 ],
548 );
549 });
550}
551
552#[gpui::test]
553async fn test_type_definition_deduplication(cx: &mut TestAppContext) {
554 init_test(cx);
555 let fs = FakeFs::new(cx.executor());
556
557 // In this project the only identifier near the cursor whose type definition
558 // resolves is `TypeA`, and its GotoTypeDefinition returns the exact same
559 // location as GotoDefinition. After deduplication the CacheEntry for `TypeA`
560 // should have an empty `type_definitions` vec, meaning the type-definition
561 // path contributes nothing extra to the related-file output.
562 fs.insert_tree(
563 path!("/root"),
564 json!({
565 "src": {
566 "types.rs": indoc! {r#"
567 pub struct TypeA {
568 value: i32,
569 }
570
571 pub struct TypeB {
572 label: String,
573 }
574 "#},
575 "main.rs": indoc! {r#"
576 use super::types::TypeA;
577
578 fn work() {
579 let item: TypeA = unimplemented!();
580 println!("{}", item.value);
581 }
582 "#},
583 },
584 }),
585 )
586 .await;
587
588 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
589 let mut servers = setup_fake_lsp(&project, cx);
590
591 let (buffer, _handle) = project
592 .update(cx, |project, cx| {
593 project.open_local_buffer_with_lsp(path!("/root/src/main.rs"), cx)
594 })
595 .await
596 .unwrap();
597
598 let _server = servers.next().await.unwrap();
599 cx.run_until_parked();
600
601 let related_excerpt_store = cx.new(|cx| RelatedExcerptStore::new(&project, cx));
602 related_excerpt_store.update(cx, |store, cx| {
603 let position = {
604 let buffer = buffer.read(cx);
605 let offset = buffer.text().find("let item").expect("let item not found");
606 buffer.anchor_before(offset)
607 };
608
609 store.set_identifier_line_count(0);
610 store.refresh(buffer.clone(), position, cx);
611 });
612
613 cx.executor().advance_clock(DEBOUNCE_DURATION);
614 // types.rs appears because `TypeA` has a regular definition there.
615 // `item`'s type definition also resolves to TypeA in types.rs, but
616 // deduplication removes it since it points to the same location.
617 // TypeB should NOT appear because nothing references it.
618 related_excerpt_store.update(cx, |store, cx| {
619 let excerpts = store.related_files(cx);
620 assert_related_files(
621 &excerpts,
622 &[
623 ("root/src/main.rs", &["fn work() {", "}"]),
624 (
625 "root/src/types.rs",
626 &[indoc! {"
627 pub struct TypeA {
628 value: i32,
629 }"}],
630 ),
631 ],
632 );
633 });
634}
635
636fn init_test(cx: &mut TestAppContext) {
637 let settings_store = cx.update(|cx| SettingsStore::test(cx));
638 cx.set_global(settings_store);
639 env_logger::try_init().ok();
640}
641
642fn setup_fake_lsp(
643 project: &Entity<Project>,
644 cx: &mut TestAppContext,
645) -> UnboundedReceiver<FakeLanguageServer> {
646 let (language_registry, fs) = project.read_with(cx, |project, _| {
647 (project.languages().clone(), project.fs().clone())
648 });
649 let language = rust_lang();
650 language_registry.add(language.clone());
651 fake_definition_lsp::register_fake_definition_server(&language_registry, language, fs)
652}
653
654fn test_project_1() -> serde_json::Value {
655 let person_rs = indoc! {r#"
656 pub struct Person {
657 first_name: String,
658 last_name: String,
659 email: String,
660 age: u32,
661 }
662
663 impl Person {
664 pub fn get_first_name(&self) -> &str {
665 &self.first_name
666 }
667
668 pub fn get_last_name(&self) -> &str {
669 &self.last_name
670 }
671
672 pub fn get_email(&self) -> &str {
673 &self.email
674 }
675
676 pub fn get_age(&self) -> u32 {
677 self.age
678 }
679 }
680 "#};
681
682 let address_rs = indoc! {r#"
683 pub struct Address {
684 street: String,
685 city: String,
686 state: State,
687 zip: u32,
688 }
689
690 pub enum State {
691 CA,
692 OR,
693 WA,
694 TX,
695 // ...
696 }
697
698 impl Address {
699 pub fn get_street(&self) -> &str {
700 &self.street
701 }
702
703 pub fn get_city(&self) -> &str {
704 &self.city
705 }
706
707 pub fn get_state(&self) -> State {
708 self.state
709 }
710
711 pub fn get_zip(&self) -> u32 {
712 self.zip
713 }
714 }
715 "#};
716
717 let company_rs = indoc! {r#"
718 use super::person::Person;
719 use super::address::Address;
720
721 pub struct Company {
722 owner: Arc<Person>,
723 address: Address,
724 }
725
726 impl Company {
727 pub fn get_owner(&self) -> &Person {
728 &self.owner
729 }
730
731 pub fn get_address(&self) -> &Address {
732 &self.address
733 }
734
735 pub fn to_string(&self) -> String {
736 format!("{} ({})", self.owner.first_name, self.address.city)
737 }
738 }
739 "#};
740
741 let main_rs = indoc! {r#"
742 use std::sync::Arc;
743 use super::person::Person;
744 use super::address::Address;
745 use super::company::Company;
746
747 pub struct Session {
748 company: Arc<Company>,
749 }
750
751 impl Session {
752 pub fn set_company(&mut self, company: Arc<Company>) {
753 self.company = company;
754 if company.owner != self.company.owner {
755 log("new owner", company.owner.get_first_name()); todo();
756 }
757 }
758 }
759
760 fn main() {
761 let company = Company {
762 owner: Arc::new(Person {
763 first_name: "John".to_string(),
764 last_name: "Doe".to_string(),
765 email: "john@example.com".to_string(),
766 age: 30,
767 }),
768 address: Address {
769 street: "123 Main St".to_string(),
770 city: "Anytown".to_string(),
771 state: State::CA,
772 zip: 12345,
773 },
774 };
775
776 println!("Company: {}", company.to_string());
777 }
778 "#};
779
780 json!({
781 "src": {
782 "person.rs": person_rs,
783 "address.rs": address_rs,
784 "company.rs": company_rs,
785 "main.rs": main_rs,
786 },
787 })
788}
789
790fn assert_related_files(actual_files: &[RelatedFile], expected_files: &[(&str, &[&str])]) {
791 let actual_files = actual_files
792 .iter()
793 .map(|file| {
794 let excerpts = file
795 .excerpts
796 .iter()
797 .map(|excerpt| excerpt.text.to_string())
798 .collect::<Vec<_>>();
799 (file.path.to_str().unwrap(), excerpts)
800 })
801 .collect::<Vec<_>>();
802 let expected_excerpts = expected_files
803 .iter()
804 .map(|(path, texts)| {
805 (
806 *path,
807 texts
808 .iter()
809 .map(|line| line.to_string())
810 .collect::<Vec<_>>(),
811 )
812 })
813 .collect::<Vec<_>>();
814 pretty_assertions::assert_eq!(actual_files, expected_excerpts)
815}
816
817fn assert_definitions(definitions: &[LocationLink], first_lines: &[&str], cx: &mut TestAppContext) {
818 let actual_first_lines = definitions
819 .iter()
820 .map(|definition| {
821 definition.target.buffer.read_with(cx, |buffer, _| {
822 let mut start = definition.target.range.start.to_point(&buffer);
823 start.column = 0;
824 let end = Point::new(start.row, buffer.line_len(start.row));
825 buffer
826 .text_for_range(start..end)
827 .collect::<String>()
828 .trim()
829 .to_string()
830 })
831 })
832 .collect::<Vec<String>>();
833
834 assert_eq!(actual_first_lines, first_lines);
835}
836
837fn format_excerpts(buffer: &Buffer, excerpts: &[RelatedExcerpt]) -> String {
838 let mut output = String::new();
839 let file_line_count = buffer.max_point().row;
840 let mut current_row = 0;
841 for excerpt in excerpts {
842 if excerpt.text.is_empty() {
843 continue;
844 }
845 if current_row < excerpt.row_range.start {
846 writeln!(&mut output, "…").unwrap();
847 }
848 current_row = excerpt.row_range.start;
849
850 for line in excerpt.text.to_string().lines() {
851 output.push_str(line);
852 output.push('\n');
853 current_row += 1;
854 }
855 }
856 if current_row < file_line_count {
857 writeln!(&mut output, "…").unwrap();
858 }
859 output
860}