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