diagnostics.rs

   1pub mod items;
   2
   3use anyhow::Result;
   4use collections::{BTreeSet, HashMap, HashSet};
   5use editor::{
   6    diagnostic_block_renderer,
   7    display_map::{BlockDisposition, BlockId, BlockProperties, RenderBlock},
   8    highlight_diagnostic_message,
   9    items::BufferItemHandle,
  10    Autoscroll, BuildSettings, Editor, ExcerptId, ExcerptProperties, MultiBuffer, ToOffset,
  11};
  12use gpui::{
  13    action, elements::*, fonts::TextStyle, keymap::Binding, AnyViewHandle, AppContext, Entity,
  14    ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext, ViewHandle,
  15    WeakViewHandle,
  16};
  17use language::{
  18    Bias, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, Point, Selection, SelectionGoal,
  19};
  20use postage::watch;
  21use project::{DiagnosticSummary, Project, ProjectPath};
  22use std::{
  23    any::{Any, TypeId},
  24    cmp::Ordering,
  25    mem,
  26    ops::Range,
  27    path::PathBuf,
  28    sync::Arc,
  29};
  30use util::TryFutureExt;
  31use workspace::{ItemNavHistory, Workspace};
  32
  33action!(Deploy);
  34action!(OpenExcerpts);
  35
  36const CONTEXT_LINE_COUNT: u32 = 1;
  37
  38pub fn init(cx: &mut MutableAppContext) {
  39    cx.add_bindings([
  40        Binding::new("alt-shift-D", Deploy, Some("Workspace")),
  41        Binding::new(
  42            "alt-shift-D",
  43            OpenExcerpts,
  44            Some("ProjectDiagnosticsEditor"),
  45        ),
  46    ]);
  47    cx.add_action(ProjectDiagnosticsEditor::deploy);
  48    cx.add_action(ProjectDiagnosticsEditor::open_excerpts);
  49}
  50
  51type Event = editor::Event;
  52
  53struct ProjectDiagnostics {
  54    project: ModelHandle<Project>,
  55}
  56
  57struct ProjectDiagnosticsEditor {
  58    model: ModelHandle<ProjectDiagnostics>,
  59    workspace: WeakViewHandle<Workspace>,
  60    editor: ViewHandle<Editor>,
  61    summary: DiagnosticSummary,
  62    excerpts: ModelHandle<MultiBuffer>,
  63    path_states: Vec<PathState>,
  64    paths_to_update: BTreeSet<ProjectPath>,
  65    build_settings: BuildSettings,
  66    settings: watch::Receiver<workspace::Settings>,
  67}
  68
  69struct PathState {
  70    path: ProjectPath,
  71    header: Option<BlockId>,
  72    diagnostic_groups: Vec<DiagnosticGroupState>,
  73}
  74
  75struct DiagnosticGroupState {
  76    primary_diagnostic: DiagnosticEntry<language::Anchor>,
  77    primary_excerpt_ix: usize,
  78    excerpts: Vec<ExcerptId>,
  79    blocks: HashSet<BlockId>,
  80    block_count: usize,
  81}
  82
  83impl ProjectDiagnostics {
  84    fn new(project: ModelHandle<Project>) -> Self {
  85        Self { project }
  86    }
  87}
  88
  89impl Entity for ProjectDiagnostics {
  90    type Event = ();
  91}
  92
  93impl Entity for ProjectDiagnosticsEditor {
  94    type Event = Event;
  95}
  96
  97impl View for ProjectDiagnosticsEditor {
  98    fn ui_name() -> &'static str {
  99        "ProjectDiagnosticsEditor"
 100    }
 101
 102    fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
 103        if self.path_states.is_empty() {
 104            let theme = &self.settings.borrow().theme.project_diagnostics;
 105            Label::new(
 106                "No problems in workspace".to_string(),
 107                theme.empty_message.clone(),
 108            )
 109            .aligned()
 110            .contained()
 111            .with_style(theme.container)
 112            .boxed()
 113        } else {
 114            ChildView::new(self.editor.id()).boxed()
 115        }
 116    }
 117
 118    fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
 119        if !self.path_states.is_empty() {
 120            cx.focus(&self.editor);
 121        }
 122    }
 123}
 124
 125impl ProjectDiagnosticsEditor {
 126    fn new(
 127        model: ModelHandle<ProjectDiagnostics>,
 128        workspace: WeakViewHandle<Workspace>,
 129        settings: watch::Receiver<workspace::Settings>,
 130        cx: &mut ViewContext<Self>,
 131    ) -> Self {
 132        let project = model.read(cx).project.clone();
 133        cx.subscribe(&project, |this, _, event, cx| match event {
 134            project::Event::DiskBasedDiagnosticsFinished => {
 135                this.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    if summary.error_count == 0 && summary.warning_count == 0 {
 774        Label::new("No problems".to_string(), text_style.clone()).boxed()
 775    } else {
 776        let icon_width = theme.tab_icon_width;
 777        let icon_spacing = theme.tab_icon_spacing;
 778        let summary_spacing = theme.tab_summary_spacing;
 779        Flex::row()
 780            .with_children([
 781                Svg::new("icons/diagnostic-summary-error.svg")
 782                    .with_color(text_style.color)
 783                    .constrained()
 784                    .with_width(icon_width)
 785                    .aligned()
 786                    .contained()
 787                    .with_margin_right(icon_spacing)
 788                    .named("no-icon"),
 789                Label::new(
 790                    summary.error_count.to_string(),
 791                    LabelStyle {
 792                        text: text_style.clone(),
 793                        highlight_text: None,
 794                    },
 795                )
 796                .aligned()
 797                .boxed(),
 798                Svg::new("icons/diagnostic-summary-warning.svg")
 799                    .with_color(text_style.color)
 800                    .constrained()
 801                    .with_width(icon_width)
 802                    .aligned()
 803                    .contained()
 804                    .with_margin_left(summary_spacing)
 805                    .with_margin_right(icon_spacing)
 806                    .named("warn-icon"),
 807                Label::new(
 808                    summary.warning_count.to_string(),
 809                    LabelStyle {
 810                        text: text_style.clone(),
 811                        highlight_text: None,
 812                    },
 813                )
 814                .aligned()
 815                .boxed(),
 816            ])
 817            .boxed()
 818    }
 819}
 820
 821fn compare_diagnostics<L: language::ToOffset, R: language::ToOffset>(
 822    lhs: &DiagnosticEntry<L>,
 823    rhs: &DiagnosticEntry<R>,
 824    snapshot: &language::BufferSnapshot,
 825) -> Ordering {
 826    lhs.range
 827        .start
 828        .to_offset(&snapshot)
 829        .cmp(&rhs.range.start.to_offset(snapshot))
 830        .then_with(|| {
 831            lhs.range
 832                .end
 833                .to_offset(&snapshot)
 834                .cmp(&rhs.range.end.to_offset(snapshot))
 835        })
 836        .then_with(|| lhs.diagnostic.message.cmp(&rhs.diagnostic.message))
 837}
 838
 839#[cfg(test)]
 840mod tests {
 841    use super::*;
 842    use editor::{display_map::BlockContext, DisplayPoint, EditorSnapshot};
 843    use gpui::TestAppContext;
 844    use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16};
 845    use serde_json::json;
 846    use unindent::Unindent as _;
 847    use workspace::WorkspaceParams;
 848
 849    #[gpui::test]
 850    async fn test_diagnostics(mut cx: TestAppContext) {
 851        let params = cx.update(WorkspaceParams::test);
 852        let project = params.project.clone();
 853        let workspace = cx.add_view(0, |cx| Workspace::new(&params, cx));
 854
 855        params
 856            .fs
 857            .as_fake()
 858            .insert_tree(
 859                "/test",
 860                json!({
 861                    "consts.rs": "
 862                    const a: i32 = 'a';
 863                    const b: i32 = c;
 864                "
 865                    .unindent(),
 866
 867                    "main.rs": "
 868                    fn main() {
 869                        let x = vec![];
 870                        let y = vec![];
 871                        a(x);
 872                        b(y);
 873                        // comment 1
 874                        // comment 2
 875                        c(y);
 876                        d(x);
 877                    }
 878                "
 879                    .unindent(),
 880                }),
 881            )
 882            .await;
 883
 884        project
 885            .update(&mut cx, |project, cx| {
 886                project.find_or_create_local_worktree("/test", false, cx)
 887            })
 888            .await
 889            .unwrap();
 890
 891        // Create some diagnostics
 892        project.update(&mut cx, |project, cx| {
 893            project
 894                .update_diagnostic_entries(
 895                    PathBuf::from("/test/main.rs"),
 896                    None,
 897                    vec![
 898                        DiagnosticEntry {
 899                            range: PointUtf16::new(1, 8)..PointUtf16::new(1, 9),
 900                            diagnostic: Diagnostic {
 901                                message:
 902                                    "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
 903                                        .to_string(),
 904                                severity: DiagnosticSeverity::INFORMATION,
 905                                is_primary: false,
 906                                is_disk_based: true,
 907                                group_id: 1,
 908                                ..Default::default()
 909                            },
 910                        },
 911                        DiagnosticEntry {
 912                            range: PointUtf16::new(2, 8)..PointUtf16::new(2, 9),
 913                            diagnostic: Diagnostic {
 914                                message:
 915                                    "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
 916                                        .to_string(),
 917                                severity: DiagnosticSeverity::INFORMATION,
 918                                is_primary: false,
 919                                is_disk_based: true,
 920                                group_id: 0,
 921                                ..Default::default()
 922                            },
 923                        },
 924                        DiagnosticEntry {
 925                            range: PointUtf16::new(3, 6)..PointUtf16::new(3, 7),
 926                            diagnostic: Diagnostic {
 927                                message: "value moved here".to_string(),
 928                                severity: DiagnosticSeverity::INFORMATION,
 929                                is_primary: false,
 930                                is_disk_based: true,
 931                                group_id: 1,
 932                                ..Default::default()
 933                            },
 934                        },
 935                        DiagnosticEntry {
 936                            range: PointUtf16::new(4, 6)..PointUtf16::new(4, 7),
 937                            diagnostic: Diagnostic {
 938                                message: "value moved here".to_string(),
 939                                severity: DiagnosticSeverity::INFORMATION,
 940                                is_primary: false,
 941                                is_disk_based: true,
 942                                group_id: 0,
 943                                ..Default::default()
 944                            },
 945                        },
 946                        DiagnosticEntry {
 947                            range: PointUtf16::new(7, 6)..PointUtf16::new(7, 7),
 948                            diagnostic: Diagnostic {
 949                                message: "use of moved value\nvalue used here after move".to_string(),
 950                                severity: DiagnosticSeverity::ERROR,
 951                                is_primary: true,
 952                                is_disk_based: true,
 953                                group_id: 0,
 954                                ..Default::default()
 955                            },
 956                        },
 957                        DiagnosticEntry {
 958                            range: PointUtf16::new(8, 6)..PointUtf16::new(8, 7),
 959                            diagnostic: Diagnostic {
 960                                message: "use of moved value\nvalue used here after move".to_string(),
 961                                severity: DiagnosticSeverity::ERROR,
 962                                is_primary: true,
 963                                is_disk_based: true,
 964                                group_id: 1,
 965                                ..Default::default()
 966                            },
 967                        },
 968                    ],
 969                    cx,
 970                )
 971                .unwrap();
 972        });
 973
 974        // Open the project diagnostics view while there are already diagnostics.
 975        let model = cx.add_model(|_| ProjectDiagnostics::new(project.clone()));
 976        let view = cx.add_view(0, |cx| {
 977            ProjectDiagnosticsEditor::new(model, workspace.downgrade(), params.settings, cx)
 978        });
 979
 980        view.next_notification(&cx).await;
 981        view.update(&mut cx, |view, cx| {
 982            let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
 983
 984            assert_eq!(
 985                editor_blocks(&editor, cx),
 986                [
 987                    (0, "path header block".into()),
 988                    (2, "diagnostic header".into()),
 989                    (15, "diagnostic header".into()),
 990                    (24, "collapsed context".into()),
 991                ]
 992            );
 993            assert_eq!(
 994                editor.text(),
 995                concat!(
 996                    //
 997                    // main.rs
 998                    //
 999                    "\n", // filename
1000                    "\n", // padding
1001                    // diagnostic group 1
1002                    "\n", // primary message
1003                    "\n", // padding
1004                    "    let x = vec![];\n",
1005                    "    let y = vec![];\n",
1006                    "\n", // supporting diagnostic
1007                    "    a(x);\n",
1008                    "    b(y);\n",
1009                    "\n", // supporting diagnostic
1010                    "    // comment 1\n",
1011                    "    // comment 2\n",
1012                    "    c(y);\n",
1013                    "\n", // supporting diagnostic
1014                    "    d(x);\n",
1015                    // diagnostic group 2
1016                    "\n", // primary message
1017                    "\n", // padding
1018                    "fn main() {\n",
1019                    "    let x = vec![];\n",
1020                    "\n", // supporting diagnostic
1021                    "    let y = vec![];\n",
1022                    "    a(x);\n",
1023                    "\n", // supporting diagnostic
1024                    "    b(y);\n",
1025                    "\n", // context ellipsis
1026                    "    c(y);\n",
1027                    "    d(x);\n",
1028                    "\n", // supporting diagnostic
1029                    "}"
1030                )
1031            );
1032
1033            // Cursor is at the first diagnostic
1034            view.editor.update(cx, |editor, cx| {
1035                assert_eq!(
1036                    editor.selected_display_ranges(cx),
1037                    [DisplayPoint::new(12, 6)..DisplayPoint::new(12, 6)]
1038                );
1039            });
1040        });
1041
1042        // Diagnostics are added for another earlier path.
1043        project.update(&mut cx, |project, cx| {
1044            project.disk_based_diagnostics_started(cx);
1045            project
1046                .update_diagnostic_entries(
1047                    PathBuf::from("/test/consts.rs"),
1048                    None,
1049                    vec![DiagnosticEntry {
1050                        range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
1051                        diagnostic: Diagnostic {
1052                            message: "mismatched types\nexpected `usize`, found `char`".to_string(),
1053                            severity: DiagnosticSeverity::ERROR,
1054                            is_primary: true,
1055                            is_disk_based: true,
1056                            group_id: 0,
1057                            ..Default::default()
1058                        },
1059                    }],
1060                    cx,
1061                )
1062                .unwrap();
1063            project.disk_based_diagnostics_finished(cx);
1064        });
1065
1066        view.next_notification(&cx).await;
1067        view.update(&mut cx, |view, cx| {
1068            let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
1069
1070            assert_eq!(
1071                editor_blocks(&editor, cx),
1072                [
1073                    (0, "path header block".into()),
1074                    (2, "diagnostic header".into()),
1075                    (7, "path header block".into()),
1076                    (9, "diagnostic header".into()),
1077                    (22, "diagnostic header".into()),
1078                    (31, "collapsed context".into()),
1079                ]
1080            );
1081            assert_eq!(
1082                editor.text(),
1083                concat!(
1084                    //
1085                    // consts.rs
1086                    //
1087                    "\n", // filename
1088                    "\n", // padding
1089                    // diagnostic group 1
1090                    "\n", // primary message
1091                    "\n", // padding
1092                    "const a: i32 = 'a';\n",
1093                    "\n", // supporting diagnostic
1094                    "const b: i32 = c;\n",
1095                    //
1096                    // main.rs
1097                    //
1098                    "\n", // filename
1099                    "\n", // padding
1100                    // diagnostic group 1
1101                    "\n", // primary message
1102                    "\n", // padding
1103                    "    let x = vec![];\n",
1104                    "    let y = vec![];\n",
1105                    "\n", // supporting diagnostic
1106                    "    a(x);\n",
1107                    "    b(y);\n",
1108                    "\n", // supporting diagnostic
1109                    "    // comment 1\n",
1110                    "    // comment 2\n",
1111                    "    c(y);\n",
1112                    "\n", // supporting diagnostic
1113                    "    d(x);\n",
1114                    // diagnostic group 2
1115                    "\n", // primary message
1116                    "\n", // filename
1117                    "fn main() {\n",
1118                    "    let x = vec![];\n",
1119                    "\n", // supporting diagnostic
1120                    "    let y = vec![];\n",
1121                    "    a(x);\n",
1122                    "\n", // supporting diagnostic
1123                    "    b(y);\n",
1124                    "\n", // context ellipsis
1125                    "    c(y);\n",
1126                    "    d(x);\n",
1127                    "\n", // supporting diagnostic
1128                    "}"
1129                )
1130            );
1131
1132            // Cursor keeps its position.
1133            view.editor.update(cx, |editor, cx| {
1134                assert_eq!(
1135                    editor.selected_display_ranges(cx),
1136                    [DisplayPoint::new(19, 6)..DisplayPoint::new(19, 6)]
1137                );
1138            });
1139        });
1140
1141        // Diagnostics are added to the first path
1142        project.update(&mut cx, |project, cx| {
1143            project.disk_based_diagnostics_started(cx);
1144            project
1145                .update_diagnostic_entries(
1146                    PathBuf::from("/test/consts.rs"),
1147                    None,
1148                    vec![
1149                        DiagnosticEntry {
1150                            range: PointUtf16::new(0, 15)..PointUtf16::new(0, 15),
1151                            diagnostic: Diagnostic {
1152                                message: "mismatched types\nexpected `usize`, found `char`"
1153                                    .to_string(),
1154                                severity: DiagnosticSeverity::ERROR,
1155                                is_primary: true,
1156                                is_disk_based: true,
1157                                group_id: 0,
1158                                ..Default::default()
1159                            },
1160                        },
1161                        DiagnosticEntry {
1162                            range: PointUtf16::new(1, 15)..PointUtf16::new(1, 15),
1163                            diagnostic: Diagnostic {
1164                                message: "unresolved name `c`".to_string(),
1165                                severity: DiagnosticSeverity::ERROR,
1166                                is_primary: true,
1167                                is_disk_based: true,
1168                                group_id: 1,
1169                                ..Default::default()
1170                            },
1171                        },
1172                    ],
1173                    cx,
1174                )
1175                .unwrap();
1176            project.disk_based_diagnostics_finished(cx);
1177        });
1178
1179        view.next_notification(&cx).await;
1180        view.update(&mut cx, |view, cx| {
1181            let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
1182
1183            assert_eq!(
1184                editor_blocks(&editor, cx),
1185                [
1186                    (0, "path header block".into()),
1187                    (2, "diagnostic header".into()),
1188                    (7, "diagnostic header".into()),
1189                    (12, "path header block".into()),
1190                    (14, "diagnostic header".into()),
1191                    (27, "diagnostic header".into()),
1192                    (36, "collapsed context".into()),
1193                ]
1194            );
1195            assert_eq!(
1196                editor.text(),
1197                concat!(
1198                    //
1199                    // consts.rs
1200                    //
1201                    "\n", // filename
1202                    "\n", // padding
1203                    // diagnostic group 1
1204                    "\n", // primary message
1205                    "\n", // padding
1206                    "const a: i32 = 'a';\n",
1207                    "\n", // supporting diagnostic
1208                    "const b: i32 = c;\n",
1209                    // diagnostic group 2
1210                    "\n", // primary message
1211                    "\n", // padding
1212                    "const a: i32 = 'a';\n",
1213                    "const b: i32 = c;\n",
1214                    "\n", // supporting diagnostic
1215                    //
1216                    // main.rs
1217                    //
1218                    "\n", // filename
1219                    "\n", // padding
1220                    // diagnostic group 1
1221                    "\n", // primary message
1222                    "\n", // padding
1223                    "    let x = vec![];\n",
1224                    "    let y = vec![];\n",
1225                    "\n", // supporting diagnostic
1226                    "    a(x);\n",
1227                    "    b(y);\n",
1228                    "\n", // supporting diagnostic
1229                    "    // comment 1\n",
1230                    "    // comment 2\n",
1231                    "    c(y);\n",
1232                    "\n", // supporting diagnostic
1233                    "    d(x);\n",
1234                    // diagnostic group 2
1235                    "\n", // primary message
1236                    "\n", // filename
1237                    "fn main() {\n",
1238                    "    let x = vec![];\n",
1239                    "\n", // supporting diagnostic
1240                    "    let y = vec![];\n",
1241                    "    a(x);\n",
1242                    "\n", // supporting diagnostic
1243                    "    b(y);\n",
1244                    "\n", // context ellipsis
1245                    "    c(y);\n",
1246                    "    d(x);\n",
1247                    "\n", // supporting diagnostic
1248                    "}"
1249                )
1250            );
1251        });
1252    }
1253
1254    fn editor_blocks(editor: &EditorSnapshot, cx: &AppContext) -> Vec<(u32, String)> {
1255        editor
1256            .blocks_in_range(0..editor.max_point().row())
1257            .filter_map(|(row, block)| {
1258                block
1259                    .render(&BlockContext {
1260                        cx,
1261                        anchor_x: 0.,
1262                        scroll_x: 0.,
1263                        gutter_padding: 0.,
1264                        gutter_width: 0.,
1265                        line_height: 0.,
1266                        em_width: 0.,
1267                    })
1268                    .name()
1269                    .map(|s| (row, s.to_string()))
1270            })
1271            .collect()
1272    }
1273}