diagnostics.rs

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