diagnostics.rs

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