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