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, 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
608fn compare_data_locations(
609 old: &DiagnosticData,
610 new: &DiagnosticData,
611 snapshot: &BufferSnapshot,
612) -> Ordering {
613 compare_diagnostics(&old.entry, &new.entry, snapshot)
614 .then_with(|| old.language_server_id.cmp(&new.language_server_id))
615}
616
617fn compare_diagnostics(
618 old: &DiagnosticEntry<language::Anchor>,
619 new: &DiagnosticEntry<language::Anchor>,
620 snapshot: &BufferSnapshot,
621) -> Ordering {
622 compare_diagnostic_ranges(&old.range, &new.range, snapshot)
623 .then_with(|| old.diagnostic.message.cmp(&new.diagnostic.message))
624}
625
626fn compare_diagnostic_ranges(
627 old: &Range<language::Anchor>,
628 new: &Range<language::Anchor>,
629 snapshot: &BufferSnapshot,
630) -> Ordering {
631 // The diagnostics may point to a previously open Buffer for this file.
632 if !old.start.is_valid(snapshot) || !new.start.is_valid(snapshot) {
633 return Ordering::Greater;
634 }
635
636 old.start
637 .to_offset(snapshot)
638 .cmp(&new.start.to_offset(snapshot))
639 .then_with(|| {
640 old.end
641 .to_offset(snapshot)
642 .cmp(&new.end.to_offset(snapshot))
643 })
644}
645
646// TODO kb wrong? What to do here instead?
647fn compare_diagnostic_range_edges(
648 old: &Range<language::Anchor>,
649 new: &Range<language::Anchor>,
650 snapshot: &BufferSnapshot,
651) -> (Ordering, Ordering) {
652 // The diagnostics may point to a previously open Buffer for this file.
653 let start_cmp = match (old.start.is_valid(snapshot), new.start.is_valid(snapshot)) {
654 (false, false) => old.start.offset.cmp(&new.start.offset),
655 (false, true) => Ordering::Greater,
656 (true, false) => Ordering::Less,
657 (true, true) => old.start.cmp(&new.start, snapshot),
658 };
659
660 let end_cmp = old
661 .end
662 .to_offset(snapshot)
663 .cmp(&new.end.to_offset(snapshot));
664 (start_cmp, end_cmp)
665}
666
667#[derive(Debug)]
668struct PathUpdate {
669 path_excerpts_borders: (Option<ExcerptId>, Option<ExcerptId>),
670 latest_excerpt_id: ExcerptId,
671 new_diagnostics: Vec<(DiagnosticData, Option<BlockId>)>,
672 diagnostics_by_row_label: BTreeMap<MultiBufferRow, (editor::Anchor, Vec<usize>)>,
673 blocks_to_remove: HashSet<BlockId>,
674 unchanged_blocks: HashMap<usize, BlockId>,
675 excerpts_with_new_diagnostics: HashSet<ExcerptId>,
676 excerpts_to_remove: Vec<ExcerptId>,
677 excerpt_expands: HashMap<(ExpandExcerptDirection, u32), Vec<ExcerptId>>,
678 excerpts_to_add: HashMap<ExcerptId, Vec<Range<language::Anchor>>>,
679 first_excerpt_id: Option<ExcerptId>,
680 last_excerpt_id: Option<ExcerptId>,
681}
682
683impl PathUpdate {
684 fn new(
685 path_excerpts_borders: (Option<ExcerptId>, Option<ExcerptId>),
686 buffer_snapshot: &BufferSnapshot,
687 server_to_update: Option<LanguageServerId>,
688 max_severity: DiagnosticSeverity,
689 path_state: &PathState,
690 ) -> Self {
691 let mut blocks_to_remove = HashSet::default();
692 let mut removed_groups = HashSet::default();
693 let mut new_diagnostics = path_state
694 .diagnostics
695 .iter()
696 .filter(|(diagnostic_data, _)| {
697 server_to_update.map_or(true, |server_id| {
698 diagnostic_data.language_server_id != server_id
699 })
700 })
701 .filter(|(diagnostic_data, block_id)| {
702 let diagnostic = &diagnostic_data.entry.diagnostic;
703 let retain = !diagnostic.is_primary || diagnostic.severity <= max_severity;
704 if !retain {
705 removed_groups.insert(diagnostic.group_id);
706 blocks_to_remove.insert(*block_id);
707 }
708 retain
709 })
710 .map(|(diagnostic, block_id)| (diagnostic.clone(), Some(*block_id)))
711 .collect::<Vec<_>>();
712 new_diagnostics.retain(|(diagnostic_data, block_id)| {
713 let retain = !removed_groups.contains(&diagnostic_data.entry.diagnostic.group_id);
714 if !retain {
715 if let Some(block_id) = block_id {
716 blocks_to_remove.insert(*block_id);
717 }
718 }
719 retain
720 });
721 for (server_id, group) in buffer_snapshot
722 .diagnostic_groups(server_to_update)
723 .into_iter()
724 .filter(|(_, group)| {
725 group.entries[group.primary_ix].diagnostic.severity <= max_severity
726 })
727 {
728 for (diagnostic_index, diagnostic) in group.entries.iter().enumerate() {
729 let new_data = DiagnosticData {
730 language_server_id: server_id,
731 is_primary: diagnostic_index == group.primary_ix,
732 entry: diagnostic.clone(),
733 };
734 let (Ok(i) | Err(i)) = new_diagnostics.binary_search_by(|probe| {
735 compare_data_locations(&probe.0, &new_data, &buffer_snapshot)
736 });
737 new_diagnostics.insert(i, (new_data, None));
738 }
739 }
740
741 let latest_excerpt_id = path_excerpts_borders.0.unwrap_or_else(|| ExcerptId::min());
742 Self {
743 latest_excerpt_id,
744 path_excerpts_borders,
745 new_diagnostics,
746 blocks_to_remove,
747 diagnostics_by_row_label: BTreeMap::new(),
748 excerpts_to_remove: Vec::new(),
749 excerpts_with_new_diagnostics: HashSet::default(),
750 unchanged_blocks: HashMap::default(),
751 excerpts_to_add: HashMap::default(),
752 excerpt_expands: HashMap::default(),
753 first_excerpt_id: None,
754 last_excerpt_id: None,
755 }
756 }
757
758 fn prepare_excerpt_data<'a>(
759 &'a mut self,
760 context: u32,
761 multi_buffer_snapshot: MultiBufferSnapshot,
762 buffer_snapshot: BufferSnapshot,
763 current_diagnostics: impl Iterator<Item = &'a (DiagnosticData, BlockId)> + 'a,
764 ) {
765 let mut current_diagnostics = current_diagnostics.fuse().peekable();
766 let mut excerpts_to_expand =
767 HashMap::<ExcerptId, HashMap<ExpandExcerptDirection, u32>>::default();
768 let mut current_excerpts = path_state_excerpts(
769 self.path_excerpts_borders.0,
770 self.path_excerpts_borders.1,
771 &multi_buffer_snapshot,
772 )
773 .fuse()
774 .peekable();
775
776 for (diagnostic_index, (new_diagnostic, existing_block)) in
777 self.new_diagnostics.iter().enumerate()
778 {
779 if let Some(existing_block) = existing_block {
780 self.unchanged_blocks
781 .insert(diagnostic_index, *existing_block);
782 }
783
784 loop {
785 match current_excerpts.peek() {
786 None => {
787 let excerpt_ranges = self
788 .excerpts_to_add
789 .entry(self.latest_excerpt_id)
790 .or_default();
791 let new_range = new_diagnostic.entry.range.clone();
792 let (Ok(i) | Err(i)) = excerpt_ranges.binary_search_by(|probe| {
793 compare_diagnostic_ranges(probe, &new_range, &buffer_snapshot)
794 });
795 excerpt_ranges.insert(i, new_range);
796 break;
797 }
798 Some((current_excerpt_id, _, current_excerpt_range)) => {
799 match compare_diagnostic_range_edges(
800 ¤t_excerpt_range.context,
801 &new_diagnostic.entry.range,
802 &buffer_snapshot,
803 ) {
804 /*
805 new_s new_e
806 ----[---->><<----]--
807 cur_s cur_e
808 */
809 (
810 Ordering::Less | Ordering::Equal,
811 Ordering::Greater | Ordering::Equal,
812 ) => {
813 self.excerpts_with_new_diagnostics
814 .insert(*current_excerpt_id);
815 if self.first_excerpt_id.is_none() {
816 self.first_excerpt_id = Some(*current_excerpt_id);
817 }
818 self.last_excerpt_id = Some(*current_excerpt_id);
819 break;
820 }
821 /*
822 cur_s cur_e
823 ---->>>>>[--]<<<<<--
824 new_s new_e
825 */
826 (
827 Ordering::Greater | Ordering::Equal,
828 Ordering::Less | Ordering::Equal,
829 ) => {
830 let expand_up = current_excerpt_range
831 .context
832 .start
833 .to_point(&buffer_snapshot)
834 .row
835 .saturating_sub(
836 new_diagnostic
837 .entry
838 .range
839 .start
840 .to_point(&buffer_snapshot)
841 .row,
842 );
843 let expand_down = new_diagnostic
844 .entry
845 .range
846 .end
847 .to_point(&buffer_snapshot)
848 .row
849 .saturating_sub(
850 current_excerpt_range
851 .context
852 .end
853 .to_point(&buffer_snapshot)
854 .row,
855 );
856 let expand_value = excerpts_to_expand
857 .entry(*current_excerpt_id)
858 .or_default()
859 .entry(ExpandExcerptDirection::UpAndDown)
860 .or_default();
861 *expand_value = (*expand_value).max(expand_up).max(expand_down);
862 self.excerpts_with_new_diagnostics
863 .insert(*current_excerpt_id);
864 if self.first_excerpt_id.is_none() {
865 self.first_excerpt_id = Some(*current_excerpt_id);
866 }
867 self.last_excerpt_id = Some(*current_excerpt_id);
868 break;
869 }
870 /*
871 new_s new_e
872 > <
873 ----[---->>>]<<<<<--
874 cur_s cur_e
875
876 or
877 new_s new_e
878 > <
879 ----[----]-->>><<<--
880 cur_s cur_e
881 */
882 (Ordering::Less, Ordering::Less) => {
883 if current_excerpt_range
884 .context
885 .end
886 .cmp(&new_diagnostic.entry.range.start, &buffer_snapshot)
887 .is_ge()
888 {
889 let expand_down = new_diagnostic
890 .entry
891 .range
892 .end
893 .to_point(&buffer_snapshot)
894 .row
895 .saturating_sub(
896 current_excerpt_range
897 .context
898 .end
899 .to_point(&buffer_snapshot)
900 .row,
901 );
902 let expand_value = excerpts_to_expand
903 .entry(*current_excerpt_id)
904 .or_default()
905 .entry(ExpandExcerptDirection::Down)
906 .or_default();
907 *expand_value = (*expand_value).max(expand_down);
908 self.excerpts_with_new_diagnostics
909 .insert(*current_excerpt_id);
910 if self.first_excerpt_id.is_none() {
911 self.first_excerpt_id = Some(*current_excerpt_id);
912 }
913 self.last_excerpt_id = Some(*current_excerpt_id);
914 break;
915 } else if !self
916 .excerpts_with_new_diagnostics
917 .contains(current_excerpt_id)
918 {
919 self.excerpts_to_remove.push(*current_excerpt_id);
920 }
921 }
922 /*
923 cur_s cur_e
924 ---->>>>>[<<<<----]--
925 > <
926 new_s new_e
927
928 or
929 cur_s cur_e
930 ---->>><<<--[----]--
931 > <
932 new_s new_e
933 */
934 (Ordering::Greater, Ordering::Greater) => {
935 if current_excerpt_range
936 .context
937 .start
938 .cmp(&new_diagnostic.entry.range.end, &buffer_snapshot)
939 .is_le()
940 {
941 let expand_up = current_excerpt_range
942 .context
943 .start
944 .to_point(&buffer_snapshot)
945 .row
946 .saturating_sub(
947 new_diagnostic
948 .entry
949 .range
950 .start
951 .to_point(&buffer_snapshot)
952 .row,
953 );
954 let expand_value = excerpts_to_expand
955 .entry(*current_excerpt_id)
956 .or_default()
957 .entry(ExpandExcerptDirection::Up)
958 .or_default();
959 *expand_value = (*expand_value).max(expand_up);
960 self.excerpts_with_new_diagnostics
961 .insert(*current_excerpt_id);
962 if self.first_excerpt_id.is_none() {
963 self.first_excerpt_id = Some(*current_excerpt_id);
964 }
965 self.last_excerpt_id = Some(*current_excerpt_id);
966 break;
967 } else {
968 let excerpt_ranges = self
969 .excerpts_to_add
970 .entry(self.latest_excerpt_id)
971 .or_default();
972 let new_range = new_diagnostic.entry.range.clone();
973 let (Ok(i) | Err(i)) =
974 excerpt_ranges.binary_search_by(|probe| {
975 compare_diagnostic_ranges(
976 probe,
977 &new_range,
978 &buffer_snapshot,
979 )
980 });
981 excerpt_ranges.insert(i, new_range);
982 break;
983 }
984 }
985 }
986 if let Some((next_id, ..)) = current_excerpts.next() {
987 self.latest_excerpt_id = next_id;
988 }
989 }
990 }
991 }
992
993 loop {
994 match current_diagnostics.peek() {
995 None => break,
996 Some((current_diagnostic, current_block)) => {
997 match compare_data_locations(
998 current_diagnostic,
999 new_diagnostic,
1000 &buffer_snapshot,
1001 ) {
1002 Ordering::Less => {
1003 self.blocks_to_remove.insert(*current_block);
1004 }
1005 Ordering::Equal => {
1006 if current_diagnostic.diagnostic_entries_equal(&new_diagnostic) {
1007 self.unchanged_blocks
1008 .insert(diagnostic_index, *current_block);
1009 } else {
1010 self.blocks_to_remove.insert(*current_block);
1011 }
1012 let _ = current_diagnostics.next();
1013 break;
1014 }
1015 Ordering::Greater => break,
1016 }
1017 let _ = current_diagnostics.next();
1018 }
1019 }
1020 }
1021 }
1022
1023 self.excerpts_to_remove.retain(|excerpt_id| {
1024 !self.excerpts_with_new_diagnostics.contains(excerpt_id)
1025 && !excerpts_to_expand.contains_key(excerpt_id)
1026 });
1027 self.excerpts_to_remove.extend(
1028 current_excerpts
1029 .filter(|(excerpt_id, ..)| {
1030 !self.excerpts_with_new_diagnostics.contains(excerpt_id)
1031 && !excerpts_to_expand.contains_key(excerpt_id)
1032 })
1033 .map(|(excerpt_id, ..)| excerpt_id),
1034 );
1035 let mut excerpt_expands = HashMap::default();
1036 for (excerpt_id, directions) in excerpts_to_expand {
1037 let excerpt_expand = if directions.len() > 1 {
1038 Some((
1039 ExpandExcerptDirection::UpAndDown,
1040 directions
1041 .values()
1042 .max()
1043 .copied()
1044 .unwrap_or_default()
1045 .max(context),
1046 ))
1047 } else {
1048 directions
1049 .into_iter()
1050 .next()
1051 .map(|(direction, expand)| (direction, expand.max(context)))
1052 };
1053 if let Some(expand) = excerpt_expand {
1054 excerpt_expands
1055 .entry(expand)
1056 .or_insert_with(|| Vec::new())
1057 .push(excerpt_id);
1058 }
1059 }
1060 self.blocks_to_remove
1061 .extend(current_diagnostics.map(|(_, block_id)| block_id));
1062 }
1063
1064 fn apply_excerpt_changes(
1065 &mut self,
1066 path_state: &mut PathState,
1067 context: u32,
1068 buffer_snapshot: BufferSnapshot,
1069 multi_buffer: &mut MultiBuffer,
1070 buffer: Model<Buffer>,
1071 cx: &mut gpui::ModelContext<MultiBuffer>,
1072 ) {
1073 let max_point = buffer_snapshot.max_point();
1074 for (after_excerpt_id, ranges) in std::mem::take(&mut self.excerpts_to_add) {
1075 let ranges = ranges
1076 .into_iter()
1077 .map(|range| {
1078 let mut extended_point_range = range.to_point(&buffer_snapshot);
1079 extended_point_range.start.row =
1080 extended_point_range.start.row.saturating_sub(context);
1081 extended_point_range.start.column = 0;
1082 extended_point_range.end.row =
1083 (extended_point_range.end.row + context).min(max_point.row);
1084 extended_point_range.end.column = u32::MAX;
1085 let extended_start =
1086 buffer_snapshot.clip_point(extended_point_range.start, Bias::Left);
1087 let extended_end =
1088 buffer_snapshot.clip_point(extended_point_range.end, Bias::Right);
1089 extended_start..extended_end
1090 })
1091 .collect::<Vec<_>>();
1092 let (joined_ranges, _) = build_excerpt_ranges(&buffer_snapshot, &ranges, context);
1093 let excerpts = multi_buffer.insert_excerpts_after(
1094 after_excerpt_id,
1095 buffer.clone(),
1096 joined_ranges,
1097 cx,
1098 );
1099 if self.first_excerpt_id.is_none() {
1100 self.first_excerpt_id = excerpts.first().copied();
1101 }
1102 self.last_excerpt_id = excerpts.last().copied();
1103 }
1104 for ((direction, line_count), excerpts) in std::mem::take(&mut self.excerpt_expands) {
1105 multi_buffer.expand_excerpts(excerpts, line_count, direction, cx);
1106 }
1107 multi_buffer.remove_excerpts(std::mem::take(&mut self.excerpts_to_remove), cx);
1108 path_state.first_excerpt_id = self.first_excerpt_id;
1109 path_state.last_excerpt_id = self.last_excerpt_id;
1110 }
1111
1112 fn prepare_blocks_to_insert(
1113 &mut self,
1114 editor: View<Editor>,
1115 multi_buffer_snapshot: MultiBufferSnapshot,
1116 ) -> Vec<BlockProperties<editor::Anchor>> {
1117 let mut updated_excerpts = path_state_excerpts(
1118 self.path_excerpts_borders.0,
1119 self.path_excerpts_borders.1,
1120 &multi_buffer_snapshot,
1121 )
1122 .fuse()
1123 .peekable();
1124 let mut used_labels = BTreeMap::new();
1125 self.diagnostics_by_row_label = self.new_diagnostics.iter().enumerate().fold(
1126 BTreeMap::new(),
1127 |mut diagnostics_by_row_label, (diagnostic_index, (diagnostic, existing_block))| {
1128 let new_diagnostic = &diagnostic.entry;
1129 let block_position = new_diagnostic.range.start;
1130 let excerpt_id = loop {
1131 match updated_excerpts.peek() {
1132 None => break None,
1133 Some((excerpt_id, excerpt_buffer_snapshot, excerpt_range)) => {
1134 let excerpt_range = &excerpt_range.context;
1135 match block_position.cmp(&excerpt_range.start, excerpt_buffer_snapshot)
1136 {
1137 Ordering::Less => break None,
1138 Ordering::Equal | Ordering::Greater => match block_position
1139 .cmp(&excerpt_range.end, excerpt_buffer_snapshot)
1140 {
1141 Ordering::Equal | Ordering::Less => break Some(*excerpt_id),
1142 Ordering::Greater => {
1143 let _ = updated_excerpts.next();
1144 }
1145 },
1146 }
1147 }
1148 }
1149 };
1150
1151 let Some(position_in_multi_buffer) = excerpt_id.and_then(|excerpt_id| {
1152 multi_buffer_snapshot.anchor_in_excerpt(excerpt_id, block_position)
1153 }) else {
1154 return diagnostics_by_row_label;
1155 };
1156
1157 let multi_buffer_row = MultiBufferRow(
1158 position_in_multi_buffer
1159 .to_point(&multi_buffer_snapshot)
1160 .row,
1161 );
1162
1163 let grouped_diagnostics = &mut diagnostics_by_row_label
1164 .entry(multi_buffer_row)
1165 .or_insert_with(|| (position_in_multi_buffer, Vec::new()))
1166 .1;
1167 let new_label = used_labels
1168 .entry(multi_buffer_row)
1169 .or_insert_with(|| HashSet::default())
1170 .insert((
1171 new_diagnostic.diagnostic.source.as_deref(),
1172 new_diagnostic.diagnostic.message.as_str(),
1173 ));
1174
1175 if !new_label || !grouped_diagnostics.is_empty() {
1176 if let Some(existing_block) = existing_block {
1177 self.blocks_to_remove.insert(*existing_block);
1178 }
1179 if let Some(block_id) = self.unchanged_blocks.remove(&diagnostic_index) {
1180 self.blocks_to_remove.insert(block_id);
1181 }
1182 }
1183 if new_label {
1184 let (Ok(i) | Err(i)) = grouped_diagnostics.binary_search_by(|&probe| {
1185 let a = &self.new_diagnostics[probe].0.entry.diagnostic;
1186 let b = &self.new_diagnostics[diagnostic_index].0.entry.diagnostic;
1187 a.group_id
1188 .cmp(&b.group_id)
1189 .then_with(|| a.is_primary.cmp(&b.is_primary).reverse())
1190 .then_with(|| a.severity.cmp(&b.severity))
1191 });
1192 grouped_diagnostics.insert(i, diagnostic_index);
1193 }
1194
1195 diagnostics_by_row_label
1196 },
1197 );
1198
1199 self.diagnostics_by_row_label
1200 .values()
1201 .filter_map(|(earliest_in_row_position, diagnostics_at_line)| {
1202 let earliest_in_row_position = *earliest_in_row_position;
1203 match diagnostics_at_line.len() {
1204 0 => None,
1205 len => {
1206 if len == 1 {
1207 let i = diagnostics_at_line.first().copied()?;
1208 if self.unchanged_blocks.contains_key(&i) {
1209 return None;
1210 }
1211 }
1212 let lines_in_first_message = diagnostic_text_lines(
1213 &self
1214 .new_diagnostics
1215 .get(diagnostics_at_line.first().copied()?)?
1216 .0
1217 .entry
1218 .diagnostic,
1219 );
1220 let folded_block_height = lines_in_first_message.clamp(1, 2);
1221 let diagnostics_to_render = Arc::new(
1222 diagnostics_at_line
1223 .iter()
1224 .filter_map(|&index| self.new_diagnostics.get(index))
1225 .map(|(diagnostic_data, _)| {
1226 diagnostic_data.entry.diagnostic.clone()
1227 })
1228 .collect::<Vec<_>>(),
1229 );
1230 Some(BlockProperties {
1231 position: earliest_in_row_position,
1232 height: folded_block_height,
1233 style: BlockStyle::Sticky,
1234 render: render_same_line_diagnostics(
1235 Arc::new(AtomicBool::new(false)),
1236 diagnostics_to_render,
1237 editor.clone(),
1238 folded_block_height,
1239 ),
1240 disposition: BlockDisposition::Above,
1241 })
1242 }
1243 }
1244 })
1245 .collect()
1246 }
1247
1248 fn new_blocks(mut self, new_block_ids: Vec<BlockId>) -> Vec<(DiagnosticData, BlockId)> {
1249 let mut new_block_ids = new_block_ids.into_iter().fuse();
1250 for (_, (_, grouped_diagnostics)) in self.diagnostics_by_row_label {
1251 let mut created_block_id = None;
1252 match grouped_diagnostics.len() {
1253 0 => {
1254 debug_panic!("Unexpected empty diagnostics group");
1255 continue;
1256 }
1257 1 => {
1258 let index = grouped_diagnostics[0];
1259 if let Some(&block_id) = self.unchanged_blocks.get(&index) {
1260 self.new_diagnostics[index].1 = Some(block_id);
1261 } else {
1262 let Some(block_id) =
1263 created_block_id.get_or_insert_with(|| new_block_ids.next())
1264 else {
1265 debug_panic!("Expected a new block for each new diagnostic");
1266 continue;
1267 };
1268 self.new_diagnostics[index].1 = Some(*block_id);
1269 }
1270 }
1271 _ => {
1272 let Some(block_id) =
1273 created_block_id.get_or_insert_with(|| new_block_ids.next())
1274 else {
1275 debug_panic!("Expected a new block for each new diagnostic group");
1276 continue;
1277 };
1278 for i in grouped_diagnostics {
1279 self.new_diagnostics[i].1 = Some(*block_id);
1280 }
1281 }
1282 }
1283 }
1284
1285 self.new_diagnostics
1286 .into_iter()
1287 .filter_map(|(diagnostic, block_id)| Some((diagnostic, block_id?)))
1288 .collect()
1289 }
1290}
1291
1292fn render_same_line_diagnostics(
1293 expanded: Arc<AtomicBool>,
1294 diagnostics: Arc<Vec<language::Diagnostic>>,
1295 editor_handle: View<Editor>,
1296 folded_block_height: u8,
1297) -> RenderBlock {
1298 Box::new(move |cx: &mut BlockContext| {
1299 let block_id = match cx.transform_block_id {
1300 TransformBlockId::Block(block_id) => block_id,
1301 _ => {
1302 debug_panic!("Expected a block id for the diagnostics block");
1303 return div().into_any_element();
1304 }
1305 };
1306 let Some(first_diagnostic) = diagnostics.first() else {
1307 debug_panic!("Expected at least one diagnostic");
1308 return div().into_any_element();
1309 };
1310 let button_expanded = expanded.clone();
1311 let expanded = expanded.load(atomic::Ordering::Acquire);
1312 let expand_label = if expanded { '-' } else { '+' };
1313 let first_diagnostics_height = diagnostic_text_lines(first_diagnostic);
1314 let extra_diagnostics = diagnostics.len() - 1;
1315 let toggle_expand_label =
1316 if folded_block_height == first_diagnostics_height && extra_diagnostics == 0 {
1317 None
1318 } else if extra_diagnostics > 0 {
1319 Some(format!("{expand_label}{extra_diagnostics}"))
1320 } else {
1321 Some(expand_label.to_string())
1322 };
1323
1324 let expanded_block_height = diagnostics
1325 .iter()
1326 .map(|diagnostic| diagnostic_text_lines(diagnostic))
1327 .sum::<u8>();
1328 let editor_handle = editor_handle.clone();
1329 let mut parent = v_flex();
1330 let mut diagnostics_iter = diagnostics.iter().fuse();
1331 if let Some(first_diagnostic) = diagnostics_iter.next() {
1332 let mut renderer = diagnostic_block_renderer(
1333 first_diagnostic.clone(),
1334 Some(folded_block_height),
1335 false,
1336 true,
1337 );
1338 parent = parent.child(
1339 h_flex()
1340 .when_some(toggle_expand_label, |parent, label| {
1341 parent.child(Button::new(cx.transform_block_id, label).on_click({
1342 let diagnostics = Arc::clone(&diagnostics);
1343 move |_, cx| {
1344 let new_expanded = !expanded;
1345 button_expanded.store(new_expanded, atomic::Ordering::Release);
1346 let new_size = if new_expanded {
1347 expanded_block_height
1348 } else {
1349 folded_block_height
1350 };
1351 editor_handle.update(cx, |editor, cx| {
1352 editor.replace_blocks(
1353 HashMap::from_iter(Some((
1354 block_id,
1355 (
1356 Some(new_size),
1357 render_same_line_diagnostics(
1358 Arc::clone(&button_expanded),
1359 Arc::clone(&diagnostics),
1360 editor_handle.clone(),
1361 folded_block_height,
1362 ),
1363 ),
1364 ))),
1365 None,
1366 cx,
1367 )
1368 });
1369 }
1370 }))
1371 })
1372 .child(renderer(cx)),
1373 );
1374 }
1375 if expanded {
1376 for diagnostic in diagnostics_iter {
1377 let mut renderer = diagnostic_block_renderer(diagnostic.clone(), None, false, true);
1378 parent = parent.child(renderer(cx));
1379 }
1380 }
1381 parent.into_any_element()
1382 })
1383}
1384
1385fn diagnostic_text_lines(diagnostic: &language::Diagnostic) -> u8 {
1386 diagnostic.message.matches('\n').count() as u8 + 1
1387}
1388
1389fn path_state_excerpts(
1390 after_excerpt_id: Option<ExcerptId>,
1391 before_excerpt_id: Option<ExcerptId>,
1392 multi_buffer_snapshot: &editor::MultiBufferSnapshot,
1393) -> impl Iterator<Item = (ExcerptId, &BufferSnapshot, ExcerptRange<language::Anchor>)> {
1394 multi_buffer_snapshot
1395 .excerpts()
1396 .skip_while(move |&(excerpt_id, ..)| match after_excerpt_id {
1397 Some(after_excerpt_id) => after_excerpt_id != excerpt_id,
1398 None => false,
1399 })
1400 .filter(move |&(excerpt_id, ..)| after_excerpt_id != Some(excerpt_id))
1401 .take_while(move |&(excerpt_id, ..)| match before_excerpt_id {
1402 Some(before_excerpt_id) => before_excerpt_id != excerpt_id,
1403 None => true,
1404 })
1405}