diagnostics.rs

  1pub mod items;
  2
  3use anyhow::Result;
  4use collections::{BTreeSet, HashMap, HashSet};
  5use editor::{
  6    diagnostic_block_renderer, diagnostic_style,
  7    display_map::{BlockDisposition, BlockId, BlockProperties, RenderBlock},
  8    items::BufferItemHandle,
  9    Autoscroll, BuildSettings, Editor, ExcerptId, ExcerptProperties, MultiBuffer, ToOffset,
 10};
 11use gpui::{
 12    action, elements::*, keymap::Binding, AppContext, Entity, ModelHandle, MutableAppContext,
 13    RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle,
 14};
 15use language::{Bias, Buffer, Diagnostic, DiagnosticEntry, Point, Selection, SelectionGoal};
 16use postage::watch;
 17use project::{DiagnosticSummary, Project, ProjectPath, WorktreeId};
 18use std::{cmp::Ordering, mem, ops::Range, sync::Arc};
 19use util::TryFutureExt;
 20use workspace::Workspace;
 21
 22action!(Deploy);
 23action!(OpenExcerpts);
 24
 25const CONTEXT_LINE_COUNT: u32 = 1;
 26
 27pub fn init(cx: &mut MutableAppContext) {
 28    cx.add_bindings([
 29        Binding::new("alt-shift-D", Deploy, Some("Workspace")),
 30        Binding::new(
 31            "alt-shift-D",
 32            OpenExcerpts,
 33            Some("ProjectDiagnosticsEditor"),
 34        ),
 35    ]);
 36    cx.add_action(ProjectDiagnosticsEditor::deploy);
 37    cx.add_action(ProjectDiagnosticsEditor::open_excerpts);
 38}
 39
 40type Event = editor::Event;
 41
 42struct ProjectDiagnostics {
 43    project: ModelHandle<Project>,
 44}
 45
 46struct ProjectDiagnosticsEditor {
 47    model: ModelHandle<ProjectDiagnostics>,
 48    workspace: WeakViewHandle<Workspace>,
 49    editor: ViewHandle<Editor>,
 50    summary: DiagnosticSummary,
 51    excerpts: ModelHandle<MultiBuffer>,
 52    path_states: Vec<PathState>,
 53    paths_to_update: HashMap<WorktreeId, BTreeSet<ProjectPath>>,
 54    build_settings: BuildSettings,
 55    settings: watch::Receiver<workspace::Settings>,
 56}
 57
 58struct PathState {
 59    path: ProjectPath,
 60    header: Option<BlockId>,
 61    diagnostic_groups: Vec<DiagnosticGroupState>,
 62}
 63
 64struct DiagnosticGroupState {
 65    primary_diagnostic: DiagnosticEntry<language::Anchor>,
 66    primary_excerpt_ix: usize,
 67    excerpts: Vec<ExcerptId>,
 68    blocks: HashSet<BlockId>,
 69    block_count: usize,
 70}
 71
 72impl ProjectDiagnostics {
 73    fn new(project: ModelHandle<Project>) -> Self {
 74        Self { project }
 75    }
 76}
 77
 78impl Entity for ProjectDiagnostics {
 79    type Event = ();
 80}
 81
 82impl Entity for ProjectDiagnosticsEditor {
 83    type Event = Event;
 84}
 85
 86impl View for ProjectDiagnosticsEditor {
 87    fn ui_name() -> &'static str {
 88        "ProjectDiagnosticsEditor"
 89    }
 90
 91    fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
 92        if self.path_states.is_empty() {
 93            let theme = &self.settings.borrow().theme.project_diagnostics;
 94            Label::new(
 95                "No problems detected in the project".to_string(),
 96                theme.empty_message.clone(),
 97            )
 98            .aligned()
 99            .contained()
100            .with_style(theme.container)
101            .boxed()
102        } else {
103            ChildView::new(self.editor.id()).boxed()
104        }
105    }
106
107    fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
108        if !self.path_states.is_empty() {
109            cx.focus(&self.editor);
110        }
111    }
112}
113
114impl ProjectDiagnosticsEditor {
115    fn new(
116        model: ModelHandle<ProjectDiagnostics>,
117        workspace: WeakViewHandle<Workspace>,
118        settings: watch::Receiver<workspace::Settings>,
119        cx: &mut ViewContext<Self>,
120    ) -> Self {
121        let project = model.read(cx).project.clone();
122        cx.subscribe(&project, |this, _, event, cx| match event {
123            project::Event::DiskBasedDiagnosticsUpdated { worktree_id } => {
124                this.summary = this.model.read(cx).project.read(cx).diagnostic_summary(cx);
125                if let Some(paths) = this.paths_to_update.remove(&worktree_id) {
126                    this.update_excerpts(paths, cx);
127                }
128                cx.emit(Event::TitleChanged)
129            }
130            project::Event::DiagnosticsUpdated(path) => {
131                this.paths_to_update
132                    .entry(path.worktree_id)
133                    .or_default()
134                    .insert(path.clone());
135            }
136            _ => {}
137        })
138        .detach();
139
140        let excerpts = cx.add_model(|cx| MultiBuffer::new(project.read(cx).replica_id()));
141        let build_settings = editor::settings_builder(excerpts.downgrade(), settings.clone());
142        let editor =
143            cx.add_view(|cx| Editor::for_buffer(excerpts.clone(), build_settings.clone(), cx));
144        cx.subscribe(&editor, |_, _, event, cx| cx.emit(*event))
145            .detach();
146
147        let project = project.read(cx);
148        let paths_to_update = project.diagnostic_summaries(cx).map(|e| e.0).collect();
149        let this = Self {
150            model,
151            summary: project.diagnostic_summary(cx),
152            workspace,
153            excerpts,
154            editor,
155            build_settings,
156            settings,
157            path_states: Default::default(),
158            paths_to_update: Default::default(),
159        };
160        this.update_excerpts(paths_to_update, cx);
161        this
162    }
163
164    #[cfg(test)]
165    fn text(&self, cx: &AppContext) -> String {
166        self.editor.read(cx).text(cx)
167    }
168
169    fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
170        if let Some(existing) = workspace.item_of_type::<ProjectDiagnostics>(cx) {
171            workspace.activate_item(&existing, cx);
172        } else {
173            let diagnostics =
174                cx.add_model(|_| ProjectDiagnostics::new(workspace.project().clone()));
175            workspace.open_item(diagnostics, cx);
176        }
177    }
178
179    fn open_excerpts(&mut self, _: &OpenExcerpts, cx: &mut ViewContext<Self>) {
180        if let Some(workspace) = self.workspace.upgrade(cx) {
181            let editor = self.editor.read(cx);
182            let excerpts = self.excerpts.read(cx);
183            let mut new_selections_by_buffer = HashMap::default();
184
185            for selection in editor.local_selections::<usize>(cx) {
186                for (buffer, mut range) in
187                    excerpts.excerpted_buffers(selection.start..selection.end, cx)
188                {
189                    if selection.reversed {
190                        mem::swap(&mut range.start, &mut range.end);
191                    }
192                    new_selections_by_buffer
193                        .entry(buffer)
194                        .or_insert(Vec::new())
195                        .push(range)
196                }
197            }
198
199            workspace.update(cx, |workspace, cx| {
200                for (buffer, ranges) in new_selections_by_buffer {
201                    let buffer = BufferItemHandle(buffer);
202                    if !workspace.activate_pane_for_item(&buffer, cx) {
203                        workspace.activate_next_pane(cx);
204                    }
205                    let editor = workspace
206                        .open_item(buffer, cx)
207                        .to_any()
208                        .downcast::<Editor>()
209                        .unwrap();
210                    editor.update(cx, |editor, cx| {
211                        editor.select_ranges(ranges, Some(Autoscroll::Center), cx)
212                    });
213                }
214            });
215        }
216    }
217
218    fn update_excerpts(&self, paths: BTreeSet<ProjectPath>, cx: &mut ViewContext<Self>) {
219        let project = self.model.read(cx).project.clone();
220        cx.spawn(|this, mut cx| {
221            async move {
222                for path in paths {
223                    let buffer = project
224                        .update(&mut cx, |project, cx| project.open_buffer(path.clone(), cx))
225                        .await?;
226                    this.update(&mut cx, |view, cx| view.populate_excerpts(path, buffer, cx))
227                }
228                Result::<_, anyhow::Error>::Ok(())
229            }
230            .log_err()
231        })
232        .detach();
233    }
234
235    fn populate_excerpts(
236        &mut self,
237        path: ProjectPath,
238        buffer: ModelHandle<Buffer>,
239        cx: &mut ViewContext<Self>,
240    ) {
241        let was_empty = self.path_states.is_empty();
242        let snapshot = buffer.read(cx).snapshot();
243        let path_ix = match self.path_states.binary_search_by_key(&&path, |e| &e.path) {
244            Ok(ix) => ix,
245            Err(ix) => {
246                self.path_states.insert(
247                    ix,
248                    PathState {
249                        path: path.clone(),
250                        header: None,
251                        diagnostic_groups: Default::default(),
252                    },
253                );
254                ix
255            }
256        };
257
258        let mut prev_excerpt_id = if path_ix > 0 {
259            let prev_path_last_group = &self.path_states[path_ix - 1]
260                .diagnostic_groups
261                .last()
262                .unwrap();
263            prev_path_last_group.excerpts.last().unwrap().clone()
264        } else {
265            ExcerptId::min()
266        };
267
268        let path_state = &mut self.path_states[path_ix];
269        let mut groups_to_add = Vec::new();
270        let mut group_ixs_to_remove = Vec::new();
271        let mut blocks_to_add = Vec::new();
272        let mut blocks_to_remove = HashSet::default();
273        let mut first_excerpt_id = None;
274        let excerpts_snapshot = self.excerpts.update(cx, |excerpts, excerpts_cx| {
275            let mut old_groups = path_state.diagnostic_groups.iter().enumerate().peekable();
276            let mut new_groups = snapshot
277                .diagnostic_groups()
278                .into_iter()
279                .filter(|group| group.entries[group.primary_ix].diagnostic.is_disk_based)
280                .peekable();
281
282            loop {
283                let mut to_insert = None;
284                let mut to_remove = None;
285                let mut to_keep = None;
286                match (old_groups.peek(), new_groups.peek()) {
287                    (None, None) => break,
288                    (None, Some(_)) => to_insert = new_groups.next(),
289                    (Some(_), None) => to_remove = old_groups.next(),
290                    (Some((_, old_group)), Some(new_group)) => {
291                        let old_primary = &old_group.primary_diagnostic;
292                        let new_primary = &new_group.entries[new_group.primary_ix];
293                        match compare_diagnostics(old_primary, new_primary, &snapshot) {
294                            Ordering::Less => to_remove = old_groups.next(),
295                            Ordering::Equal => {
296                                to_keep = old_groups.next();
297                                new_groups.next();
298                            }
299                            Ordering::Greater => to_insert = new_groups.next(),
300                        }
301                    }
302                }
303
304                if let Some(group) = to_insert {
305                    let mut group_state = DiagnosticGroupState {
306                        primary_diagnostic: group.entries[group.primary_ix].clone(),
307                        primary_excerpt_ix: 0,
308                        excerpts: Default::default(),
309                        blocks: Default::default(),
310                        block_count: 0,
311                    };
312                    let mut pending_range: Option<(Range<Point>, usize)> = None;
313                    let mut is_first_excerpt_for_group = true;
314                    for (ix, entry) in group.entries.iter().map(Some).chain([None]).enumerate() {
315                        let resolved_entry = entry.map(|e| e.resolve::<Point>(&snapshot));
316                        if let Some((range, start_ix)) = &mut pending_range {
317                            if let Some(entry) = resolved_entry.as_ref() {
318                                if entry.range.start.row
319                                    <= range.end.row + 1 + CONTEXT_LINE_COUNT * 2
320                                {
321                                    range.end = range.end.max(entry.range.end);
322                                    continue;
323                                }
324                            }
325
326                            let excerpt_start =
327                                Point::new(range.start.row.saturating_sub(CONTEXT_LINE_COUNT), 0);
328                            let excerpt_end = snapshot.clip_point(
329                                Point::new(range.end.row + CONTEXT_LINE_COUNT, u32::MAX),
330                                Bias::Left,
331                            );
332                            let excerpt_id = excerpts.insert_excerpt_after(
333                                &prev_excerpt_id,
334                                ExcerptProperties {
335                                    buffer: &buffer,
336                                    range: excerpt_start..excerpt_end,
337                                },
338                                excerpts_cx,
339                            );
340
341                            prev_excerpt_id = excerpt_id.clone();
342                            first_excerpt_id.get_or_insert_with(|| prev_excerpt_id.clone());
343                            group_state.excerpts.push(excerpt_id.clone());
344                            let header_position = (excerpt_id.clone(), language::Anchor::min());
345
346                            if is_first_excerpt_for_group {
347                                is_first_excerpt_for_group = false;
348                                let primary = &group.entries[group.primary_ix].diagnostic;
349                                let mut header = primary.clone();
350                                header.message =
351                                    primary.message.split('\n').next().unwrap().to_string();
352                                group_state.block_count += 1;
353                                blocks_to_add.push(BlockProperties {
354                                    position: header_position,
355                                    height: 2,
356                                    render: diagnostic_header_renderer(
357                                        header,
358                                        true,
359                                        self.build_settings.clone(),
360                                    ),
361                                    disposition: BlockDisposition::Above,
362                                });
363                            } else {
364                                group_state.block_count += 1;
365                                blocks_to_add.push(BlockProperties {
366                                    position: header_position,
367                                    height: 1,
368                                    render: context_header_renderer(self.build_settings.clone()),
369                                    disposition: BlockDisposition::Above,
370                                });
371                            }
372
373                            for entry in &group.entries[*start_ix..ix] {
374                                let mut diagnostic = entry.diagnostic.clone();
375                                if diagnostic.is_primary {
376                                    group_state.primary_excerpt_ix = group_state.excerpts.len() - 1;
377                                    diagnostic.message =
378                                        entry.diagnostic.message.split('\n').skip(1).collect();
379                                }
380
381                                if !diagnostic.message.is_empty() {
382                                    group_state.block_count += 1;
383                                    blocks_to_add.push(BlockProperties {
384                                        position: (excerpt_id.clone(), entry.range.start.clone()),
385                                        height: diagnostic.message.matches('\n').count() as u8 + 1,
386                                        render: diagnostic_block_renderer(
387                                            diagnostic,
388                                            true,
389                                            self.build_settings.clone(),
390                                        ),
391                                        disposition: BlockDisposition::Below,
392                                    });
393                                }
394                            }
395
396                            pending_range.take();
397                        }
398
399                        if let Some(entry) = resolved_entry {
400                            pending_range = Some((entry.range.clone(), ix));
401                        }
402                    }
403
404                    groups_to_add.push(group_state);
405                } else if let Some((group_ix, group_state)) = to_remove {
406                    excerpts.remove_excerpts(group_state.excerpts.iter(), excerpts_cx);
407                    group_ixs_to_remove.push(group_ix);
408                    blocks_to_remove.extend(group_state.blocks.iter().copied());
409                } else if let Some((_, group)) = to_keep {
410                    prev_excerpt_id = group.excerpts.last().unwrap().clone();
411                    first_excerpt_id.get_or_insert_with(|| prev_excerpt_id.clone());
412                }
413            }
414
415            excerpts.snapshot(excerpts_cx)
416        });
417
418        self.editor.update(cx, |editor, cx| {
419            blocks_to_remove.extend(path_state.header);
420            editor.remove_blocks(blocks_to_remove, cx);
421            let header_block = first_excerpt_id.map(|excerpt_id| BlockProperties {
422                position: excerpts_snapshot.anchor_in_excerpt(excerpt_id, language::Anchor::min()),
423                height: 2,
424                render: path_header_renderer(buffer, self.build_settings.clone()),
425                disposition: BlockDisposition::Above,
426            });
427            let block_ids = editor.insert_blocks(
428                blocks_to_add
429                    .into_iter()
430                    .map(|block| {
431                        let (excerpt_id, text_anchor) = block.position;
432                        BlockProperties {
433                            position: excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor),
434                            height: block.height,
435                            render: block.render,
436                            disposition: block.disposition,
437                        }
438                    })
439                    .chain(header_block.into_iter()),
440                cx,
441            );
442
443            let mut block_ids = block_ids.into_iter();
444            for group_state in &mut groups_to_add {
445                group_state.blocks = block_ids.by_ref().take(group_state.block_count).collect();
446            }
447            path_state.header = block_ids.next();
448        });
449
450        for ix in group_ixs_to_remove.into_iter().rev() {
451            path_state.diagnostic_groups.remove(ix);
452        }
453        path_state.diagnostic_groups.extend(groups_to_add);
454        path_state.diagnostic_groups.sort_unstable_by(|a, b| {
455            let range_a = &a.primary_diagnostic.range;
456            let range_b = &b.primary_diagnostic.range;
457            range_a
458                .start
459                .cmp(&range_b.start, &snapshot)
460                .unwrap()
461                .then_with(|| range_a.end.cmp(&range_b.end, &snapshot).unwrap())
462        });
463
464        if path_state.diagnostic_groups.is_empty() {
465            self.path_states.remove(path_ix);
466        }
467
468        self.editor.update(cx, |editor, cx| {
469            let groups;
470            let mut selections;
471            let new_excerpt_ids_by_selection_id;
472            if was_empty {
473                groups = self.path_states.first()?.diagnostic_groups.as_slice();
474                new_excerpt_ids_by_selection_id = [(0, ExcerptId::min())].into_iter().collect();
475                selections = vec![Selection {
476                    id: 0,
477                    start: 0,
478                    end: 0,
479                    reversed: false,
480                    goal: SelectionGoal::None,
481                }];
482            } else {
483                groups = self.path_states.get(path_ix)?.diagnostic_groups.as_slice();
484                new_excerpt_ids_by_selection_id = editor.refresh_selections(cx);
485                selections = editor.local_selections::<usize>(cx);
486            }
487
488            // If any selection has lost its position, move it to start of the next primary diagnostic.
489            for selection in &mut selections {
490                if let Some(new_excerpt_id) = new_excerpt_ids_by_selection_id.get(&selection.id) {
491                    let group_ix = match groups.binary_search_by(|probe| {
492                        probe.excerpts.last().unwrap().cmp(&new_excerpt_id)
493                    }) {
494                        Ok(ix) | Err(ix) => ix,
495                    };
496                    if let Some(group) = groups.get(group_ix) {
497                        let offset = excerpts_snapshot
498                            .anchor_in_excerpt(
499                                group.excerpts[group.primary_excerpt_ix].clone(),
500                                group.primary_diagnostic.range.start.clone(),
501                            )
502                            .to_offset(&excerpts_snapshot);
503                        selection.start = offset;
504                        selection.end = offset;
505                    }
506                }
507            }
508            editor.update_selections(selections, None, cx);
509            Some(())
510        });
511
512        if self.path_states.is_empty() {
513            if self.editor.is_focused(cx) {
514                cx.focus_self();
515            }
516        } else {
517            if cx.handle().is_focused(cx) {
518                cx.focus(&self.editor);
519            }
520        }
521        cx.notify();
522    }
523}
524
525impl workspace::Item for ProjectDiagnostics {
526    type View = ProjectDiagnosticsEditor;
527
528    fn build_view(
529        handle: ModelHandle<Self>,
530        workspace: &Workspace,
531        cx: &mut ViewContext<Self::View>,
532    ) -> Self::View {
533        ProjectDiagnosticsEditor::new(handle, workspace.weak_handle(), workspace.settings(), cx)
534    }
535
536    fn project_path(&self) -> Option<project::ProjectPath> {
537        None
538    }
539}
540
541impl workspace::ItemView for ProjectDiagnosticsEditor {
542    type ItemHandle = ModelHandle<ProjectDiagnostics>;
543
544    fn item_handle(&self, _: &AppContext) -> Self::ItemHandle {
545        self.model.clone()
546    }
547
548    fn tab_content(&self, style: &theme::Tab, _: &AppContext) -> ElementBox {
549        let theme = &self.settings.borrow().theme.project_diagnostics;
550        let icon_width = theme.tab_icon_width;
551        let icon_spacing = theme.tab_icon_spacing;
552        let summary_spacing = theme.tab_summary_spacing;
553        Flex::row()
554            .with_children([
555                Svg::new("icons/no.svg")
556                    .with_color(style.label.text.color)
557                    .constrained()
558                    .with_width(icon_width)
559                    .aligned()
560                    .contained()
561                    .with_margin_right(icon_spacing)
562                    .named("no-icon"),
563                Label::new(self.summary.error_count.to_string(), style.label.clone())
564                    .aligned()
565                    .boxed(),
566                Svg::new("icons/warning.svg")
567                    .with_color(style.label.text.color)
568                    .constrained()
569                    .with_width(icon_width)
570                    .aligned()
571                    .contained()
572                    .with_margin_left(summary_spacing)
573                    .with_margin_right(icon_spacing)
574                    .named("warn-icon"),
575                Label::new(self.summary.warning_count.to_string(), style.label.clone())
576                    .aligned()
577                    .boxed(),
578            ])
579            .boxed()
580    }
581
582    fn project_path(&self, _: &AppContext) -> Option<project::ProjectPath> {
583        None
584    }
585
586    fn is_dirty(&self, cx: &AppContext) -> bool {
587        self.excerpts.read(cx).read(cx).is_dirty()
588    }
589
590    fn has_conflict(&self, cx: &AppContext) -> bool {
591        self.excerpts.read(cx).read(cx).has_conflict()
592    }
593
594    fn can_save(&self, _: &AppContext) -> bool {
595        true
596    }
597
598    fn save(&mut self, cx: &mut ViewContext<Self>) -> Result<Task<Result<()>>> {
599        self.excerpts.update(cx, |excerpts, cx| excerpts.save(cx))
600    }
601
602    fn can_save_as(&self, _: &AppContext) -> bool {
603        false
604    }
605
606    fn save_as(
607        &mut self,
608        _: ModelHandle<project::Worktree>,
609        _: &std::path::Path,
610        _: &mut ViewContext<Self>,
611    ) -> Task<Result<()>> {
612        unreachable!()
613    }
614
615    fn should_activate_item_on_event(event: &Self::Event) -> bool {
616        Editor::should_activate_item_on_event(event)
617    }
618
619    fn should_update_tab_on_event(event: &Event) -> bool {
620        matches!(event, Event::Saved | Event::Dirtied | Event::TitleChanged)
621    }
622}
623
624fn path_header_renderer(buffer: ModelHandle<Buffer>, build_settings: BuildSettings) -> RenderBlock {
625    Arc::new(move |cx| {
626        let settings = build_settings(cx);
627        let file_path = if let Some(file) = buffer.read(&**cx).file() {
628            file.path().to_string_lossy().to_string()
629        } else {
630            "untitled".to_string()
631        };
632        let mut text_style = settings.style.text.clone();
633        let style = settings.style.diagnostic_path_header;
634        text_style.color = style.text;
635        Label::new(file_path, text_style)
636            .aligned()
637            .left()
638            .contained()
639            .with_style(style.header)
640            .with_padding_left(cx.line_number_x)
641            .expanded()
642            .boxed()
643    })
644}
645
646fn diagnostic_header_renderer(
647    diagnostic: Diagnostic,
648    is_valid: bool,
649    build_settings: BuildSettings,
650) -> RenderBlock {
651    Arc::new(move |cx| {
652        let settings = build_settings(cx);
653        let mut text_style = settings.style.text.clone();
654        let diagnostic_style = diagnostic_style(diagnostic.severity, is_valid, &settings.style);
655        text_style.color = diagnostic_style.text;
656        Text::new(diagnostic.message.clone(), text_style)
657            .with_soft_wrap(false)
658            .aligned()
659            .left()
660            .contained()
661            .with_style(diagnostic_style.header)
662            .with_padding_left(cx.line_number_x)
663            .expanded()
664            .boxed()
665    })
666}
667
668fn context_header_renderer(build_settings: BuildSettings) -> RenderBlock {
669    Arc::new(move |cx| {
670        let settings = build_settings(cx);
671        let text_style = settings.style.text.clone();
672        Label::new("".to_string(), text_style)
673            .contained()
674            .with_padding_left(cx.line_number_x)
675            .boxed()
676    })
677}
678
679fn compare_diagnostics<L: language::ToOffset, R: language::ToOffset>(
680    lhs: &DiagnosticEntry<L>,
681    rhs: &DiagnosticEntry<R>,
682    snapshot: &language::BufferSnapshot,
683) -> Ordering {
684    lhs.range
685        .start
686        .to_offset(&snapshot)
687        .cmp(&rhs.range.start.to_offset(snapshot))
688        .then_with(|| {
689            lhs.range
690                .end
691                .to_offset(&snapshot)
692                .cmp(&rhs.range.end.to_offset(snapshot))
693        })
694        .then_with(|| lhs.diagnostic.message.cmp(&rhs.diagnostic.message))
695}
696
697#[cfg(test)]
698mod tests {
699    use super::*;
700    use client::{http::ServerResponse, test::FakeHttpClient, Client, UserStore};
701    use editor::DisplayPoint;
702    use gpui::TestAppContext;
703    use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, LanguageRegistry, PointUtf16};
704    use project::{worktree, FakeFs};
705    use serde_json::json;
706    use std::sync::Arc;
707    use unindent::Unindent as _;
708    use workspace::WorkspaceParams;
709
710    #[gpui::test]
711    async fn test_diagnostics(mut cx: TestAppContext) {
712        let workspace_params = cx.update(WorkspaceParams::test);
713        let settings = workspace_params.settings.clone();
714        let http_client = FakeHttpClient::new(|_| async move { Ok(ServerResponse::new(404)) });
715        let client = Client::new(http_client.clone());
716        let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
717        let fs = Arc::new(FakeFs::new());
718
719        let project = cx.update(|cx| {
720            Project::local(
721                client.clone(),
722                user_store,
723                Arc::new(LanguageRegistry::new()),
724                fs.clone(),
725                cx,
726            )
727        });
728
729        fs.insert_tree(
730            "/test",
731            json!({
732                "a.rs": "
733                    const a: i32 = 'a';
734                ".unindent(),
735
736                "main.rs": "
737                    fn main() {
738                        let x = vec![];
739                        let y = vec![];
740                        a(x);
741                        b(y);
742                        // comment 1
743                        // comment 2
744                        c(y);
745                        d(x);
746                    }
747                "
748                .unindent(),
749            }),
750        )
751        .await;
752
753        let worktree = project
754            .update(&mut cx, |project, cx| {
755                project.add_local_worktree("/test", cx)
756            })
757            .await
758            .unwrap();
759
760        worktree.update(&mut cx, |worktree, cx| {
761            worktree
762                .update_diagnostic_entries(
763                    Arc::from("/test/main.rs".as_ref()),
764                    None,
765                    vec![
766                        DiagnosticEntry {
767                            range: PointUtf16::new(1, 8)..PointUtf16::new(1, 9),
768                            diagnostic: Diagnostic {
769                                message:
770                                    "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
771                                        .to_string(),
772                                severity: DiagnosticSeverity::INFORMATION,
773                                is_primary: false,
774                                is_disk_based: true,
775                                group_id: 1,
776                                ..Default::default()
777                            },
778                        },
779                        DiagnosticEntry {
780                            range: PointUtf16::new(2, 8)..PointUtf16::new(2, 9),
781                            diagnostic: Diagnostic {
782                                message:
783                                    "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
784                                        .to_string(),
785                                severity: DiagnosticSeverity::INFORMATION,
786                                is_primary: false,
787                                is_disk_based: true,
788                                group_id: 0,
789                                ..Default::default()
790                            },
791                        },
792                        DiagnosticEntry {
793                            range: PointUtf16::new(3, 6)..PointUtf16::new(3, 7),
794                            diagnostic: Diagnostic {
795                                message: "value moved here".to_string(),
796                                severity: DiagnosticSeverity::INFORMATION,
797                                is_primary: false,
798                                is_disk_based: true,
799                                group_id: 1,
800                                ..Default::default()
801                            },
802                        },
803                        DiagnosticEntry {
804                            range: PointUtf16::new(4, 6)..PointUtf16::new(4, 7),
805                            diagnostic: Diagnostic {
806                                message: "value moved here".to_string(),
807                                severity: DiagnosticSeverity::INFORMATION,
808                                is_primary: false,
809                                is_disk_based: true,
810                                group_id: 0,
811                                ..Default::default()
812                            },
813                        },
814                        DiagnosticEntry {
815                            range: PointUtf16::new(7, 6)..PointUtf16::new(7, 7),
816                            diagnostic: Diagnostic {
817                                message: "use of moved value\nvalue used here after move".to_string(),
818                                severity: DiagnosticSeverity::ERROR,
819                                is_primary: true,
820                                is_disk_based: true,
821                                group_id: 0,
822                                ..Default::default()
823                            },
824                        },
825                        DiagnosticEntry {
826                            range: PointUtf16::new(8, 6)..PointUtf16::new(8, 7),
827                            diagnostic: Diagnostic {
828                                message: "use of moved value\nvalue used here after move".to_string(),
829                                severity: DiagnosticSeverity::ERROR,
830                                is_primary: true,
831                                is_disk_based: true,
832                                group_id: 1,
833                                ..Default::default()
834                            },
835                        },
836                    ],
837                    cx,
838                )
839                .unwrap();
840        });
841
842        let model = cx.add_model(|_| ProjectDiagnostics::new(project.clone()));
843        let workspace = cx.add_view(0, |cx| Workspace::new(&workspace_params, cx));
844
845        let view = cx.add_view(0, |cx| {
846            ProjectDiagnosticsEditor::new(model, workspace.downgrade(), settings, cx)
847        });
848
849        view.condition(&mut cx, |view, cx| view.text(cx).contains("fn main()"))
850            .await;
851
852        view.update(&mut cx, |view, cx| {
853            let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
854
855            assert_eq!(
856                editor.text(),
857                concat!(
858                    //
859                    // main.rs
860                    //
861                    "\n", // filename
862                    "\n", // padding
863                    // diagnostic group 1
864                    "\n", // primary message
865                    "\n", // padding
866                    "    let x = vec![];\n",
867                    "    let y = vec![];\n",
868                    "\n", // supporting diagnostic
869                    "    a(x);\n",
870                    "    b(y);\n",
871                    "\n", // supporting diagnostic
872                    "    // comment 1\n",
873                    "    // comment 2\n",
874                    "    c(y);\n",
875                    "\n", // supporting diagnostic
876                    "    d(x);\n",
877                    // diagnostic group 2
878                    "\n", // primary message
879                    "\n", // padding
880                    "fn main() {\n",
881                    "    let x = vec![];\n",
882                    "\n", // supporting diagnostic
883                    "    let y = vec![];\n",
884                    "    a(x);\n",
885                    "\n", // supporting diagnostic
886                    "    b(y);\n",
887                    "\n", // context ellipsis
888                    "    c(y);\n",
889                    "    d(x);\n",
890                    "\n", // supporting diagnostic
891                    "}"
892                )
893            );
894
895            view.editor.update(cx, |editor, cx| {
896                assert_eq!(
897                    editor.selected_display_ranges(cx),
898                    [DisplayPoint::new(12, 6)..DisplayPoint::new(12, 6)]
899                );
900            });
901        });
902
903        worktree.update(&mut cx, |worktree, cx| {
904            worktree
905                .update_diagnostic_entries(
906                    Arc::from("/test/a.rs".as_ref()),
907                    None,
908                    vec![DiagnosticEntry {
909                        range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
910                        diagnostic: Diagnostic {
911                            message: "mismatched types\nexpected `usize`, found `char`".to_string(),
912                            severity: DiagnosticSeverity::ERROR,
913                            is_primary: true,
914                            is_disk_based: true,
915                            group_id: 0,
916                            ..Default::default()
917                        },
918                    }],
919                    cx,
920                )
921                .unwrap();
922            cx.emit(worktree::Event::DiskBasedDiagnosticsUpdated);
923        });
924
925        view.condition(&mut cx, |view, cx| view.text(cx).contains("const a"))
926            .await;
927
928        view.update(&mut cx, |view, cx| {
929            let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
930
931            assert_eq!(
932                editor.text(),
933                concat!(
934                    //
935                    // a.rs
936                    //
937                    "\n", // filename
938                    "\n", // padding
939                    // diagnostic group 1
940                    "\n", // primary message
941                    "\n", // padding
942                    "const a: i32 = 'a';\n",
943                    "\n", // supporting diagnostic
944                    "\n", // context line
945                    //
946                    // main.rs
947                    //
948                    "\n", // filename
949                    "\n", // padding
950                    // diagnostic group 1
951                    "\n", // primary message
952                    "\n", // padding
953                    "    let x = vec![];\n",
954                    "    let y = vec![];\n",
955                    "\n", // supporting diagnostic
956                    "    a(x);\n",
957                    "    b(y);\n",
958                    "\n", // supporting diagnostic
959                    "    // comment 1\n",
960                    "    // comment 2\n",
961                    "    c(y);\n",
962                    "\n", // supporting diagnostic
963                    "    d(x);\n",
964                    // diagnostic group 2
965                    "\n", // primary message
966                    "\n", // filename
967                    "fn main() {\n",
968                    "    let x = vec![];\n",
969                    "\n", // supporting diagnostic
970                    "    let y = vec![];\n",
971                    "    a(x);\n",
972                    "\n", // supporting diagnostic
973                    "    b(y);\n",
974                    "\n", // context ellipsis
975                    "    c(y);\n",
976                    "    d(x);\n",
977                    "\n", // supporting diagnostic
978                    "}"
979                )
980            );
981        });
982    }
983}