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