use anyhow::Result; use collections::{BTreeMap, BTreeSet, HashMap, HashSet}; use editor::{ diagnostic_block_renderer, display_map::{ BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, CustomBlockId, RenderBlock, }, scroll::Autoscroll, Bias, Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, ToPoint, }; use futures::{ channel::mpsc::{self, UnboundedSender}, StreamExt as _, }; use gpui::{ actions, div, AnyElement, AnyView, AppContext, Context, EventEmitter, FocusHandle, FocusableView, InteractiveElement, IntoElement, Model, ParentElement, Render, SharedString, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowContext, }; use language::{ Buffer, BufferSnapshot, DiagnosticEntry, DiagnosticSeverity, OffsetRangeExt, ToOffset, ToPoint as _, }; use lsp::LanguageServerId; use multi_buffer::{build_excerpt_ranges, ExpandExcerptDirection, MultiBufferRow}; use project::{DiagnosticSummary, Project, ProjectPath}; use settings::Settings; use std::{ any::{Any, TypeId}, cmp::Ordering, ops::Range, sync::{ atomic::{self, AtomicBool}, Arc, }, }; use theme::ActiveTheme; use ui::{h_flex, prelude::*, Icon, IconName, Label}; use util::{debug_panic, ResultExt}; use workspace::{ item::{BreadcrumbText, Item, ItemEvent, ItemHandle, TabContentParams}, ItemNavHistory, ToolbarItemLocation, Workspace, }; use crate::project_diagnostics_settings::ProjectDiagnosticsSettings; actions!(grouped_diagnostics, [Deploy, ToggleWarnings]); pub fn init(cx: &mut AppContext) { cx.observe_new_views(GroupedDiagnosticsEditor::register) .detach(); } pub struct GroupedDiagnosticsEditor { pub project: Model, workspace: WeakView, focus_handle: FocusHandle, editor: View, summary: DiagnosticSummary, excerpts: Model, path_states: Vec, pub paths_to_update: BTreeSet<(ProjectPath, LanguageServerId)>, pub include_warnings: bool, context: u32, pub update_paths_tx: UnboundedSender<(ProjectPath, Option)>, _update_excerpts_task: Task>, _subscription: Subscription, } struct PathState { path: ProjectPath, first_excerpt_id: Option, last_excerpt_id: Option, diagnostics: Vec<(DiagnosticData, CustomBlockId)>, } #[derive(Debug, Clone)] struct DiagnosticData { language_server_id: LanguageServerId, is_primary: bool, entry: DiagnosticEntry, } impl DiagnosticData { fn diagnostic_entries_equal(&self, other: &DiagnosticData) -> bool { self.language_server_id == other.language_server_id && self.is_primary == other.is_primary && self.entry.range == other.entry.range && equal_without_group_ids(&self.entry.diagnostic, &other.entry.diagnostic) } } // `group_id` can differ between LSP server diagnostics output, // hence ignore it when checking diagnostics for updates. fn equal_without_group_ids(a: &language::Diagnostic, b: &language::Diagnostic) -> bool { a.source == b.source && a.code == b.code && a.severity == b.severity && a.message == b.message && a.is_primary == b.is_primary && a.is_disk_based == b.is_disk_based && a.is_unnecessary == b.is_unnecessary } impl EventEmitter for GroupedDiagnosticsEditor {} impl Render for GroupedDiagnosticsEditor { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { let child = if self.path_states.is_empty() { div() .bg(cx.theme().colors().editor_background) .flex() .items_center() .justify_center() .size_full() .child(Label::new("No problems in workspace")) } else { div().size_full().child(self.editor.clone()) }; div() .track_focus(&self.focus_handle) .when(self.path_states.is_empty(), |el| { el.key_context("EmptyPane") }) .size_full() .on_action(cx.listener(Self::toggle_warnings)) .child(child) } } impl GroupedDiagnosticsEditor { fn register(workspace: &mut Workspace, _: &mut ViewContext) { workspace.register_action(Self::deploy); } fn new_with_context( context: u32, project_handle: Model, workspace: WeakView, cx: &mut ViewContext, ) -> Self { let project_event_subscription = cx.subscribe(&project_handle, |this, project, event, cx| match event { project::Event::DiskBasedDiagnosticsStarted { .. } => { cx.notify(); } project::Event::DiskBasedDiagnosticsFinished { language_server_id } => { log::debug!("disk based diagnostics finished for server {language_server_id}"); this.enqueue_update_stale_excerpts(Some(*language_server_id)); } project::Event::DiagnosticsUpdated { language_server_id, path, } => { this.paths_to_update .insert((path.clone(), *language_server_id)); this.summary = project.read(cx).diagnostic_summary(false, cx); cx.emit(EditorEvent::TitleChanged); if this.editor.focus_handle(cx).contains_focused(cx) || this.focus_handle.contains_focused(cx) { log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. recording change"); } else { log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. updating excerpts"); this.enqueue_update_stale_excerpts(Some(*language_server_id)); } } _ => {} }); let focus_handle = cx.focus_handle(); cx.on_focus_in(&focus_handle, |this, cx| this.focus_in(cx)) .detach(); cx.on_focus_out(&focus_handle, |this, _event, cx| this.focus_out(cx)) .detach(); let excerpts = cx.new_model(|cx| { MultiBuffer::new( project_handle.read(cx).replica_id(), project_handle.read(cx).capability(), ) }); let editor = cx.new_view(|cx| { let mut editor = Editor::for_multibuffer(excerpts.clone(), Some(project_handle.clone()), false, cx); editor.set_vertical_scroll_margin(5, cx); editor }); cx.subscribe(&editor, |this, _editor, event: &EditorEvent, cx| { cx.emit(event.clone()); match event { EditorEvent::Focused => { if this.path_states.is_empty() { cx.focus(&this.focus_handle); } } EditorEvent::Blurred => this.enqueue_update_stale_excerpts(None), _ => {} } }) .detach(); let (update_excerpts_tx, mut update_excerpts_rx) = mpsc::unbounded(); let project = project_handle.read(cx); let mut this = Self { project: project_handle.clone(), context, summary: project.diagnostic_summary(false, cx), workspace, excerpts, focus_handle, editor, path_states: Vec::new(), paths_to_update: BTreeSet::new(), include_warnings: ProjectDiagnosticsSettings::get_global(cx).include_warnings, update_paths_tx: update_excerpts_tx, _update_excerpts_task: cx.spawn(move |this, mut cx| async move { while let Some((path, language_server_id)) = update_excerpts_rx.next().await { if let Some(buffer) = project_handle .update(&mut cx, |project, cx| project.open_buffer(path.clone(), cx))? .await .log_err() { this.update(&mut cx, |this, cx| { this.update_excerpts(path, language_server_id, buffer, cx); })?; } } anyhow::Ok(()) }), _subscription: project_event_subscription, }; this.enqueue_update_all_excerpts(cx); this } fn new( project_handle: Model, workspace: WeakView, cx: &mut ViewContext, ) -> Self { Self::new_with_context( editor::DEFAULT_MULTIBUFFER_CONTEXT, project_handle, workspace, cx, ) } fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext) { if let Some(existing) = workspace.item_of_type::(cx) { workspace.activate_item(&existing, true, true, cx); } else { let workspace_handle = cx.view().downgrade(); let diagnostics = cx.new_view(|cx| { GroupedDiagnosticsEditor::new(workspace.project().clone(), workspace_handle, cx) }); workspace.add_item_to_active_pane(Box::new(diagnostics), None, true, cx); } } pub fn toggle_warnings(&mut self, _: &ToggleWarnings, cx: &mut ViewContext) { self.include_warnings = !self.include_warnings; self.enqueue_update_all_excerpts(cx); cx.notify(); } fn focus_in(&mut self, cx: &mut ViewContext) { if self.focus_handle.is_focused(cx) && !self.path_states.is_empty() { self.editor.focus_handle(cx).focus(cx) } } fn focus_out(&mut self, cx: &mut ViewContext) { if !self.focus_handle.is_focused(cx) && !self.editor.focus_handle(cx).is_focused(cx) { self.enqueue_update_stale_excerpts(None); } } /// Enqueue an update of all excerpts. Updates all paths that either /// currently have diagnostics or are currently present in this view. fn enqueue_update_all_excerpts(&mut self, cx: &mut ViewContext) { self.project.update(cx, |project, cx| { let mut paths = project .diagnostic_summaries(false, cx) .map(|(path, _, _)| path) .collect::>(); paths.extend(self.path_states.iter().map(|state| state.path.clone())); for path in paths { self.update_paths_tx.unbounded_send((path, None)).unwrap(); } }); } /// Enqueue an update of the excerpts for any path whose diagnostics are known /// to have changed. If a language server id is passed, then only the excerpts for /// that language server's diagnostics will be updated. Otherwise, all stale excerpts /// will be refreshed. pub fn enqueue_update_stale_excerpts(&mut self, language_server_id: Option) { for (path, server_id) in &self.paths_to_update { if language_server_id.map_or(true, |id| id == *server_id) { self.update_paths_tx .unbounded_send((path.clone(), Some(*server_id))) .unwrap(); } } } fn update_excerpts( &mut self, path_to_update: ProjectPath, server_to_update: Option, buffer: Model, cx: &mut ViewContext, ) { self.paths_to_update.retain(|(path, server_id)| { *path != path_to_update || server_to_update.map_or(false, |to_update| *server_id != to_update) }); // TODO change selections as in the old panel, to the next primary diagnostics // TODO make [shift-]f8 to work, jump to the next block group let _was_empty = self.path_states.is_empty(); let path_ix = match self.path_states.binary_search_by(|probe| { project::compare_paths((&probe.path.path, true), (&path_to_update.path, true)) }) { Ok(ix) => ix, Err(ix) => { self.path_states.insert( ix, PathState { path: path_to_update.clone(), diagnostics: Vec::new(), last_excerpt_id: None, first_excerpt_id: None, }, ); ix } }; let max_severity = if self.include_warnings { DiagnosticSeverity::WARNING } else { DiagnosticSeverity::ERROR }; let excerpt_borders = self.excerpt_borders_for_path(path_ix); let path_state = &mut self.path_states[path_ix]; let buffer_snapshot = buffer.read(cx).snapshot(); let mut path_update = PathUpdate::new( excerpt_borders, &buffer_snapshot, server_to_update, max_severity, path_state, ); path_update.prepare_excerpt_data( self.context, self.excerpts.read(cx).snapshot(cx), buffer.read(cx).snapshot(), path_state.diagnostics.iter(), ); self.excerpts.update(cx, |multi_buffer, cx| { path_update.apply_excerpt_changes( path_state, self.context, buffer_snapshot, multi_buffer, buffer, cx, ); }); let new_multi_buffer_snapshot = self.excerpts.read(cx).snapshot(cx); let blocks_to_insert = path_update.prepare_blocks_to_insert(self.editor.clone(), new_multi_buffer_snapshot); let new_block_ids = self.editor.update(cx, |editor, cx| { editor.remove_blocks(std::mem::take(&mut path_update.blocks_to_remove), None, cx); editor.insert_blocks(blocks_to_insert, Some(Autoscroll::fit()), cx) }); path_state.diagnostics = path_update.new_blocks(new_block_ids); if self.path_states.is_empty() { if self.editor.focus_handle(cx).is_focused(cx) { cx.focus(&self.focus_handle); } } else if self.focus_handle.is_focused(cx) { let focus_handle = self.editor.focus_handle(cx); cx.focus(&focus_handle); } #[cfg(test)] self.check_invariants(cx); cx.notify(); } fn excerpt_borders_for_path(&self, path_ix: usize) -> (Option, Option) { let previous_path_state_ix = Some(path_ix.saturating_sub(1)).filter(|&previous_path_ix| previous_path_ix != path_ix); let next_path_state_ix = path_ix + 1; let start = previous_path_state_ix.and_then(|i| { self.path_states[..=i] .iter() .rev() .find_map(|state| state.last_excerpt_id) }); let end = self.path_states[next_path_state_ix..] .iter() .find_map(|state| state.first_excerpt_id); (start, end) } #[cfg(test)] fn check_invariants(&self, cx: &mut ViewContext) { let mut excerpts = Vec::new(); for (id, buffer, _) in self.excerpts.read(cx).snapshot(cx).excerpts() { if let Some(file) = buffer.file() { excerpts.push((id, file.path().clone())); } } let mut prev_path = None; for (_, path) in &excerpts { if let Some(prev_path) = prev_path { if path < prev_path { panic!("excerpts are not sorted by path {:?}", excerpts); } } prev_path = Some(path); } } } impl FocusableView for GroupedDiagnosticsEditor { fn focus_handle(&self, _: &AppContext) -> FocusHandle { self.focus_handle.clone() } } impl Item for GroupedDiagnosticsEditor { type Event = EditorEvent; fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) { Editor::to_item_events(event, f) } fn deactivated(&mut self, cx: &mut ViewContext) { self.editor.update(cx, |editor, cx| editor.deactivated(cx)); } fn navigate(&mut self, data: Box, cx: &mut ViewContext) -> bool { self.editor .update(cx, |editor, cx| editor.navigate(data, cx)) } fn tab_tooltip_text(&self, _: &AppContext) -> Option { Some("Project Diagnostics".into()) } fn tab_content(&self, params: TabContentParams, _: &WindowContext) -> AnyElement { if self.summary.error_count == 0 && self.summary.warning_count == 0 { Label::new("No problems") .color(params.text_color()) .into_any_element() } else { h_flex() .gap_1() .when(self.summary.error_count > 0, |then| { then.child( h_flex() .gap_1() .child(Icon::new(IconName::XCircle).color(Color::Error)) .child( Label::new(self.summary.error_count.to_string()) .color(params.text_color()), ), ) }) .when(self.summary.warning_count > 0, |then| { then.child( h_flex() .gap_1() .child(Icon::new(IconName::ExclamationTriangle).color(Color::Warning)) .child( Label::new(self.summary.warning_count.to_string()) .color(params.text_color()), ), ) }) .into_any_element() } } fn telemetry_event_text(&self) -> Option<&'static str> { Some("project diagnostics") } fn for_each_project_item( &self, cx: &AppContext, f: &mut dyn FnMut(gpui::EntityId, &dyn project::Item), ) { self.editor.for_each_project_item(cx, f) } fn is_singleton(&self, _: &AppContext) -> bool { false } fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext) { self.editor.update(cx, |editor, _| { editor.set_nav_history(Some(nav_history)); }); } fn clone_on_split( &self, _workspace_id: Option, cx: &mut ViewContext, ) -> Option> where Self: Sized, { Some(cx.new_view(|cx| { GroupedDiagnosticsEditor::new(self.project.clone(), self.workspace.clone(), cx) })) } fn is_dirty(&self, cx: &AppContext) -> bool { self.excerpts.read(cx).is_dirty(cx) } fn has_conflict(&self, cx: &AppContext) -> bool { self.excerpts.read(cx).has_conflict(cx) } fn can_save(&self, _: &AppContext) -> bool { true } fn save( &mut self, format: bool, project: Model, cx: &mut ViewContext, ) -> Task> { self.editor.save(format, project, cx) } fn save_as( &mut self, _: Model, _: ProjectPath, _: &mut ViewContext, ) -> Task> { unreachable!() } fn reload(&mut self, project: Model, cx: &mut ViewContext) -> Task> { self.editor.reload(project, cx) } fn act_as_type<'a>( &'a self, type_id: TypeId, self_handle: &'a View, _: &'a AppContext, ) -> Option { if type_id == TypeId::of::() { Some(self_handle.to_any()) } else if type_id == TypeId::of::() { Some(self.editor.to_any()) } else { None } } fn breadcrumb_location(&self) -> ToolbarItemLocation { ToolbarItemLocation::PrimaryLeft } fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option> { self.editor.breadcrumbs(theme, cx) } fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext) { self.editor .update(cx, |editor, cx| editor.added_to_workspace(workspace, cx)); } } fn compare_data_locations( old: &DiagnosticData, new: &DiagnosticData, snapshot: &BufferSnapshot, ) -> Ordering { compare_diagnostics(&old.entry, &new.entry, snapshot) .then_with(|| old.language_server_id.cmp(&new.language_server_id)) } fn compare_diagnostics( old: &DiagnosticEntry, new: &DiagnosticEntry, snapshot: &BufferSnapshot, ) -> Ordering { compare_diagnostic_ranges(&old.range, &new.range, snapshot) .then_with(|| old.diagnostic.message.cmp(&new.diagnostic.message)) } fn compare_diagnostic_ranges( old: &Range, new: &Range, snapshot: &BufferSnapshot, ) -> Ordering { // The diagnostics may point to a previously open Buffer for this file. if !old.start.is_valid(snapshot) || !new.start.is_valid(snapshot) { return Ordering::Greater; } old.start .to_offset(snapshot) .cmp(&new.start.to_offset(snapshot)) .then_with(|| { old.end .to_offset(snapshot) .cmp(&new.end.to_offset(snapshot)) }) } // TODO wrong? What to do here instead? fn compare_diagnostic_range_edges( old: &Range, new: &Range, snapshot: &BufferSnapshot, ) -> (Ordering, Ordering) { // The diagnostics may point to a previously open Buffer for this file. let start_cmp = match (old.start.is_valid(snapshot), new.start.is_valid(snapshot)) { (false, false) => old.start.offset.cmp(&new.start.offset), (false, true) => Ordering::Greater, (true, false) => Ordering::Less, (true, true) => old.start.cmp(&new.start, snapshot), }; let end_cmp = old .end .to_offset(snapshot) .cmp(&new.end.to_offset(snapshot)); (start_cmp, end_cmp) } #[derive(Debug)] struct PathUpdate { path_excerpts_borders: (Option, Option), latest_excerpt_id: ExcerptId, new_diagnostics: Vec<(DiagnosticData, Option)>, diagnostics_by_row_label: BTreeMap)>, blocks_to_remove: HashSet, unchanged_blocks: HashMap, excerpts_with_new_diagnostics: HashSet, excerpts_to_remove: Vec, excerpt_expands: HashMap<(ExpandExcerptDirection, u32), Vec>, excerpts_to_add: HashMap>>, first_excerpt_id: Option, last_excerpt_id: Option, } impl PathUpdate { fn new( path_excerpts_borders: (Option, Option), buffer_snapshot: &BufferSnapshot, server_to_update: Option, max_severity: DiagnosticSeverity, path_state: &PathState, ) -> Self { let mut blocks_to_remove = HashSet::default(); let mut removed_groups = HashSet::default(); let mut new_diagnostics = path_state .diagnostics .iter() .filter(|(diagnostic_data, _)| { server_to_update.map_or(true, |server_id| { diagnostic_data.language_server_id != server_id }) }) .filter(|(diagnostic_data, block_id)| { let diagnostic = &diagnostic_data.entry.diagnostic; let retain = !diagnostic.is_primary || diagnostic.severity <= max_severity; if !retain { removed_groups.insert(diagnostic.group_id); blocks_to_remove.insert(*block_id); } retain }) .map(|(diagnostic, block_id)| (diagnostic.clone(), Some(*block_id))) .collect::>(); new_diagnostics.retain(|(diagnostic_data, block_id)| { let retain = !removed_groups.contains(&diagnostic_data.entry.diagnostic.group_id); if !retain { if let Some(block_id) = block_id { blocks_to_remove.insert(*block_id); } } retain }); for (server_id, group) in buffer_snapshot .diagnostic_groups(server_to_update) .into_iter() .filter(|(_, group)| { group.entries[group.primary_ix].diagnostic.severity <= max_severity }) { for (diagnostic_index, diagnostic) in group.entries.iter().enumerate() { let new_data = DiagnosticData { language_server_id: server_id, is_primary: diagnostic_index == group.primary_ix, entry: diagnostic.clone(), }; let (Ok(i) | Err(i)) = new_diagnostics.binary_search_by(|probe| { compare_data_locations(&probe.0, &new_data, &buffer_snapshot) }); new_diagnostics.insert(i, (new_data, None)); } } let latest_excerpt_id = path_excerpts_borders.0.unwrap_or_else(|| ExcerptId::min()); Self { latest_excerpt_id, path_excerpts_borders, new_diagnostics, blocks_to_remove, diagnostics_by_row_label: BTreeMap::new(), excerpts_to_remove: Vec::new(), excerpts_with_new_diagnostics: HashSet::default(), unchanged_blocks: HashMap::default(), excerpts_to_add: HashMap::default(), excerpt_expands: HashMap::default(), first_excerpt_id: None, last_excerpt_id: None, } } fn prepare_excerpt_data<'a>( &'a mut self, context: u32, multi_buffer_snapshot: MultiBufferSnapshot, buffer_snapshot: BufferSnapshot, current_diagnostics: impl Iterator + 'a, ) { let mut current_diagnostics = current_diagnostics.fuse().peekable(); let mut excerpts_to_expand = HashMap::>::default(); let mut current_excerpts = path_state_excerpts( self.path_excerpts_borders.0, self.path_excerpts_borders.1, &multi_buffer_snapshot, ) .fuse() .peekable(); for (diagnostic_index, (new_diagnostic, existing_block)) in self.new_diagnostics.iter().enumerate() { if let Some(existing_block) = existing_block { self.unchanged_blocks .insert(diagnostic_index, *existing_block); } loop { match current_excerpts.peek() { None => { let excerpt_ranges = self .excerpts_to_add .entry(self.latest_excerpt_id) .or_default(); let new_range = new_diagnostic.entry.range.clone(); let (Ok(i) | Err(i)) = excerpt_ranges.binary_search_by(|probe| { compare_diagnostic_ranges(probe, &new_range, &buffer_snapshot) }); excerpt_ranges.insert(i, new_range); break; } Some((current_excerpt_id, _, current_excerpt_range)) => { match compare_diagnostic_range_edges( ¤t_excerpt_range.context, &new_diagnostic.entry.range, &buffer_snapshot, ) { /* new_s new_e ----[---->><<----]-- cur_s cur_e */ ( Ordering::Less | Ordering::Equal, Ordering::Greater | Ordering::Equal, ) => { self.excerpts_with_new_diagnostics .insert(*current_excerpt_id); if self.first_excerpt_id.is_none() { self.first_excerpt_id = Some(*current_excerpt_id); } self.last_excerpt_id = Some(*current_excerpt_id); break; } /* cur_s cur_e ---->>>>>[--]<<<<<-- new_s new_e */ ( Ordering::Greater | Ordering::Equal, Ordering::Less | Ordering::Equal, ) => { let expand_up = current_excerpt_range .context .start .to_point(&buffer_snapshot) .row .saturating_sub( new_diagnostic .entry .range .start .to_point(&buffer_snapshot) .row, ); let expand_down = new_diagnostic .entry .range .end .to_point(&buffer_snapshot) .row .saturating_sub( current_excerpt_range .context .end .to_point(&buffer_snapshot) .row, ); let expand_value = excerpts_to_expand .entry(*current_excerpt_id) .or_default() .entry(ExpandExcerptDirection::UpAndDown) .or_default(); *expand_value = (*expand_value).max(expand_up).max(expand_down); self.excerpts_with_new_diagnostics .insert(*current_excerpt_id); if self.first_excerpt_id.is_none() { self.first_excerpt_id = Some(*current_excerpt_id); } self.last_excerpt_id = Some(*current_excerpt_id); break; } /* new_s new_e > < ----[---->>>]<<<<<-- cur_s cur_e or new_s new_e > < ----[----]-->>><<<-- cur_s cur_e */ (Ordering::Less, Ordering::Less) => { if current_excerpt_range .context .end .cmp(&new_diagnostic.entry.range.start, &buffer_snapshot) .is_ge() { let expand_down = new_diagnostic .entry .range .end .to_point(&buffer_snapshot) .row .saturating_sub( current_excerpt_range .context .end .to_point(&buffer_snapshot) .row, ); let expand_value = excerpts_to_expand .entry(*current_excerpt_id) .or_default() .entry(ExpandExcerptDirection::Down) .or_default(); *expand_value = (*expand_value).max(expand_down); self.excerpts_with_new_diagnostics .insert(*current_excerpt_id); if self.first_excerpt_id.is_none() { self.first_excerpt_id = Some(*current_excerpt_id); } self.last_excerpt_id = Some(*current_excerpt_id); break; } else if !self .excerpts_with_new_diagnostics .contains(current_excerpt_id) { self.excerpts_to_remove.push(*current_excerpt_id); } } /* cur_s cur_e ---->>>>>[<<<<----]-- > < new_s new_e or cur_s cur_e ---->>><<<--[----]-- > < new_s new_e */ (Ordering::Greater, Ordering::Greater) => { if current_excerpt_range .context .start .cmp(&new_diagnostic.entry.range.end, &buffer_snapshot) .is_le() { let expand_up = current_excerpt_range .context .start .to_point(&buffer_snapshot) .row .saturating_sub( new_diagnostic .entry .range .start .to_point(&buffer_snapshot) .row, ); let expand_value = excerpts_to_expand .entry(*current_excerpt_id) .or_default() .entry(ExpandExcerptDirection::Up) .or_default(); *expand_value = (*expand_value).max(expand_up); self.excerpts_with_new_diagnostics .insert(*current_excerpt_id); if self.first_excerpt_id.is_none() { self.first_excerpt_id = Some(*current_excerpt_id); } self.last_excerpt_id = Some(*current_excerpt_id); break; } else { let excerpt_ranges = self .excerpts_to_add .entry(self.latest_excerpt_id) .or_default(); let new_range = new_diagnostic.entry.range.clone(); let (Ok(i) | Err(i)) = excerpt_ranges.binary_search_by(|probe| { compare_diagnostic_ranges( probe, &new_range, &buffer_snapshot, ) }); excerpt_ranges.insert(i, new_range); break; } } } if let Some((next_id, ..)) = current_excerpts.next() { self.latest_excerpt_id = next_id; } } } } loop { match current_diagnostics.peek() { None => break, Some((current_diagnostic, current_block)) => { match compare_data_locations( current_diagnostic, new_diagnostic, &buffer_snapshot, ) { Ordering::Less => { self.blocks_to_remove.insert(*current_block); } Ordering::Equal => { if current_diagnostic.diagnostic_entries_equal(&new_diagnostic) { self.unchanged_blocks .insert(diagnostic_index, *current_block); } else { self.blocks_to_remove.insert(*current_block); } let _ = current_diagnostics.next(); break; } Ordering::Greater => break, } let _ = current_diagnostics.next(); } } } } self.excerpts_to_remove.retain(|excerpt_id| { !self.excerpts_with_new_diagnostics.contains(excerpt_id) && !excerpts_to_expand.contains_key(excerpt_id) }); self.excerpts_to_remove.extend( current_excerpts .filter(|(excerpt_id, ..)| { !self.excerpts_with_new_diagnostics.contains(excerpt_id) && !excerpts_to_expand.contains_key(excerpt_id) }) .map(|(excerpt_id, ..)| excerpt_id), ); let mut excerpt_expands = HashMap::default(); for (excerpt_id, directions) in excerpts_to_expand { let excerpt_expand = if directions.len() > 1 { Some(( ExpandExcerptDirection::UpAndDown, directions .values() .max() .copied() .unwrap_or_default() .max(context), )) } else { directions .into_iter() .next() .map(|(direction, expand)| (direction, expand.max(context))) }; if let Some(expand) = excerpt_expand { excerpt_expands .entry(expand) .or_insert_with(|| Vec::new()) .push(excerpt_id); } } self.blocks_to_remove .extend(current_diagnostics.map(|(_, block_id)| block_id)); } fn apply_excerpt_changes( &mut self, path_state: &mut PathState, context: u32, buffer_snapshot: BufferSnapshot, multi_buffer: &mut MultiBuffer, buffer: Model, cx: &mut gpui::ModelContext, ) { let max_point = buffer_snapshot.max_point(); for (after_excerpt_id, ranges) in std::mem::take(&mut self.excerpts_to_add) { let ranges = ranges .into_iter() .map(|range| { let mut extended_point_range = range.to_point(&buffer_snapshot); extended_point_range.start.row = extended_point_range.start.row.saturating_sub(context); extended_point_range.start.column = 0; extended_point_range.end.row = (extended_point_range.end.row + context).min(max_point.row); extended_point_range.end.column = u32::MAX; let extended_start = buffer_snapshot.clip_point(extended_point_range.start, Bias::Left); let extended_end = buffer_snapshot.clip_point(extended_point_range.end, Bias::Right); extended_start..extended_end }) .collect::>(); let (joined_ranges, _) = build_excerpt_ranges(&buffer_snapshot, &ranges, context); let excerpts = multi_buffer.insert_excerpts_after( after_excerpt_id, buffer.clone(), joined_ranges, cx, ); if self.first_excerpt_id.is_none() { self.first_excerpt_id = excerpts.first().copied(); } self.last_excerpt_id = excerpts.last().copied(); } for ((direction, line_count), excerpts) in std::mem::take(&mut self.excerpt_expands) { multi_buffer.expand_excerpts(excerpts, line_count, direction, cx); } multi_buffer.remove_excerpts(std::mem::take(&mut self.excerpts_to_remove), cx); path_state.first_excerpt_id = self.first_excerpt_id; path_state.last_excerpt_id = self.last_excerpt_id; } fn prepare_blocks_to_insert( &mut self, editor: View, multi_buffer_snapshot: MultiBufferSnapshot, ) -> Vec> { let mut updated_excerpts = path_state_excerpts( self.path_excerpts_borders.0, self.path_excerpts_borders.1, &multi_buffer_snapshot, ) .fuse() .peekable(); let mut used_labels = BTreeMap::new(); self.diagnostics_by_row_label = self.new_diagnostics.iter().enumerate().fold( BTreeMap::new(), |mut diagnostics_by_row_label, (diagnostic_index, (diagnostic, existing_block))| { let new_diagnostic = &diagnostic.entry; let block_position = new_diagnostic.range.start; let excerpt_id = loop { match updated_excerpts.peek() { None => break None, Some((excerpt_id, excerpt_buffer_snapshot, excerpt_range)) => { let excerpt_range = &excerpt_range.context; match block_position.cmp(&excerpt_range.start, excerpt_buffer_snapshot) { Ordering::Less => break None, Ordering::Equal | Ordering::Greater => match block_position .cmp(&excerpt_range.end, excerpt_buffer_snapshot) { Ordering::Equal | Ordering::Less => break Some(*excerpt_id), Ordering::Greater => { let _ = updated_excerpts.next(); } }, } } } }; let Some(position_in_multi_buffer) = excerpt_id.and_then(|excerpt_id| { multi_buffer_snapshot.anchor_in_excerpt(excerpt_id, block_position) }) else { return diagnostics_by_row_label; }; let multi_buffer_row = MultiBufferRow( position_in_multi_buffer .to_point(&multi_buffer_snapshot) .row, ); let grouped_diagnostics = &mut diagnostics_by_row_label .entry(multi_buffer_row) .or_insert_with(|| (position_in_multi_buffer, Vec::new())) .1; let new_label = used_labels .entry(multi_buffer_row) .or_insert_with(|| HashSet::default()) .insert(( new_diagnostic.diagnostic.source.as_deref(), new_diagnostic.diagnostic.message.as_str(), )); if !new_label || !grouped_diagnostics.is_empty() { if let Some(existing_block) = existing_block { self.blocks_to_remove.insert(*existing_block); } if let Some(block_id) = self.unchanged_blocks.remove(&diagnostic_index) { self.blocks_to_remove.insert(block_id); } } if new_label { let (Ok(i) | Err(i)) = grouped_diagnostics.binary_search_by(|&probe| { let a = &self.new_diagnostics[probe].0.entry.diagnostic; let b = &self.new_diagnostics[diagnostic_index].0.entry.diagnostic; a.group_id .cmp(&b.group_id) .then_with(|| a.is_primary.cmp(&b.is_primary).reverse()) .then_with(|| a.severity.cmp(&b.severity)) }); grouped_diagnostics.insert(i, diagnostic_index); } diagnostics_by_row_label }, ); self.diagnostics_by_row_label .values() .filter_map(|(earliest_in_row_position, diagnostics_at_line)| { let earliest_in_row_position = *earliest_in_row_position; match diagnostics_at_line.len() { 0 => None, len => { if len == 1 { let i = diagnostics_at_line.first().copied()?; if self.unchanged_blocks.contains_key(&i) { return None; } } let lines_in_first_message = diagnostic_text_lines( &self .new_diagnostics .get(diagnostics_at_line.first().copied()?)? .0 .entry .diagnostic, ); let folded_block_height = lines_in_first_message.clamp(1, 2); let diagnostics_to_render = Arc::new( diagnostics_at_line .iter() .filter_map(|&index| self.new_diagnostics.get(index)) .map(|(diagnostic_data, _)| { diagnostic_data.entry.diagnostic.clone() }) .collect::>(), ); Some(BlockProperties { position: earliest_in_row_position, height: folded_block_height, style: BlockStyle::Sticky, render: render_same_line_diagnostics( Arc::new(AtomicBool::new(false)), diagnostics_to_render, editor.clone(), folded_block_height, ), disposition: BlockDisposition::Above, }) } } }) .collect() } fn new_blocks( mut self, new_block_ids: Vec, ) -> Vec<(DiagnosticData, CustomBlockId)> { let mut new_block_ids = new_block_ids.into_iter().fuse(); for (_, (_, grouped_diagnostics)) in self.diagnostics_by_row_label { let mut created_block_id = None; match grouped_diagnostics.len() { 0 => { debug_panic!("Unexpected empty diagnostics group"); continue; } 1 => { let index = grouped_diagnostics[0]; if let Some(&block_id) = self.unchanged_blocks.get(&index) { self.new_diagnostics[index].1 = Some(block_id); } else { let Some(block_id) = created_block_id.get_or_insert_with(|| new_block_ids.next()) else { debug_panic!("Expected a new block for each new diagnostic"); continue; }; self.new_diagnostics[index].1 = Some(*block_id); } } _ => { let Some(block_id) = created_block_id.get_or_insert_with(|| new_block_ids.next()) else { debug_panic!("Expected a new block for each new diagnostic group"); continue; }; for i in grouped_diagnostics { self.new_diagnostics[i].1 = Some(*block_id); } } } } self.new_diagnostics .into_iter() .filter_map(|(diagnostic, block_id)| Some((diagnostic, block_id?))) .collect() } } fn render_same_line_diagnostics( expanded: Arc, diagnostics: Arc>, editor_handle: View, folded_block_height: u8, ) -> RenderBlock { Box::new(move |cx: &mut BlockContext| { let block_id = match cx.block_id { BlockId::Custom(block_id) => block_id, _ => { debug_panic!("Expected a block id for the diagnostics block"); return div().into_any_element(); } }; let Some(first_diagnostic) = diagnostics.first() else { debug_panic!("Expected at least one diagnostic"); return div().into_any_element(); }; let button_expanded = expanded.clone(); let expanded = expanded.load(atomic::Ordering::Acquire); let expand_label = if expanded { '-' } else { '+' }; let first_diagnostics_height = diagnostic_text_lines(first_diagnostic); let extra_diagnostics = diagnostics.len() - 1; let toggle_expand_label = if folded_block_height == first_diagnostics_height && extra_diagnostics == 0 { None } else if extra_diagnostics > 0 { Some(format!("{expand_label}{extra_diagnostics}")) } else { Some(expand_label.to_string()) }; let expanded_block_height = diagnostics .iter() .map(|diagnostic| diagnostic_text_lines(diagnostic)) .sum::(); let editor_handle = editor_handle.clone(); let parent = h_flex() .items_start() .child(v_flex().size_full().map(|parent| { if let Some(label) = toggle_expand_label { parent.child(Button::new(cx.block_id, label).on_click({ let diagnostics = Arc::clone(&diagnostics); move |_, cx| { let new_expanded = !expanded; button_expanded.store(new_expanded, atomic::Ordering::Release); let new_size = if new_expanded { expanded_block_height } else { folded_block_height }; editor_handle.update(cx, |editor, cx| { editor.replace_blocks( HashMap::from_iter(Some(( block_id, ( Some(new_size), render_same_line_diagnostics( Arc::clone(&button_expanded), Arc::clone(&diagnostics), editor_handle.clone(), folded_block_height, ), ), ))), None, cx, ) }); } })) } else { parent.child( h_flex() .size(IconSize::default().rems()) .invisible() .flex_none(), ) } })); let max_message_rows = if expanded { None } else { Some(folded_block_height) }; let mut renderer = diagnostic_block_renderer(first_diagnostic.clone(), max_message_rows, false, true); let mut diagnostics_element = v_flex(); diagnostics_element = diagnostics_element.child(renderer(cx)); if expanded { for diagnostic in diagnostics.iter().skip(1) { let mut renderer = diagnostic_block_renderer(diagnostic.clone(), None, false, true); diagnostics_element = diagnostics_element.child(renderer(cx)); } } parent.child(diagnostics_element).into_any_element() }) } fn diagnostic_text_lines(diagnostic: &language::Diagnostic) -> u8 { diagnostic.message.matches('\n').count() as u8 + 1 } fn path_state_excerpts( after_excerpt_id: Option, before_excerpt_id: Option, multi_buffer_snapshot: &editor::MultiBufferSnapshot, ) -> impl Iterator)> { multi_buffer_snapshot .excerpts() .skip_while(move |&(excerpt_id, ..)| match after_excerpt_id { Some(after_excerpt_id) => after_excerpt_id != excerpt_id, None => false, }) .filter(move |&(excerpt_id, ..)| after_excerpt_id != Some(excerpt_id)) .take_while(move |&(excerpt_id, ..)| match before_excerpt_id { Some(before_excerpt_id) => before_excerpt_id != excerpt_id, None => true, }) }