1use anyhow::Result;
2use collections::{BTreeMap, BTreeSet, HashMap, HashSet};
3use editor::{
4 diagnostic_block_renderer,
5 display_map::{
6 BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock,
7 TransformBlockId,
8 },
9 scroll::Autoscroll,
10 Bias, Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, ToPoint,
11};
12use futures::{
13 channel::mpsc::{self, UnboundedSender},
14 StreamExt as _,
15};
16use gpui::{
17 actions, div, AnyElement, AnyView, AppContext, Context, EventEmitter, FocusHandle,
18 FocusableView, InteractiveElement, IntoElement, Model, ParentElement, Render, SharedString,
19 Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowContext,
20};
21use language::{
22 Buffer, BufferSnapshot, DiagnosticEntry, DiagnosticSeverity, OffsetRangeExt, ToOffset,
23 ToPoint as _,
24};
25use lsp::LanguageServerId;
26use multi_buffer::{build_excerpt_ranges, ExpandExcerptDirection, MultiBufferRow};
27use project::{DiagnosticSummary, Project, ProjectPath};
28use settings::Settings;
29use std::{
30 any::{Any, TypeId},
31 cmp::Ordering,
32 ops::Range,
33 sync::{
34 atomic::{self, AtomicBool},
35 Arc,
36 },
37};
38use theme::ActiveTheme;
39use ui::{h_flex, prelude::*, Icon, IconName, Label};
40use util::{debug_panic, ResultExt};
41use workspace::{
42 item::{BreadcrumbText, Item, ItemEvent, ItemHandle, TabContentParams},
43 ItemNavHistory, Pane, ToolbarItemLocation, Workspace,
44};
45
46use crate::project_diagnostics_settings::ProjectDiagnosticsSettings;
47actions!(grouped_diagnostics, [Deploy, ToggleWarnings]);
48
49pub fn init(cx: &mut AppContext) {
50 cx.observe_new_views(GroupedDiagnosticsEditor::register)
51 .detach();
52}
53
54struct GroupedDiagnosticsEditor {
55 project: Model<Project>,
56 workspace: WeakView<Workspace>,
57 focus_handle: FocusHandle,
58 editor: View<Editor>,
59 summary: DiagnosticSummary,
60 excerpts: Model<MultiBuffer>,
61 path_states: Vec<PathState>,
62 paths_to_update: BTreeSet<(ProjectPath, LanguageServerId)>,
63 include_warnings: bool,
64 context: u32,
65 update_paths_tx: UnboundedSender<(ProjectPath, Option<LanguageServerId>)>,
66 _update_excerpts_task: Task<Result<()>>,
67 _subscription: Subscription,
68}
69
70struct PathState {
71 path: ProjectPath,
72 first_excerpt_id: Option<ExcerptId>,
73 last_excerpt_id: Option<ExcerptId>,
74 diagnostics: Vec<(DiagnosticData, BlockId)>,
75}
76
77#[derive(Debug, Clone)]
78struct DiagnosticData {
79 language_server_id: LanguageServerId,
80 is_primary: bool,
81 entry: DiagnosticEntry<language::Anchor>,
82}
83
84impl DiagnosticData {
85 fn diagnostic_entries_equal(&self, other: &DiagnosticData) -> bool {
86 self.language_server_id == other.language_server_id
87 && self.is_primary == other.is_primary
88 && self.entry.range == other.entry.range
89 && equal_without_group_ids(&self.entry.diagnostic, &other.entry.diagnostic)
90 }
91}
92
93// `group_id` can differ between LSP server diagnostics output,
94// hence ignore it when checking diagnostics for updates.
95fn equal_without_group_ids(a: &language::Diagnostic, b: &language::Diagnostic) -> bool {
96 a.source == b.source
97 && a.code == b.code
98 && a.severity == b.severity
99 && a.message == b.message
100 && a.is_primary == b.is_primary
101 && a.is_disk_based == b.is_disk_based
102 && a.is_unnecessary == b.is_unnecessary
103}
104
105impl EventEmitter<EditorEvent> for GroupedDiagnosticsEditor {}
106
107impl Render for GroupedDiagnosticsEditor {
108 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
109 let child = if self.path_states.is_empty() {
110 div()
111 .bg(cx.theme().colors().editor_background)
112 .flex()
113 .items_center()
114 .justify_center()
115 .size_full()
116 .child(Label::new("No problems in workspace"))
117 } else {
118 div().size_full().child(self.editor.clone())
119 };
120
121 div()
122 .track_focus(&self.focus_handle)
123 .when(self.path_states.is_empty(), |el| {
124 el.key_context("EmptyPane")
125 })
126 .size_full()
127 .on_action(cx.listener(Self::toggle_warnings))
128 .child(child)
129 }
130}
131
132impl GroupedDiagnosticsEditor {
133 fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
134 workspace.register_action(Self::deploy);
135 }
136
137 fn new_with_context(
138 context: u32,
139 project_handle: Model<Project>,
140 workspace: WeakView<Workspace>,
141 cx: &mut ViewContext<Self>,
142 ) -> Self {
143 let project_event_subscription =
144 cx.subscribe(&project_handle, |this, project, event, cx| match event {
145 project::Event::DiskBasedDiagnosticsStarted { .. } => {
146 cx.notify();
147 }
148 project::Event::DiskBasedDiagnosticsFinished { language_server_id } => {
149 log::debug!("disk based diagnostics finished for server {language_server_id}");
150 this.enqueue_update_stale_excerpts(Some(*language_server_id));
151 }
152 project::Event::DiagnosticsUpdated {
153 language_server_id,
154 path,
155 } => {
156 this.paths_to_update
157 .insert((path.clone(), *language_server_id));
158 this.summary = project.read(cx).diagnostic_summary(false, cx);
159 cx.emit(EditorEvent::TitleChanged);
160
161 if this.editor.focus_handle(cx).contains_focused(cx) || this.focus_handle.contains_focused(cx) {
162 log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. recording change");
163 } else {
164 log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. updating excerpts");
165 this.enqueue_update_stale_excerpts(Some(*language_server_id));
166 }
167 }
168 _ => {}
169 });
170
171 let focus_handle = cx.focus_handle();
172 cx.on_focus_in(&focus_handle, |this, cx| this.focus_in(cx))
173 .detach();
174 cx.on_focus_out(&focus_handle, |this, _event, cx| this.focus_out(cx))
175 .detach();
176
177 let excerpts = cx.new_model(|cx| {
178 MultiBuffer::new(
179 project_handle.read(cx).replica_id(),
180 project_handle.read(cx).capability(),
181 )
182 });
183 let editor = cx.new_view(|cx| {
184 let mut editor =
185 Editor::for_multibuffer(excerpts.clone(), Some(project_handle.clone()), false, cx);
186 editor.set_vertical_scroll_margin(5, cx);
187 editor
188 });
189 cx.subscribe(&editor, |this, _editor, event: &EditorEvent, cx| {
190 cx.emit(event.clone());
191 match event {
192 EditorEvent::Focused => {
193 if this.path_states.is_empty() {
194 cx.focus(&this.focus_handle);
195 }
196 }
197 EditorEvent::Blurred => this.enqueue_update_stale_excerpts(None),
198 _ => {}
199 }
200 })
201 .detach();
202
203 let (update_excerpts_tx, mut update_excerpts_rx) = mpsc::unbounded();
204
205 let project = project_handle.read(cx);
206 let mut this = Self {
207 project: project_handle.clone(),
208 context,
209 summary: project.diagnostic_summary(false, cx),
210 workspace,
211 excerpts,
212 focus_handle,
213 editor,
214 path_states: Vec::new(),
215 paths_to_update: BTreeSet::new(),
216 include_warnings: ProjectDiagnosticsSettings::get_global(cx).include_warnings,
217 update_paths_tx: update_excerpts_tx,
218 _update_excerpts_task: cx.spawn(move |this, mut cx| async move {
219 while let Some((path, language_server_id)) = update_excerpts_rx.next().await {
220 if let Some(buffer) = project_handle
221 .update(&mut cx, |project, cx| project.open_buffer(path.clone(), cx))?
222 .await
223 .log_err()
224 {
225 this.update(&mut cx, |this, cx| {
226 this.update_excerpts(path, language_server_id, buffer, cx);
227 })?;
228 }
229 }
230 anyhow::Ok(())
231 }),
232 _subscription: project_event_subscription,
233 };
234 this.enqueue_update_all_excerpts(cx);
235 this
236 }
237
238 fn new(
239 project_handle: Model<Project>,
240 workspace: WeakView<Workspace>,
241 cx: &mut ViewContext<Self>,
242 ) -> Self {
243 Self::new_with_context(
244 editor::DEFAULT_MULTIBUFFER_CONTEXT,
245 project_handle,
246 workspace,
247 cx,
248 )
249 }
250
251 fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
252 if let Some(existing) = workspace.item_of_type::<GroupedDiagnosticsEditor>(cx) {
253 workspace.activate_item(&existing, cx);
254 } else {
255 let workspace_handle = cx.view().downgrade();
256 let diagnostics = cx.new_view(|cx| {
257 GroupedDiagnosticsEditor::new(workspace.project().clone(), workspace_handle, cx)
258 });
259 workspace.add_item_to_active_pane(Box::new(diagnostics), None, cx);
260 }
261 }
262
263 fn toggle_warnings(&mut self, _: &ToggleWarnings, cx: &mut ViewContext<Self>) {
264 self.include_warnings = !self.include_warnings;
265 self.enqueue_update_all_excerpts(cx);
266 cx.notify();
267 }
268
269 fn focus_in(&mut self, cx: &mut ViewContext<Self>) {
270 if self.focus_handle.is_focused(cx) && !self.path_states.is_empty() {
271 self.editor.focus_handle(cx).focus(cx)
272 }
273 }
274
275 fn focus_out(&mut self, cx: &mut ViewContext<Self>) {
276 if !self.focus_handle.is_focused(cx) && !self.editor.focus_handle(cx).is_focused(cx) {
277 self.enqueue_update_stale_excerpts(None);
278 }
279 }
280
281 /// Enqueue an update of all excerpts. Updates all paths that either
282 /// currently have diagnostics or are currently present in this view.
283 fn enqueue_update_all_excerpts(&mut self, cx: &mut ViewContext<Self>) {
284 self.project.update(cx, |project, cx| {
285 let mut paths = project
286 .diagnostic_summaries(false, cx)
287 .map(|(path, _, _)| path)
288 .collect::<BTreeSet<_>>();
289 paths.extend(self.path_states.iter().map(|state| state.path.clone()));
290 for path in paths {
291 self.update_paths_tx.unbounded_send((path, None)).unwrap();
292 }
293 });
294 }
295
296 /// Enqueue an update of the excerpts for any path whose diagnostics are known
297 /// to have changed. If a language server id is passed, then only the excerpts for
298 /// that language server's diagnostics will be updated. Otherwise, all stale excerpts
299 /// will be refreshed.
300 fn enqueue_update_stale_excerpts(&mut self, language_server_id: Option<LanguageServerId>) {
301 for (path, server_id) in &self.paths_to_update {
302 if language_server_id.map_or(true, |id| id == *server_id) {
303 self.update_paths_tx
304 .unbounded_send((path.clone(), Some(*server_id)))
305 .unwrap();
306 }
307 }
308 }
309
310 fn update_excerpts(
311 &mut self,
312 path_to_update: ProjectPath,
313 server_to_update: Option<LanguageServerId>,
314 buffer: Model<Buffer>,
315 cx: &mut ViewContext<Self>,
316 ) {
317 self.paths_to_update.retain(|(path, server_id)| {
318 *path != path_to_update
319 || server_to_update.map_or(false, |to_update| *server_id != to_update)
320 });
321
322 // TODO kb change selections as in the old panel, to the next primary diagnostics
323 // TODO kb make [shift-]f8 to work, jump to the next block group
324 let _was_empty = self.path_states.is_empty();
325 let path_ix = match self.path_states.binary_search_by(|probe| {
326 project::compare_paths((&probe.path.path, true), (&path_to_update.path, true))
327 }) {
328 Ok(ix) => ix,
329 Err(ix) => {
330 self.path_states.insert(
331 ix,
332 PathState {
333 path: path_to_update.clone(),
334 diagnostics: Vec::new(),
335 last_excerpt_id: None,
336 first_excerpt_id: None,
337 },
338 );
339 ix
340 }
341 };
342
343 // TODO kb when warnings are turned off, there's a lot of refresh for many paths happening, why?
344 let max_severity = if self.include_warnings {
345 DiagnosticSeverity::WARNING
346 } else {
347 DiagnosticSeverity::ERROR
348 };
349
350 let excerpt_borders = self.excerpt_borders_for_path(path_ix);
351 let path_state = &mut self.path_states[path_ix];
352 let buffer_snapshot = buffer.read(cx).snapshot();
353
354 let mut path_update = PathUpdate::new(
355 excerpt_borders,
356 &buffer_snapshot,
357 server_to_update,
358 max_severity,
359 path_state,
360 );
361 path_update.prepare_excerpt_data(
362 self.context,
363 self.excerpts.read(cx).snapshot(cx),
364 buffer.read(cx).snapshot(),
365 path_state.diagnostics.iter(),
366 );
367 self.excerpts.update(cx, |multi_buffer, cx| {
368 path_update.apply_excerpt_changes(
369 path_state,
370 self.context,
371 buffer_snapshot,
372 multi_buffer,
373 buffer,
374 cx,
375 );
376 });
377
378 let new_multi_buffer_snapshot = self.excerpts.read(cx).snapshot(cx);
379 let blocks_to_insert =
380 path_update.prepare_blocks_to_insert(self.editor.clone(), new_multi_buffer_snapshot);
381
382 let new_block_ids = self.editor.update(cx, |editor, cx| {
383 editor.remove_blocks(std::mem::take(&mut path_update.blocks_to_remove), None, cx);
384 editor.insert_blocks(blocks_to_insert, Some(Autoscroll::fit()), cx)
385 });
386 path_state.diagnostics = path_update.new_blocks(new_block_ids);
387
388 if self.path_states.is_empty() {
389 if self.editor.focus_handle(cx).is_focused(cx) {
390 cx.focus(&self.focus_handle);
391 }
392 } else if self.focus_handle.is_focused(cx) {
393 let focus_handle = self.editor.focus_handle(cx);
394 cx.focus(&focus_handle);
395 }
396
397 #[cfg(test)]
398 self.check_invariants(cx);
399
400 cx.notify();
401 }
402
403 fn excerpt_borders_for_path(&self, path_ix: usize) -> (Option<ExcerptId>, Option<ExcerptId>) {
404 let previous_path_state_ix =
405 Some(path_ix.saturating_sub(1)).filter(|&previous_path_ix| previous_path_ix != path_ix);
406 let next_path_state_ix = path_ix + 1;
407 let start = previous_path_state_ix.and_then(|i| {
408 self.path_states[..=i]
409 .iter()
410 .rev()
411 .find_map(|state| state.last_excerpt_id)
412 });
413 let end = self.path_states[next_path_state_ix..]
414 .iter()
415 .find_map(|state| state.first_excerpt_id);
416 (start, end)
417 }
418
419 #[cfg(test)]
420 fn check_invariants(&self, cx: &mut ViewContext<Self>) {
421 let mut excerpts = Vec::new();
422 for (id, buffer, _) in self.excerpts.read(cx).snapshot(cx).excerpts() {
423 if let Some(file) = buffer.file() {
424 excerpts.push((id, file.path().clone()));
425 }
426 }
427
428 let mut prev_path = None;
429 for (_, path) in &excerpts {
430 if let Some(prev_path) = prev_path {
431 if path < prev_path {
432 panic!("excerpts are not sorted by path {:?}", excerpts);
433 }
434 }
435 prev_path = Some(path);
436 }
437 }
438}
439
440impl FocusableView for GroupedDiagnosticsEditor {
441 fn focus_handle(&self, _: &AppContext) -> FocusHandle {
442 self.focus_handle.clone()
443 }
444}
445
446impl Item for GroupedDiagnosticsEditor {
447 type Event = EditorEvent;
448
449 fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
450 Editor::to_item_events(event, f)
451 }
452
453 fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
454 self.editor.update(cx, |editor, cx| editor.deactivated(cx));
455 }
456
457 fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
458 self.editor
459 .update(cx, |editor, cx| editor.navigate(data, cx))
460 }
461
462 fn tab_tooltip_text(&self, _: &AppContext) -> Option<SharedString> {
463 Some("Project Diagnostics".into())
464 }
465
466 fn tab_content(&self, params: TabContentParams, _: &WindowContext) -> AnyElement {
467 if self.summary.error_count == 0 && self.summary.warning_count == 0 {
468 Label::new("No problems")
469 .color(if params.selected {
470 Color::Default
471 } else {
472 Color::Muted
473 })
474 .into_any_element()
475 } else {
476 h_flex()
477 .gap_1()
478 .when(self.summary.error_count > 0, |then| {
479 then.child(
480 h_flex()
481 .gap_1()
482 .child(Icon::new(IconName::XCircle).color(Color::Error))
483 .child(Label::new(self.summary.error_count.to_string()).color(
484 if params.selected {
485 Color::Default
486 } else {
487 Color::Muted
488 },
489 )),
490 )
491 })
492 .when(self.summary.warning_count > 0, |then| {
493 then.child(
494 h_flex()
495 .gap_1()
496 .child(Icon::new(IconName::ExclamationTriangle).color(Color::Warning))
497 .child(Label::new(self.summary.warning_count.to_string()).color(
498 if params.selected {
499 Color::Default
500 } else {
501 Color::Muted
502 },
503 )),
504 )
505 })
506 .into_any_element()
507 }
508 }
509
510 fn telemetry_event_text(&self) -> Option<&'static str> {
511 Some("project diagnostics")
512 }
513
514 fn for_each_project_item(
515 &self,
516 cx: &AppContext,
517 f: &mut dyn FnMut(gpui::EntityId, &dyn project::Item),
518 ) {
519 self.editor.for_each_project_item(cx, f)
520 }
521
522 fn is_singleton(&self, _: &AppContext) -> bool {
523 false
524 }
525
526 fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext<Self>) {
527 self.editor.update(cx, |editor, _| {
528 editor.set_nav_history(Some(nav_history));
529 });
530 }
531
532 fn clone_on_split(
533 &self,
534 _workspace_id: Option<workspace::WorkspaceId>,
535 cx: &mut ViewContext<Self>,
536 ) -> Option<View<Self>>
537 where
538 Self: Sized,
539 {
540 Some(cx.new_view(|cx| {
541 GroupedDiagnosticsEditor::new(self.project.clone(), self.workspace.clone(), cx)
542 }))
543 }
544
545 fn is_dirty(&self, cx: &AppContext) -> bool {
546 self.excerpts.read(cx).is_dirty(cx)
547 }
548
549 fn has_conflict(&self, cx: &AppContext) -> bool {
550 self.excerpts.read(cx).has_conflict(cx)
551 }
552
553 fn can_save(&self, _: &AppContext) -> bool {
554 true
555 }
556
557 fn save(
558 &mut self,
559 format: bool,
560 project: Model<Project>,
561 cx: &mut ViewContext<Self>,
562 ) -> Task<Result<()>> {
563 self.editor.save(format, project, cx)
564 }
565
566 fn save_as(
567 &mut self,
568 _: Model<Project>,
569 _: ProjectPath,
570 _: &mut ViewContext<Self>,
571 ) -> Task<Result<()>> {
572 unreachable!()
573 }
574
575 fn reload(&mut self, project: Model<Project>, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
576 self.editor.reload(project, cx)
577 }
578
579 fn act_as_type<'a>(
580 &'a self,
581 type_id: TypeId,
582 self_handle: &'a View<Self>,
583 _: &'a AppContext,
584 ) -> Option<AnyView> {
585 if type_id == TypeId::of::<Self>() {
586 Some(self_handle.to_any())
587 } else if type_id == TypeId::of::<Editor>() {
588 Some(self.editor.to_any())
589 } else {
590 None
591 }
592 }
593
594 fn breadcrumb_location(&self) -> ToolbarItemLocation {
595 ToolbarItemLocation::PrimaryLeft
596 }
597
598 fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
599 self.editor.breadcrumbs(theme, cx)
600 }
601
602 fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
603 self.editor
604 .update(cx, |editor, cx| editor.added_to_workspace(workspace, cx));
605 }
606
607 fn serialized_item_kind() -> Option<&'static str> {
608 Some("diagnostics")
609 }
610
611 fn deserialize(
612 project: Model<Project>,
613 workspace: WeakView<Workspace>,
614 _workspace_id: workspace::WorkspaceId,
615 _item_id: workspace::ItemId,
616 cx: &mut ViewContext<Pane>,
617 ) -> Task<Result<View<Self>>> {
618 Task::ready(Ok(cx.new_view(|cx| Self::new(project, workspace, cx))))
619 }
620}
621
622fn compare_data_locations(
623 old: &DiagnosticData,
624 new: &DiagnosticData,
625 snapshot: &BufferSnapshot,
626) -> Ordering {
627 compare_diagnostics(&old.entry, &new.entry, snapshot)
628 .then_with(|| old.language_server_id.cmp(&new.language_server_id))
629}
630
631fn compare_diagnostics(
632 old: &DiagnosticEntry<language::Anchor>,
633 new: &DiagnosticEntry<language::Anchor>,
634 snapshot: &BufferSnapshot,
635) -> Ordering {
636 compare_diagnostic_ranges(&old.range, &new.range, snapshot)
637 .then_with(|| old.diagnostic.message.cmp(&new.diagnostic.message))
638}
639
640fn compare_diagnostic_ranges(
641 old: &Range<language::Anchor>,
642 new: &Range<language::Anchor>,
643 snapshot: &BufferSnapshot,
644) -> Ordering {
645 // The diagnostics may point to a previously open Buffer for this file.
646 if !old.start.is_valid(snapshot) || !new.start.is_valid(snapshot) {
647 return Ordering::Greater;
648 }
649
650 old.start
651 .to_offset(snapshot)
652 .cmp(&new.start.to_offset(snapshot))
653 .then_with(|| {
654 old.end
655 .to_offset(snapshot)
656 .cmp(&new.end.to_offset(snapshot))
657 })
658}
659
660// TODO kb wrong? What to do here instead?
661fn compare_diagnostic_range_edges(
662 old: &Range<language::Anchor>,
663 new: &Range<language::Anchor>,
664 snapshot: &BufferSnapshot,
665) -> (Ordering, Ordering) {
666 // The diagnostics may point to a previously open Buffer for this file.
667 let start_cmp = match (old.start.is_valid(snapshot), new.start.is_valid(snapshot)) {
668 (false, false) => old.start.offset.cmp(&new.start.offset),
669 (false, true) => Ordering::Greater,
670 (true, false) => Ordering::Less,
671 (true, true) => old.start.cmp(&new.start, snapshot),
672 };
673
674 let end_cmp = old
675 .end
676 .to_offset(snapshot)
677 .cmp(&new.end.to_offset(snapshot));
678 (start_cmp, end_cmp)
679}
680
681#[derive(Debug)]
682struct PathUpdate {
683 path_excerpts_borders: (Option<ExcerptId>, Option<ExcerptId>),
684 latest_excerpt_id: ExcerptId,
685 new_diagnostics: Vec<(DiagnosticData, Option<BlockId>)>,
686 diagnostics_by_row_label: BTreeMap<MultiBufferRow, (editor::Anchor, Vec<usize>)>,
687 blocks_to_remove: HashSet<BlockId>,
688 unchanged_blocks: HashMap<usize, BlockId>,
689 excerpts_with_new_diagnostics: HashSet<ExcerptId>,
690 excerpts_to_remove: Vec<ExcerptId>,
691 excerpt_expands: HashMap<(ExpandExcerptDirection, u32), Vec<ExcerptId>>,
692 excerpts_to_add: HashMap<ExcerptId, Vec<Range<language::Anchor>>>,
693 first_excerpt_id: Option<ExcerptId>,
694 last_excerpt_id: Option<ExcerptId>,
695}
696
697impl PathUpdate {
698 fn new(
699 path_excerpts_borders: (Option<ExcerptId>, Option<ExcerptId>),
700 buffer_snapshot: &BufferSnapshot,
701 server_to_update: Option<LanguageServerId>,
702 max_severity: DiagnosticSeverity,
703 path_state: &PathState,
704 ) -> Self {
705 let mut blocks_to_remove = HashSet::default();
706 let mut removed_groups = HashSet::default();
707 let mut new_diagnostics = path_state
708 .diagnostics
709 .iter()
710 .filter(|(diagnostic_data, _)| {
711 server_to_update.map_or(true, |server_id| {
712 diagnostic_data.language_server_id != server_id
713 })
714 })
715 .filter(|(diagnostic_data, block_id)| {
716 let diagnostic = &diagnostic_data.entry.diagnostic;
717 let retain = !diagnostic.is_primary || diagnostic.severity <= max_severity;
718 if !retain {
719 removed_groups.insert(diagnostic.group_id);
720 blocks_to_remove.insert(*block_id);
721 }
722 retain
723 })
724 .map(|(diagnostic, block_id)| (diagnostic.clone(), Some(*block_id)))
725 .collect::<Vec<_>>();
726 new_diagnostics.retain(|(diagnostic_data, block_id)| {
727 let retain = !removed_groups.contains(&diagnostic_data.entry.diagnostic.group_id);
728 if !retain {
729 if let Some(block_id) = block_id {
730 blocks_to_remove.insert(*block_id);
731 }
732 }
733 retain
734 });
735 for (server_id, group) in buffer_snapshot
736 .diagnostic_groups(server_to_update)
737 .into_iter()
738 .filter(|(_, group)| {
739 group.entries[group.primary_ix].diagnostic.severity <= max_severity
740 })
741 {
742 for (diagnostic_index, diagnostic) in group.entries.iter().enumerate() {
743 let new_data = DiagnosticData {
744 language_server_id: server_id,
745 is_primary: diagnostic_index == group.primary_ix,
746 entry: diagnostic.clone(),
747 };
748 let (Ok(i) | Err(i)) = new_diagnostics.binary_search_by(|probe| {
749 compare_data_locations(&probe.0, &new_data, &buffer_snapshot)
750 });
751 new_diagnostics.insert(i, (new_data, None));
752 }
753 }
754
755 let latest_excerpt_id = path_excerpts_borders.0.unwrap_or_else(|| ExcerptId::min());
756 Self {
757 latest_excerpt_id,
758 path_excerpts_borders,
759 new_diagnostics,
760 blocks_to_remove,
761 diagnostics_by_row_label: BTreeMap::new(),
762 excerpts_to_remove: Vec::new(),
763 excerpts_with_new_diagnostics: HashSet::default(),
764 unchanged_blocks: HashMap::default(),
765 excerpts_to_add: HashMap::default(),
766 excerpt_expands: HashMap::default(),
767 first_excerpt_id: None,
768 last_excerpt_id: None,
769 }
770 }
771
772 fn prepare_excerpt_data<'a>(
773 &'a mut self,
774 context: u32,
775 multi_buffer_snapshot: MultiBufferSnapshot,
776 buffer_snapshot: BufferSnapshot,
777 current_diagnostics: impl Iterator<Item = &'a (DiagnosticData, BlockId)> + 'a,
778 ) {
779 let mut current_diagnostics = current_diagnostics.fuse().peekable();
780 let mut excerpts_to_expand =
781 HashMap::<ExcerptId, HashMap<ExpandExcerptDirection, u32>>::default();
782 let mut current_excerpts = path_state_excerpts(
783 self.path_excerpts_borders.0,
784 self.path_excerpts_borders.1,
785 &multi_buffer_snapshot,
786 )
787 .fuse()
788 .peekable();
789
790 for (diagnostic_index, (new_diagnostic, existing_block)) in
791 self.new_diagnostics.iter().enumerate()
792 {
793 if let Some(existing_block) = existing_block {
794 self.unchanged_blocks
795 .insert(diagnostic_index, *existing_block);
796 }
797
798 loop {
799 match current_excerpts.peek() {
800 None => {
801 let excerpt_ranges = self
802 .excerpts_to_add
803 .entry(self.latest_excerpt_id)
804 .or_default();
805 let new_range = new_diagnostic.entry.range.clone();
806 let (Ok(i) | Err(i)) = excerpt_ranges.binary_search_by(|probe| {
807 compare_diagnostic_ranges(probe, &new_range, &buffer_snapshot)
808 });
809 excerpt_ranges.insert(i, new_range);
810 break;
811 }
812 Some((current_excerpt_id, _, current_excerpt_range)) => {
813 match compare_diagnostic_range_edges(
814 ¤t_excerpt_range.context,
815 &new_diagnostic.entry.range,
816 &buffer_snapshot,
817 ) {
818 /*
819 new_s new_e
820 ----[---->><<----]--
821 cur_s cur_e
822 */
823 (
824 Ordering::Less | Ordering::Equal,
825 Ordering::Greater | Ordering::Equal,
826 ) => {
827 self.excerpts_with_new_diagnostics
828 .insert(*current_excerpt_id);
829 if self.first_excerpt_id.is_none() {
830 self.first_excerpt_id = Some(*current_excerpt_id);
831 }
832 self.last_excerpt_id = Some(*current_excerpt_id);
833 break;
834 }
835 /*
836 cur_s cur_e
837 ---->>>>>[--]<<<<<--
838 new_s new_e
839 */
840 (
841 Ordering::Greater | Ordering::Equal,
842 Ordering::Less | Ordering::Equal,
843 ) => {
844 let expand_up = current_excerpt_range
845 .context
846 .start
847 .to_point(&buffer_snapshot)
848 .row
849 .saturating_sub(
850 new_diagnostic
851 .entry
852 .range
853 .start
854 .to_point(&buffer_snapshot)
855 .row,
856 );
857 let expand_down = new_diagnostic
858 .entry
859 .range
860 .end
861 .to_point(&buffer_snapshot)
862 .row
863 .saturating_sub(
864 current_excerpt_range
865 .context
866 .end
867 .to_point(&buffer_snapshot)
868 .row,
869 );
870 let expand_value = excerpts_to_expand
871 .entry(*current_excerpt_id)
872 .or_default()
873 .entry(ExpandExcerptDirection::UpAndDown)
874 .or_default();
875 *expand_value = (*expand_value).max(expand_up).max(expand_down);
876 self.excerpts_with_new_diagnostics
877 .insert(*current_excerpt_id);
878 if self.first_excerpt_id.is_none() {
879 self.first_excerpt_id = Some(*current_excerpt_id);
880 }
881 self.last_excerpt_id = Some(*current_excerpt_id);
882 break;
883 }
884 /*
885 new_s new_e
886 > <
887 ----[---->>>]<<<<<--
888 cur_s cur_e
889
890 or
891 new_s new_e
892 > <
893 ----[----]-->>><<<--
894 cur_s cur_e
895 */
896 (Ordering::Less, Ordering::Less) => {
897 if current_excerpt_range
898 .context
899 .end
900 .cmp(&new_diagnostic.entry.range.start, &buffer_snapshot)
901 .is_ge()
902 {
903 let expand_down = new_diagnostic
904 .entry
905 .range
906 .end
907 .to_point(&buffer_snapshot)
908 .row
909 .saturating_sub(
910 current_excerpt_range
911 .context
912 .end
913 .to_point(&buffer_snapshot)
914 .row,
915 );
916 let expand_value = excerpts_to_expand
917 .entry(*current_excerpt_id)
918 .or_default()
919 .entry(ExpandExcerptDirection::Down)
920 .or_default();
921 *expand_value = (*expand_value).max(expand_down);
922 self.excerpts_with_new_diagnostics
923 .insert(*current_excerpt_id);
924 if self.first_excerpt_id.is_none() {
925 self.first_excerpt_id = Some(*current_excerpt_id);
926 }
927 self.last_excerpt_id = Some(*current_excerpt_id);
928 break;
929 } else if !self
930 .excerpts_with_new_diagnostics
931 .contains(current_excerpt_id)
932 {
933 self.excerpts_to_remove.push(*current_excerpt_id);
934 }
935 }
936 /*
937 cur_s cur_e
938 ---->>>>>[<<<<----]--
939 > <
940 new_s new_e
941
942 or
943 cur_s cur_e
944 ---->>><<<--[----]--
945 > <
946 new_s new_e
947 */
948 (Ordering::Greater, Ordering::Greater) => {
949 if current_excerpt_range
950 .context
951 .start
952 .cmp(&new_diagnostic.entry.range.end, &buffer_snapshot)
953 .is_le()
954 {
955 let expand_up = current_excerpt_range
956 .context
957 .start
958 .to_point(&buffer_snapshot)
959 .row
960 .saturating_sub(
961 new_diagnostic
962 .entry
963 .range
964 .start
965 .to_point(&buffer_snapshot)
966 .row,
967 );
968 let expand_value = excerpts_to_expand
969 .entry(*current_excerpt_id)
970 .or_default()
971 .entry(ExpandExcerptDirection::Up)
972 .or_default();
973 *expand_value = (*expand_value).max(expand_up);
974 self.excerpts_with_new_diagnostics
975 .insert(*current_excerpt_id);
976 if self.first_excerpt_id.is_none() {
977 self.first_excerpt_id = Some(*current_excerpt_id);
978 }
979 self.last_excerpt_id = Some(*current_excerpt_id);
980 break;
981 } else {
982 let excerpt_ranges = self
983 .excerpts_to_add
984 .entry(self.latest_excerpt_id)
985 .or_default();
986 let new_range = new_diagnostic.entry.range.clone();
987 let (Ok(i) | Err(i)) =
988 excerpt_ranges.binary_search_by(|probe| {
989 compare_diagnostic_ranges(
990 probe,
991 &new_range,
992 &buffer_snapshot,
993 )
994 });
995 excerpt_ranges.insert(i, new_range);
996 break;
997 }
998 }
999 }
1000 if let Some((next_id, ..)) = current_excerpts.next() {
1001 self.latest_excerpt_id = next_id;
1002 }
1003 }
1004 }
1005 }
1006
1007 loop {
1008 match current_diagnostics.peek() {
1009 None => break,
1010 Some((current_diagnostic, current_block)) => {
1011 match compare_data_locations(
1012 current_diagnostic,
1013 new_diagnostic,
1014 &buffer_snapshot,
1015 ) {
1016 Ordering::Less => {
1017 self.blocks_to_remove.insert(*current_block);
1018 }
1019 Ordering::Equal => {
1020 if current_diagnostic.diagnostic_entries_equal(&new_diagnostic) {
1021 self.unchanged_blocks
1022 .insert(diagnostic_index, *current_block);
1023 } else {
1024 self.blocks_to_remove.insert(*current_block);
1025 }
1026 let _ = current_diagnostics.next();
1027 break;
1028 }
1029 Ordering::Greater => break,
1030 }
1031 let _ = current_diagnostics.next();
1032 }
1033 }
1034 }
1035 }
1036
1037 self.excerpts_to_remove.retain(|excerpt_id| {
1038 !self.excerpts_with_new_diagnostics.contains(excerpt_id)
1039 && !excerpts_to_expand.contains_key(excerpt_id)
1040 });
1041 self.excerpts_to_remove.extend(
1042 current_excerpts
1043 .filter(|(excerpt_id, ..)| {
1044 !self.excerpts_with_new_diagnostics.contains(excerpt_id)
1045 && !excerpts_to_expand.contains_key(excerpt_id)
1046 })
1047 .map(|(excerpt_id, ..)| excerpt_id),
1048 );
1049 let mut excerpt_expands = HashMap::default();
1050 for (excerpt_id, directions) in excerpts_to_expand {
1051 let excerpt_expand = if directions.len() > 1 {
1052 Some((
1053 ExpandExcerptDirection::UpAndDown,
1054 directions
1055 .values()
1056 .max()
1057 .copied()
1058 .unwrap_or_default()
1059 .max(context),
1060 ))
1061 } else {
1062 directions
1063 .into_iter()
1064 .next()
1065 .map(|(direction, expand)| (direction, expand.max(context)))
1066 };
1067 if let Some(expand) = excerpt_expand {
1068 excerpt_expands
1069 .entry(expand)
1070 .or_insert_with(|| Vec::new())
1071 .push(excerpt_id);
1072 }
1073 }
1074 self.blocks_to_remove
1075 .extend(current_diagnostics.map(|(_, block_id)| block_id));
1076 }
1077
1078 fn apply_excerpt_changes(
1079 &mut self,
1080 path_state: &mut PathState,
1081 context: u32,
1082 buffer_snapshot: BufferSnapshot,
1083 multi_buffer: &mut MultiBuffer,
1084 buffer: Model<Buffer>,
1085 cx: &mut gpui::ModelContext<MultiBuffer>,
1086 ) {
1087 let max_point = buffer_snapshot.max_point();
1088 for (after_excerpt_id, ranges) in std::mem::take(&mut self.excerpts_to_add) {
1089 let ranges = ranges
1090 .into_iter()
1091 .map(|range| {
1092 let mut extended_point_range = range.to_point(&buffer_snapshot);
1093 extended_point_range.start.row =
1094 extended_point_range.start.row.saturating_sub(context);
1095 extended_point_range.start.column = 0;
1096 extended_point_range.end.row =
1097 (extended_point_range.end.row + context).min(max_point.row);
1098 extended_point_range.end.column = u32::MAX;
1099 let extended_start =
1100 buffer_snapshot.clip_point(extended_point_range.start, Bias::Left);
1101 let extended_end =
1102 buffer_snapshot.clip_point(extended_point_range.end, Bias::Right);
1103 extended_start..extended_end
1104 })
1105 .collect::<Vec<_>>();
1106 let (joined_ranges, _) = build_excerpt_ranges(&buffer_snapshot, &ranges, context);
1107 let excerpts = multi_buffer.insert_excerpts_after(
1108 after_excerpt_id,
1109 buffer.clone(),
1110 joined_ranges,
1111 cx,
1112 );
1113 if self.first_excerpt_id.is_none() {
1114 self.first_excerpt_id = excerpts.first().copied();
1115 }
1116 self.last_excerpt_id = excerpts.last().copied();
1117 }
1118 for ((direction, line_count), excerpts) in std::mem::take(&mut self.excerpt_expands) {
1119 multi_buffer.expand_excerpts(excerpts, line_count, direction, cx);
1120 }
1121 multi_buffer.remove_excerpts(std::mem::take(&mut self.excerpts_to_remove), cx);
1122 path_state.first_excerpt_id = self.first_excerpt_id;
1123 path_state.last_excerpt_id = self.last_excerpt_id;
1124 }
1125
1126 fn prepare_blocks_to_insert(
1127 &mut self,
1128 editor: View<Editor>,
1129 multi_buffer_snapshot: MultiBufferSnapshot,
1130 ) -> Vec<BlockProperties<editor::Anchor>> {
1131 let mut updated_excerpts = path_state_excerpts(
1132 self.path_excerpts_borders.0,
1133 self.path_excerpts_borders.1,
1134 &multi_buffer_snapshot,
1135 )
1136 .fuse()
1137 .peekable();
1138 let mut used_labels = BTreeMap::new();
1139 self.diagnostics_by_row_label = self.new_diagnostics.iter().enumerate().fold(
1140 BTreeMap::new(),
1141 |mut diagnostics_by_row_label, (diagnostic_index, (diagnostic, existing_block))| {
1142 let new_diagnostic = &diagnostic.entry;
1143 let block_position = new_diagnostic.range.start;
1144 let excerpt_id = loop {
1145 match updated_excerpts.peek() {
1146 None => break None,
1147 Some((excerpt_id, excerpt_buffer_snapshot, excerpt_range)) => {
1148 let excerpt_range = &excerpt_range.context;
1149 match block_position.cmp(&excerpt_range.start, excerpt_buffer_snapshot)
1150 {
1151 Ordering::Less => break None,
1152 Ordering::Equal | Ordering::Greater => match block_position
1153 .cmp(&excerpt_range.end, excerpt_buffer_snapshot)
1154 {
1155 Ordering::Equal | Ordering::Less => break Some(*excerpt_id),
1156 Ordering::Greater => {
1157 let _ = updated_excerpts.next();
1158 }
1159 },
1160 }
1161 }
1162 }
1163 };
1164
1165 let Some(position_in_multi_buffer) = excerpt_id.and_then(|excerpt_id| {
1166 multi_buffer_snapshot.anchor_in_excerpt(excerpt_id, block_position)
1167 }) else {
1168 return diagnostics_by_row_label;
1169 };
1170
1171 let multi_buffer_row = MultiBufferRow(
1172 position_in_multi_buffer
1173 .to_point(&multi_buffer_snapshot)
1174 .row,
1175 );
1176
1177 let grouped_diagnostics = &mut diagnostics_by_row_label
1178 .entry(multi_buffer_row)
1179 .or_insert_with(|| (position_in_multi_buffer, Vec::new()))
1180 .1;
1181 let new_label = used_labels
1182 .entry(multi_buffer_row)
1183 .or_insert_with(|| HashSet::default())
1184 .insert((
1185 new_diagnostic.diagnostic.source.as_deref(),
1186 new_diagnostic.diagnostic.message.as_str(),
1187 ));
1188
1189 if !new_label || !grouped_diagnostics.is_empty() {
1190 if let Some(existing_block) = existing_block {
1191 self.blocks_to_remove.insert(*existing_block);
1192 }
1193 if let Some(block_id) = self.unchanged_blocks.remove(&diagnostic_index) {
1194 self.blocks_to_remove.insert(block_id);
1195 }
1196 }
1197 if new_label {
1198 let (Ok(i) | Err(i)) = grouped_diagnostics.binary_search_by(|&probe| {
1199 let a = &self.new_diagnostics[probe].0.entry.diagnostic;
1200 let b = &self.new_diagnostics[diagnostic_index].0.entry.diagnostic;
1201 a.group_id
1202 .cmp(&b.group_id)
1203 .then_with(|| a.is_primary.cmp(&b.is_primary).reverse())
1204 .then_with(|| a.severity.cmp(&b.severity))
1205 });
1206 grouped_diagnostics.insert(i, diagnostic_index);
1207 }
1208
1209 diagnostics_by_row_label
1210 },
1211 );
1212
1213 self.diagnostics_by_row_label
1214 .values()
1215 .filter_map(|(earliest_in_row_position, diagnostics_at_line)| {
1216 let earliest_in_row_position = *earliest_in_row_position;
1217 match diagnostics_at_line.len() {
1218 0 => None,
1219 len => {
1220 if len == 1 {
1221 let i = diagnostics_at_line.first().copied()?;
1222 if self.unchanged_blocks.contains_key(&i) {
1223 return None;
1224 }
1225 }
1226 let lines_in_first_message = diagnostic_text_lines(
1227 &self
1228 .new_diagnostics
1229 .get(diagnostics_at_line.first().copied()?)?
1230 .0
1231 .entry
1232 .diagnostic,
1233 );
1234 let folded_block_height = lines_in_first_message.clamp(1, 2);
1235 let diagnostics_to_render = Arc::new(
1236 diagnostics_at_line
1237 .iter()
1238 .filter_map(|&index| self.new_diagnostics.get(index))
1239 .map(|(diagnostic_data, _)| {
1240 diagnostic_data.entry.diagnostic.clone()
1241 })
1242 .collect::<Vec<_>>(),
1243 );
1244 Some(BlockProperties {
1245 position: earliest_in_row_position,
1246 height: folded_block_height,
1247 style: BlockStyle::Sticky,
1248 render: render_same_line_diagnostics(
1249 Arc::new(AtomicBool::new(false)),
1250 diagnostics_to_render,
1251 editor.clone(),
1252 folded_block_height,
1253 ),
1254 disposition: BlockDisposition::Above,
1255 })
1256 }
1257 }
1258 })
1259 .collect()
1260 }
1261
1262 fn new_blocks(mut self, new_block_ids: Vec<BlockId>) -> Vec<(DiagnosticData, BlockId)> {
1263 let mut new_block_ids = new_block_ids.into_iter().fuse();
1264 for (_, (_, grouped_diagnostics)) in self.diagnostics_by_row_label {
1265 let mut created_block_id = None;
1266 match grouped_diagnostics.len() {
1267 0 => {
1268 debug_panic!("Unexpected empty diagnostics group");
1269 continue;
1270 }
1271 1 => {
1272 let index = grouped_diagnostics[0];
1273 if let Some(&block_id) = self.unchanged_blocks.get(&index) {
1274 self.new_diagnostics[index].1 = Some(block_id);
1275 } else {
1276 let Some(block_id) =
1277 created_block_id.get_or_insert_with(|| new_block_ids.next())
1278 else {
1279 debug_panic!("Expected a new block for each new diagnostic");
1280 continue;
1281 };
1282 self.new_diagnostics[index].1 = Some(*block_id);
1283 }
1284 }
1285 _ => {
1286 let Some(block_id) =
1287 created_block_id.get_or_insert_with(|| new_block_ids.next())
1288 else {
1289 debug_panic!("Expected a new block for each new diagnostic group");
1290 continue;
1291 };
1292 for i in grouped_diagnostics {
1293 self.new_diagnostics[i].1 = Some(*block_id);
1294 }
1295 }
1296 }
1297 }
1298
1299 self.new_diagnostics
1300 .into_iter()
1301 .filter_map(|(diagnostic, block_id)| Some((diagnostic, block_id?)))
1302 .collect()
1303 }
1304}
1305
1306fn render_same_line_diagnostics(
1307 expanded: Arc<AtomicBool>,
1308 diagnostics: Arc<Vec<language::Diagnostic>>,
1309 editor_handle: View<Editor>,
1310 folded_block_height: u8,
1311) -> RenderBlock {
1312 Box::new(move |cx: &mut BlockContext| {
1313 let block_id = match cx.transform_block_id {
1314 TransformBlockId::Block(block_id) => block_id,
1315 _ => {
1316 debug_panic!("Expected a block id for the diagnostics block");
1317 return div().into_any_element();
1318 }
1319 };
1320 let Some(first_diagnostic) = diagnostics.first() else {
1321 debug_panic!("Expected at least one diagnostic");
1322 return div().into_any_element();
1323 };
1324 let button_expanded = expanded.clone();
1325 let expanded = expanded.load(atomic::Ordering::Acquire);
1326 let expand_label = if expanded { '-' } else { '+' };
1327 let first_diagnostics_height = diagnostic_text_lines(first_diagnostic);
1328 let extra_diagnostics = diagnostics.len() - 1;
1329 let toggle_expand_label =
1330 if folded_block_height == first_diagnostics_height && extra_diagnostics == 0 {
1331 None
1332 } else if extra_diagnostics > 0 {
1333 Some(format!("{expand_label}{extra_diagnostics}"))
1334 } else {
1335 Some(expand_label.to_string())
1336 };
1337
1338 let expanded_block_height = diagnostics
1339 .iter()
1340 .map(|diagnostic| diagnostic_text_lines(diagnostic))
1341 .sum::<u8>();
1342 let editor_handle = editor_handle.clone();
1343 let mut parent = v_flex();
1344 let mut diagnostics_iter = diagnostics.iter().fuse();
1345 if let Some(first_diagnostic) = diagnostics_iter.next() {
1346 let mut renderer = diagnostic_block_renderer(
1347 first_diagnostic.clone(),
1348 Some(folded_block_height),
1349 false,
1350 true,
1351 );
1352 parent = parent.child(
1353 h_flex()
1354 .when_some(toggle_expand_label, |parent, label| {
1355 parent.child(Button::new(cx.transform_block_id, label).on_click({
1356 let diagnostics = Arc::clone(&diagnostics);
1357 move |_, cx| {
1358 let new_expanded = !expanded;
1359 button_expanded.store(new_expanded, atomic::Ordering::Release);
1360 let new_size = if new_expanded {
1361 expanded_block_height
1362 } else {
1363 folded_block_height
1364 };
1365 editor_handle.update(cx, |editor, cx| {
1366 editor.replace_blocks(
1367 HashMap::from_iter(Some((
1368 block_id,
1369 (
1370 Some(new_size),
1371 render_same_line_diagnostics(
1372 Arc::clone(&button_expanded),
1373 Arc::clone(&diagnostics),
1374 editor_handle.clone(),
1375 folded_block_height,
1376 ),
1377 ),
1378 ))),
1379 None,
1380 cx,
1381 )
1382 });
1383 }
1384 }))
1385 })
1386 .child(renderer(cx)),
1387 );
1388 }
1389 if expanded {
1390 for diagnostic in diagnostics_iter {
1391 let mut renderer = diagnostic_block_renderer(diagnostic.clone(), None, false, true);
1392 parent = parent.child(renderer(cx));
1393 }
1394 }
1395 parent.into_any_element()
1396 })
1397}
1398
1399fn diagnostic_text_lines(diagnostic: &language::Diagnostic) -> u8 {
1400 diagnostic.message.matches('\n').count() as u8 + 1
1401}
1402
1403fn path_state_excerpts(
1404 after_excerpt_id: Option<ExcerptId>,
1405 before_excerpt_id: Option<ExcerptId>,
1406 multi_buffer_snapshot: &editor::MultiBufferSnapshot,
1407) -> impl Iterator<Item = (ExcerptId, &BufferSnapshot, ExcerptRange<language::Anchor>)> {
1408 multi_buffer_snapshot
1409 .excerpts()
1410 .skip_while(move |&(excerpt_id, ..)| match after_excerpt_id {
1411 Some(after_excerpt_id) => after_excerpt_id != excerpt_id,
1412 None => false,
1413 })
1414 .filter(move |&(excerpt_id, ..)| after_excerpt_id != Some(excerpt_id))
1415 .take_while(move |&(excerpt_id, ..)| match before_excerpt_id {
1416 Some(before_excerpt_id) => before_excerpt_id != excerpt_id,
1417 None => true,
1418 })
1419}