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