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