diagnostics.rs

  1use anyhow::Result;
  2use collections::{HashMap, HashSet};
  3use editor::{
  4    context_header_renderer, diagnostic_block_renderer, diagnostic_header_renderer,
  5    display_map::{BlockDisposition, BlockId, BlockProperties},
  6    BuildSettings, Editor, ExcerptId, ExcerptProperties, MultiBuffer,
  7};
  8use gpui::{
  9    action, elements::*, keymap::Binding, AppContext, Entity, ModelHandle, MutableAppContext,
 10    RenderContext, Task, View, ViewContext, ViewHandle,
 11};
 12use language::{Bias, Buffer, Diagnostic, DiagnosticEntry, Point};
 13use postage::watch;
 14use project::Project;
 15use std::{cmp::Ordering, ops::Range, path::Path, sync::Arc};
 16use util::TryFutureExt;
 17use workspace::Workspace;
 18
 19action!(Toggle);
 20action!(ClearInvalid);
 21
 22const CONTEXT_LINE_COUNT: u32 = 1;
 23
 24pub fn init(cx: &mut MutableAppContext) {
 25    cx.add_bindings([
 26        Binding::new("alt-shift-D", Toggle, None),
 27        Binding::new(
 28            "alt-shift-C",
 29            ClearInvalid,
 30            Some("ProjectDiagnosticsEditor"),
 31        ),
 32    ]);
 33    cx.add_action(ProjectDiagnosticsEditor::toggle);
 34    cx.add_action(ProjectDiagnosticsEditor::clear_invalid);
 35}
 36
 37type Event = editor::Event;
 38
 39struct ProjectDiagnostics {
 40    project: ModelHandle<Project>,
 41}
 42
 43struct ProjectDiagnosticsEditor {
 44    editor: ViewHandle<Editor>,
 45    excerpts: ModelHandle<MultiBuffer>,
 46    path_states: Vec<(Arc<Path>, Vec<DiagnosticGroupState>)>,
 47    build_settings: BuildSettings,
 48}
 49
 50struct DiagnosticGroupState {
 51    primary_diagnostic: DiagnosticEntry<language::Anchor>,
 52    excerpts: Vec<ExcerptId>,
 53    blocks: HashMap<BlockId, DiagnosticBlock>,
 54    block_count: usize,
 55    is_valid: bool,
 56}
 57
 58enum DiagnosticBlock {
 59    Header(Diagnostic),
 60    Inline(Diagnostic),
 61    Context,
 62}
 63
 64impl ProjectDiagnostics {
 65    fn new(project: ModelHandle<Project>) -> Self {
 66        Self { project }
 67    }
 68}
 69
 70impl Entity for ProjectDiagnostics {
 71    type Event = ();
 72}
 73
 74impl Entity for ProjectDiagnosticsEditor {
 75    type Event = Event;
 76}
 77
 78impl View for ProjectDiagnosticsEditor {
 79    fn ui_name() -> &'static str {
 80        "ProjectDiagnosticsEditor"
 81    }
 82
 83    fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
 84        ChildView::new(self.editor.id()).boxed()
 85    }
 86
 87    fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
 88        cx.focus(&self.editor);
 89    }
 90}
 91
 92impl ProjectDiagnosticsEditor {
 93    fn new(
 94        project: ModelHandle<Project>,
 95        settings: watch::Receiver<workspace::Settings>,
 96        cx: &mut ViewContext<Self>,
 97    ) -> Self {
 98        let project_paths = project
 99            .read(cx)
100            .diagnostic_summaries(cx)
101            .map(|e| e.0)
102            .collect::<Vec<_>>();
103
104        cx.spawn(|this, mut cx| {
105            let project = project.clone();
106            async move {
107                for project_path in project_paths {
108                    let buffer = project
109                        .update(&mut cx, |project, cx| project.open_buffer(project_path, cx))
110                        .await?;
111                    this.update(&mut cx, |view, cx| view.populate_excerpts(buffer, cx))
112                }
113                Result::<_, anyhow::Error>::Ok(())
114            }
115        })
116        .detach();
117
118        cx.subscribe(&project, |_, project, event, cx| {
119            if let project::Event::DiagnosticsUpdated(project_path) = event {
120                let project_path = project_path.clone();
121                cx.spawn(|this, mut cx| {
122                    async move {
123                        let buffer = project
124                            .update(&mut cx, |project, cx| project.open_buffer(project_path, cx))
125                            .await?;
126                        this.update(&mut cx, |view, cx| view.populate_excerpts(buffer, cx));
127                        Ok(())
128                    }
129                    .log_err()
130                })
131                .detach();
132            }
133        })
134        .detach();
135
136        let excerpts = cx.add_model(|cx| MultiBuffer::new(project.read(cx).replica_id()));
137        let build_settings = editor::settings_builder(excerpts.downgrade(), settings.clone());
138        let editor =
139            cx.add_view(|cx| Editor::for_buffer(excerpts.clone(), build_settings.clone(), cx));
140        cx.subscribe(&editor, |_, _, event, cx| cx.emit(*event))
141            .detach();
142        Self {
143            excerpts,
144            editor,
145            build_settings,
146            path_states: Default::default(),
147        }
148    }
149
150    #[cfg(test)]
151    fn text(&self, cx: &AppContext) -> String {
152        self.editor.read(cx).text(cx)
153    }
154
155    fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
156        let diagnostics = cx.add_model(|_| ProjectDiagnostics::new(workspace.project().clone()));
157        workspace.add_item(diagnostics, cx);
158    }
159
160    fn clear_invalid(&mut self, _: &ClearInvalid, cx: &mut ViewContext<Self>) {
161        let mut blocks_to_delete = HashSet::default();
162        let mut excerpts_to_delete = Vec::new();
163        let mut path_ixs_to_delete = Vec::new();
164        for (ix, (_, groups)) in self.path_states.iter_mut().enumerate() {
165            groups.retain(|group| {
166                if group.is_valid {
167                    true
168                } else {
169                    blocks_to_delete.extend(group.blocks.keys().copied());
170                    excerpts_to_delete.extend(group.excerpts.iter().cloned());
171                    false
172                }
173            });
174
175            if groups.is_empty() {
176                path_ixs_to_delete.push(ix);
177            }
178        }
179
180        for ix in path_ixs_to_delete.into_iter().rev() {
181            self.path_states.remove(ix);
182        }
183
184        self.excerpts.update(cx, |excerpts, cx| {
185            excerpts_to_delete.sort_unstable();
186            excerpts.remove_excerpts(&excerpts_to_delete, cx)
187        });
188        self.editor
189            .update(cx, |editor, cx| editor.remove_blocks(blocks_to_delete, cx));
190    }
191
192    fn populate_excerpts(&mut self, buffer: ModelHandle<Buffer>, cx: &mut ViewContext<Self>) {
193        let snapshot;
194        let path;
195        {
196            let buffer = buffer.read(cx);
197            snapshot = buffer.snapshot();
198            if let Some(file) = buffer.file() {
199                path = file.path().clone();
200            } else {
201                return;
202            }
203        }
204
205        let path_ix = match self
206            .path_states
207            .binary_search_by_key(&path.as_ref(), |e| e.0.as_ref())
208        {
209            Ok(ix) => ix,
210            Err(ix) => {
211                self.path_states
212                    .insert(ix, (path.clone(), Default::default()));
213                ix
214            }
215        };
216
217        let mut prev_excerpt_id = if path_ix > 0 {
218            let prev_path_last_group = &self.path_states[path_ix - 1].1.last().unwrap();
219            prev_path_last_group.excerpts.last().unwrap().clone()
220        } else {
221            ExcerptId::min()
222        };
223
224        let groups = &mut self.path_states[path_ix].1;
225        let mut groups_to_add = Vec::new();
226        let mut group_ixs_to_remove = Vec::new();
227        let mut blocks_to_add = Vec::new();
228        let mut blocks_to_restyle = HashMap::default();
229        let mut blocks_to_remove = HashSet::default();
230        let selected_excerpts = self
231            .editor
232            .read(cx)
233            .local_anchor_selections()
234            .iter()
235            .flat_map(|s| [s.start.excerpt_id().clone(), s.end.excerpt_id().clone()])
236            .collect::<HashSet<_>>();
237        let mut diagnostic_blocks = Vec::new();
238        let excerpts_snapshot = self.excerpts.update(cx, |excerpts, excerpts_cx| {
239            let mut old_groups = groups.iter_mut().enumerate().peekable();
240            let mut new_groups = snapshot.diagnostic_groups().into_iter().peekable();
241
242            loop {
243                let mut to_insert = None;
244                let mut to_invalidate = None;
245                let mut to_validate = None;
246                match (old_groups.peek(), new_groups.peek()) {
247                    (None, None) => break,
248                    (None, Some(_)) => to_insert = new_groups.next(),
249                    (Some(_), None) => to_invalidate = old_groups.next(),
250                    (Some((_, old_group)), Some(new_group)) => {
251                        let old_primary = &old_group.primary_diagnostic;
252                        let new_primary = &new_group.entries[new_group.primary_ix];
253                        match compare_diagnostics(old_primary, new_primary, &snapshot) {
254                            Ordering::Less => to_invalidate = old_groups.next(),
255                            Ordering::Equal => {
256                                to_validate = old_groups.next();
257                                new_groups.next();
258                            }
259                            Ordering::Greater => to_insert = new_groups.next(),
260                        }
261                    }
262                }
263
264                if let Some(group) = to_insert {
265                    let mut group_state = DiagnosticGroupState {
266                        primary_diagnostic: group.entries[group.primary_ix].clone(),
267                        excerpts: Default::default(),
268                        blocks: Default::default(),
269                        block_count: 0,
270                        is_valid: true,
271                    };
272                    let mut pending_range: Option<(Range<Point>, usize)> = None;
273                    let mut is_first_excerpt_for_group = true;
274                    for (ix, entry) in group.entries.iter().map(Some).chain([None]).enumerate() {
275                        let resolved_entry = entry.map(|e| e.resolve::<Point>(&snapshot));
276                        if let Some((range, start_ix)) = &mut pending_range {
277                            if let Some(entry) = resolved_entry.as_ref() {
278                                if entry.range.start.row
279                                    <= range.end.row + 1 + CONTEXT_LINE_COUNT * 2
280                                {
281                                    range.end = range.end.max(entry.range.end);
282                                    continue;
283                                }
284                            }
285
286                            let excerpt_start =
287                                Point::new(range.start.row.saturating_sub(CONTEXT_LINE_COUNT), 0);
288                            let excerpt_end = snapshot.clip_point(
289                                Point::new(range.end.row + CONTEXT_LINE_COUNT, u32::MAX),
290                                Bias::Left,
291                            );
292                            let excerpt_id = excerpts.insert_excerpt_after(
293                                &prev_excerpt_id,
294                                ExcerptProperties {
295                                    buffer: &buffer,
296                                    range: excerpt_start..excerpt_end,
297                                },
298                                excerpts_cx,
299                            );
300
301                            prev_excerpt_id = excerpt_id.clone();
302                            group_state.excerpts.push(excerpt_id.clone());
303                            let header_position = (excerpt_id.clone(), language::Anchor::min());
304
305                            if is_first_excerpt_for_group {
306                                is_first_excerpt_for_group = false;
307                                let primary = &group.entries[group.primary_ix].diagnostic;
308                                let mut header = primary.clone();
309                                header.message =
310                                    primary.message.split('\n').next().unwrap().to_string();
311                                group_state.block_count += 1;
312                                diagnostic_blocks.push(DiagnosticBlock::Header(header.clone()));
313                                blocks_to_add.push(BlockProperties {
314                                    position: header_position,
315                                    height: 2,
316                                    render: diagnostic_header_renderer(
317                                        buffer.clone(),
318                                        header,
319                                        true,
320                                        self.build_settings.clone(),
321                                    ),
322                                    disposition: BlockDisposition::Above,
323                                });
324                            } else {
325                                group_state.block_count += 1;
326                                diagnostic_blocks.push(DiagnosticBlock::Context);
327                                blocks_to_add.push(BlockProperties {
328                                    position: header_position,
329                                    height: 1,
330                                    render: context_header_renderer(self.build_settings.clone()),
331                                    disposition: BlockDisposition::Above,
332                                });
333                            }
334
335                            for entry in &group.entries[*start_ix..ix] {
336                                let mut diagnostic = entry.diagnostic.clone();
337                                if diagnostic.is_primary {
338                                    let mut lines = entry.diagnostic.message.split('\n');
339                                    lines.next();
340                                    diagnostic.message = lines.collect();
341                                }
342
343                                if !diagnostic.message.is_empty() {
344                                    group_state.block_count += 1;
345                                    diagnostic_blocks
346                                        .push(DiagnosticBlock::Inline(diagnostic.clone()));
347                                    blocks_to_add.push(BlockProperties {
348                                        position: (excerpt_id.clone(), entry.range.start.clone()),
349                                        height: diagnostic.message.matches('\n').count() as u8 + 1,
350                                        render: diagnostic_block_renderer(
351                                            diagnostic,
352                                            true,
353                                            self.build_settings.clone(),
354                                        ),
355                                        disposition: BlockDisposition::Below,
356                                    });
357                                }
358                            }
359
360                            pending_range.take();
361                        }
362
363                        if let Some(entry) = resolved_entry {
364                            pending_range = Some((entry.range.clone(), ix));
365                        }
366                    }
367
368                    groups_to_add.push(group_state);
369                } else if let Some((group_ix, group_state)) = to_invalidate {
370                    if group_state
371                        .excerpts
372                        .iter()
373                        .any(|excerpt_id| selected_excerpts.contains(excerpt_id))
374                    {
375                        for (block_id, block) in &group_state.blocks {
376                            match block {
377                                DiagnosticBlock::Header(diagnostic) => {
378                                    blocks_to_restyle.insert(
379                                        *block_id,
380                                        diagnostic_header_renderer(
381                                            buffer.clone(),
382                                            diagnostic.clone(),
383                                            false,
384                                            self.build_settings.clone(),
385                                        ),
386                                    );
387                                }
388                                DiagnosticBlock::Inline(diagnostic) => {
389                                    blocks_to_restyle.insert(
390                                        *block_id,
391                                        diagnostic_block_renderer(
392                                            diagnostic.clone(),
393                                            false,
394                                            self.build_settings.clone(),
395                                        ),
396                                    );
397                                }
398                                DiagnosticBlock::Context => {}
399                            }
400                        }
401
402                        group_state.is_valid = false;
403                        prev_excerpt_id = group_state.excerpts.last().unwrap().clone();
404                    } else {
405                        excerpts.remove_excerpts(group_state.excerpts.iter(), excerpts_cx);
406                        group_ixs_to_remove.push(group_ix);
407                        blocks_to_remove.extend(group_state.blocks.keys().copied());
408                    }
409                } else if let Some((_, group_state)) = to_validate {
410                    for (block_id, block) in &group_state.blocks {
411                        match block {
412                            DiagnosticBlock::Header(diagnostic) => {
413                                blocks_to_restyle.insert(
414                                    *block_id,
415                                    diagnostic_header_renderer(
416                                        buffer.clone(),
417                                        diagnostic.clone(),
418                                        true,
419                                        self.build_settings.clone(),
420                                    ),
421                                );
422                            }
423                            DiagnosticBlock::Inline(diagnostic) => {
424                                blocks_to_restyle.insert(
425                                    *block_id,
426                                    diagnostic_block_renderer(
427                                        diagnostic.clone(),
428                                        true,
429                                        self.build_settings.clone(),
430                                    ),
431                                );
432                            }
433                            DiagnosticBlock::Context => {}
434                        }
435                    }
436                    group_state.is_valid = true;
437                    prev_excerpt_id = group_state.excerpts.last().unwrap().clone();
438                } else {
439                    unreachable!();
440                }
441            }
442
443            excerpts.snapshot(excerpts_cx)
444        });
445
446        self.editor.update(cx, |editor, cx| {
447            editor.remove_blocks(blocks_to_remove, cx);
448            editor.replace_blocks(blocks_to_restyle, cx);
449            let mut block_ids = editor
450                .insert_blocks(
451                    blocks_to_add.into_iter().map(|block| {
452                        let (excerpt_id, text_anchor) = block.position;
453                        BlockProperties {
454                            position: excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor),
455                            height: block.height,
456                            render: block.render,
457                            disposition: block.disposition,
458                        }
459                    }),
460                    cx,
461                )
462                .into_iter()
463                .zip(diagnostic_blocks);
464
465            for group_state in &mut groups_to_add {
466                group_state.blocks = block_ids.by_ref().take(group_state.block_count).collect();
467            }
468        });
469
470        for ix in group_ixs_to_remove.into_iter().rev() {
471            groups.remove(ix);
472        }
473        groups.extend(groups_to_add);
474        groups.sort_unstable_by(|a, b| {
475            let range_a = &a.primary_diagnostic.range;
476            let range_b = &b.primary_diagnostic.range;
477            range_a
478                .start
479                .cmp(&range_b.start, &snapshot)
480                .unwrap()
481                .then_with(|| range_a.end.cmp(&range_b.end, &snapshot).unwrap())
482        });
483
484        if groups.is_empty() {
485            self.path_states.remove(path_ix);
486        }
487
488        cx.notify();
489    }
490}
491
492impl workspace::Item for ProjectDiagnostics {
493    type View = ProjectDiagnosticsEditor;
494
495    fn build_view(
496        handle: ModelHandle<Self>,
497        settings: watch::Receiver<workspace::Settings>,
498        cx: &mut ViewContext<Self::View>,
499    ) -> Self::View {
500        let project = handle.read(cx).project.clone();
501        ProjectDiagnosticsEditor::new(project, settings, cx)
502    }
503
504    fn project_path(&self) -> Option<project::ProjectPath> {
505        None
506    }
507}
508
509impl workspace::ItemView for ProjectDiagnosticsEditor {
510    fn title(&self, _: &AppContext) -> String {
511        "Project Diagnostics".to_string()
512    }
513
514    fn project_path(&self, _: &AppContext) -> Option<project::ProjectPath> {
515        None
516    }
517
518    fn save(&mut self, cx: &mut ViewContext<Self>) -> Result<Task<Result<()>>> {
519        self.excerpts.update(cx, |excerpts, cx| excerpts.save(cx))
520    }
521
522    fn save_as(
523        &mut self,
524        _: ModelHandle<project::Worktree>,
525        _: &std::path::Path,
526        _: &mut ViewContext<Self>,
527    ) -> Task<Result<()>> {
528        unreachable!()
529    }
530
531    fn is_dirty(&self, cx: &AppContext) -> bool {
532        self.excerpts.read(cx).read(cx).is_dirty()
533    }
534
535    fn has_conflict(&self, cx: &AppContext) -> bool {
536        self.excerpts.read(cx).read(cx).has_conflict()
537    }
538
539    fn should_update_tab_on_event(event: &Event) -> bool {
540        matches!(
541            event,
542            Event::Saved | Event::Dirtied | Event::FileHandleChanged
543        )
544    }
545
546    fn can_save(&self, _: &AppContext) -> bool {
547        true
548    }
549
550    fn can_save_as(&self, _: &AppContext) -> bool {
551        false
552    }
553}
554
555fn compare_diagnostics<L: language::ToOffset, R: language::ToOffset>(
556    lhs: &DiagnosticEntry<L>,
557    rhs: &DiagnosticEntry<R>,
558    snapshot: &language::BufferSnapshot,
559) -> Ordering {
560    lhs.range
561        .start
562        .to_offset(&snapshot)
563        .cmp(&rhs.range.start.to_offset(snapshot))
564        .then_with(|| {
565            lhs.range
566                .end
567                .to_offset(&snapshot)
568                .cmp(&rhs.range.end.to_offset(snapshot))
569        })
570        .then_with(|| lhs.diagnostic.message.cmp(&rhs.diagnostic.message))
571}
572
573#[cfg(test)]
574mod tests {
575    use super::*;
576    use client::{http::ServerResponse, test::FakeHttpClient, Client, UserStore};
577    use gpui::TestAppContext;
578    use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, LanguageRegistry, PointUtf16};
579    use project::FakeFs;
580    use serde_json::json;
581    use std::sync::Arc;
582    use unindent::Unindent as _;
583    use workspace::WorkspaceParams;
584
585    #[gpui::test]
586    async fn test_diagnostics(mut cx: TestAppContext) {
587        let settings = cx.update(WorkspaceParams::test).settings;
588        let http_client = FakeHttpClient::new(|_| async move { Ok(ServerResponse::new(404)) });
589        let client = Client::new();
590        let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
591        let fs = Arc::new(FakeFs::new());
592
593        let project = cx.update(|cx| {
594            Project::local(
595                client.clone(),
596                user_store,
597                Arc::new(LanguageRegistry::new()),
598                fs.clone(),
599                cx,
600            )
601        });
602
603        fs.insert_tree(
604            "/test",
605            json!({
606                "a.rs": "
607                    const a: i32 = 'a';
608                ".unindent(),
609
610                "main.rs": "
611                    fn main() {
612                        let x = vec![];
613                        let y = vec![];
614                        a(x);
615                        b(y);
616                        // comment 1
617                        // comment 2
618                        c(y);
619                        d(x);
620                    }
621                "
622                .unindent(),
623            }),
624        )
625        .await;
626
627        let worktree = project
628            .update(&mut cx, |project, cx| {
629                project.add_local_worktree("/test", cx)
630            })
631            .await
632            .unwrap();
633
634        worktree.update(&mut cx, |worktree, cx| {
635            worktree
636                .update_diagnostic_entries(
637                    Arc::from("/test/main.rs".as_ref()),
638                    None,
639                    vec![
640                        DiagnosticEntry {
641                            range: PointUtf16::new(1, 8)..PointUtf16::new(1, 9),
642                            diagnostic: Diagnostic {
643                                message:
644                                    "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
645                                        .to_string(),
646                                severity: DiagnosticSeverity::INFORMATION,
647                                is_primary: false,
648                                group_id: 1,
649                                ..Default::default()
650                            },
651                        },
652                        DiagnosticEntry {
653                            range: PointUtf16::new(2, 8)..PointUtf16::new(2, 9),
654                            diagnostic: Diagnostic {
655                                message:
656                                    "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
657                                        .to_string(),
658                                severity: DiagnosticSeverity::INFORMATION,
659                                is_primary: false,
660                                group_id: 0,
661                                ..Default::default()
662                            },
663                        },
664                        DiagnosticEntry {
665                            range: PointUtf16::new(3, 6)..PointUtf16::new(3, 7),
666                            diagnostic: Diagnostic {
667                                message: "value moved here".to_string(),
668                                severity: DiagnosticSeverity::INFORMATION,
669                                is_primary: false,
670                                group_id: 1,
671                                ..Default::default()
672                            },
673                        },
674                        DiagnosticEntry {
675                            range: PointUtf16::new(4, 6)..PointUtf16::new(4, 7),
676                            diagnostic: Diagnostic {
677                                message: "value moved here".to_string(),
678                                severity: DiagnosticSeverity::INFORMATION,
679                                is_primary: false,
680                                group_id: 0,
681                                ..Default::default()
682                            },
683                        },
684                        DiagnosticEntry {
685                            range: PointUtf16::new(7, 6)..PointUtf16::new(7, 7),
686                            diagnostic: Diagnostic {
687                                message: "use of moved value\nvalue used here after move".to_string(),
688                                severity: DiagnosticSeverity::ERROR,
689                                is_primary: true,
690                                group_id: 0,
691                                ..Default::default()
692                            },
693                        },
694                        DiagnosticEntry {
695                            range: PointUtf16::new(8, 6)..PointUtf16::new(8, 7),
696                            diagnostic: Diagnostic {
697                                message: "use of moved value\nvalue used here after move".to_string(),
698                                severity: DiagnosticSeverity::ERROR,
699                                is_primary: true,
700                                group_id: 1,
701                                ..Default::default()
702                            },
703                        },
704                    ],
705                    cx,
706                )
707                .unwrap();
708        });
709
710        let view = cx.add_view(Default::default(), |cx| {
711            ProjectDiagnosticsEditor::new(project.clone(), settings, cx)
712        });
713
714        view.condition(&mut cx, |view, cx| view.text(cx).contains("fn main()"))
715            .await;
716
717        view.update(&mut cx, |view, cx| {
718            let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
719
720            assert_eq!(
721                editor.text(),
722                concat!(
723                    //
724                    // main.rs, diagnostic group 1
725                    //
726                    "\n", // primary message
727                    "\n", // filename
728                    "    let x = vec![];\n",
729                    "    let y = vec![];\n",
730                    "\n", // supporting diagnostic
731                    "    a(x);\n",
732                    "    b(y);\n",
733                    "\n", // supporting diagnostic
734                    "    // comment 1\n",
735                    "    // comment 2\n",
736                    "    c(y);\n",
737                    "\n", // supporting diagnostic
738                    "    d(x);\n",
739                    //
740                    // main.rs, diagnostic group 2
741                    //
742                    "\n", // primary message
743                    "\n", // filename
744                    "fn main() {\n",
745                    "    let x = vec![];\n",
746                    "\n", // supporting diagnostic
747                    "    let y = vec![];\n",
748                    "    a(x);\n",
749                    "\n", // supporting diagnostic
750                    "    b(y);\n",
751                    "\n", // context ellipsis
752                    "    c(y);\n",
753                    "    d(x);\n",
754                    "\n", // supporting diagnostic
755                    "}"
756                )
757            );
758        });
759
760        worktree.update(&mut cx, |worktree, cx| {
761            worktree
762                .update_diagnostic_entries(
763                    Arc::from("/test/a.rs".as_ref()),
764                    None,
765                    vec![DiagnosticEntry {
766                        range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
767                        diagnostic: Diagnostic {
768                            message: "mismatched types\nexpected `usize`, found `char`".to_string(),
769                            severity: DiagnosticSeverity::ERROR,
770                            is_primary: true,
771                            group_id: 0,
772                            ..Default::default()
773                        },
774                    }],
775                    cx,
776                )
777                .unwrap();
778        });
779
780        view.condition(&mut cx, |view, cx| view.text(cx).contains("const a"))
781            .await;
782
783        view.update(&mut cx, |view, cx| {
784            let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
785
786            assert_eq!(
787                editor.text(),
788                concat!(
789                    //
790                    // a.rs
791                    //
792                    "\n", // primary message
793                    "\n", // filename
794                    "const a: i32 = 'a';\n",
795                    //
796                    // main.rs, diagnostic group 1
797                    //
798                    "\n", // primary message
799                    "\n", // filename
800                    "    let x = vec![];\n",
801                    "    let y = vec![];\n",
802                    "\n", // supporting diagnostic
803                    "    a(x);\n",
804                    "    b(y);\n",
805                    "\n", // supporting diagnostic
806                    "    // comment 1\n",
807                    "    // comment 2\n",
808                    "    c(y);\n",
809                    "\n", // supporting diagnostic
810                    "    d(x);\n",
811                    //
812                    // main.rs, diagnostic group 2
813                    //
814                    "\n", // primary message
815                    "\n", // filename
816                    "fn main() {\n",
817                    "    let x = vec![];\n",
818                    "\n", // supporting diagnostic
819                    "    let y = vec![];\n",
820                    "    a(x);\n",
821                    "\n", // supporting diagnostic
822                    "    b(y);\n",
823                    "\n", // context ellipsis
824                    "    c(y);\n",
825                    "    d(x);\n",
826                    "\n", // supporting diagnostic
827                    "}"
828                )
829            );
830        });
831    }
832}