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