edit_prediction_context_tests.rs

  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,
189190                }
191
192                impl User {
193194                    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                }
218219            "#},
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> {
245246                    }
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> {
280281                    }
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}