Cargo.lock 🔗
@@ -3402,11 +3402,13 @@ dependencies = [
"ctor",
"editor",
"env_logger",
+ "feature_flags",
"futures 0.3.28",
"gpui",
"language",
"log",
"lsp",
+ "multi_buffer",
"pretty_assertions",
"project",
"rand 0.8.5",
Kirill Bulatov created
Provide a current, broken state as an experimental way to browse
diagnostics.
The diagnostics are grouped by lines and reduced into a block that, in
case of multiple diagnostics per line, could be toggled back and forth
to show more diagnostics on the line.
Use `grouped_diagnostics::Deploy` to show the panel.
Issues remaining:
* panic on warnings toggle due to incorrect excerpt manipulation
* badly styled blocks
* no key bindings to navigate between blocks and toggle them
* overall odd usability gains for certain groups of people
Due to all above, the thing is feature-gated and not exposed to regular
people.
Release Notes:
- N/A
Cargo.lock | 2
crates/assistant/src/assistant_panel.rs | 11
crates/diagnostics/Cargo.toml | 2
crates/diagnostics/src/diagnostics.rs | 11
crates/diagnostics/src/diagnostics_tests.rs | 6
crates/diagnostics/src/grouped_diagnostics.rs | 1419 +++++++++++++++++++++
crates/editor/src/display_map.rs | 1
crates/editor/src/display_map/block_map.rs | 73 +
crates/editor/src/editor.rs | 94
crates/editor/src/element.rs | 105
crates/feature_flags/src/feature_flags.rs | 5
crates/multi_buffer/src/multi_buffer.rs | 11
crates/project/src/project.rs | 2
13 files changed, 1,647 insertions(+), 95 deletions(-)
@@ -3402,11 +3402,13 @@ dependencies = [
"ctor",
"editor",
"env_logger",
+ "feature_flags",
"futures 0.3.28",
"gpui",
"language",
"log",
"lsp",
+ "multi_buffer",
"pretty_assertions",
"project",
"rand 0.8.5",
@@ -2549,10 +2549,13 @@ fn render_slash_command_output_toggle(
fold: ToggleFold,
_cx: &mut WindowContext,
) -> AnyElement {
- Disclosure::new(("slash-command-output-fold-indicator", row.0), !is_folded)
- .selected(is_folded)
- .on_click(move |_e, cx| fold(!is_folded, cx))
- .into_any_element()
+ Disclosure::new(
+ ("slash-command-output-fold-indicator", row.0 as u64),
+ !is_folded,
+ )
+ .selected(is_folded)
+ .on_click(move |_e, cx| fold(!is_folded, cx))
+ .into_any_element()
}
fn render_pending_slash_command_gutter_decoration(
@@ -18,11 +18,13 @@ collections.workspace = true
ctor.workspace = true
editor.workspace = true
env_logger.workspace = true
+feature_flags.workspace = true
futures.workspace = true
gpui.workspace = true
language.workspace = true
log.workspace = true
lsp.workspace = true
+multi_buffer.workspace = true
project.workspace = true
rand.workspace = true
schemars.workspace = true
@@ -4,6 +4,7 @@ mod toolbar_controls;
#[cfg(test)]
mod diagnostics_tests;
+mod grouped_diagnostics;
use anyhow::Result;
use collections::{BTreeSet, HashSet};
@@ -14,6 +15,7 @@ use editor::{
scroll::Autoscroll,
Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer, ToOffset,
};
+use feature_flags::FeatureFlagAppExt;
use futures::{
channel::mpsc::{self, UnboundedSender},
StreamExt as _,
@@ -52,6 +54,9 @@ pub fn init(cx: &mut AppContext) {
ProjectDiagnosticsSettings::register(cx);
cx.observe_new_views(ProjectDiagnosticsEditor::register)
.detach();
+ if !cx.has_flag::<feature_flags::GroupedDiagnostics>() {
+ grouped_diagnostics::init(cx);
+ }
}
struct ProjectDiagnosticsEditor {
@@ -466,7 +471,9 @@ impl ProjectDiagnosticsEditor {
position: (excerpt_id, entry.range.start),
height: diagnostic.message.matches('\n').count() as u8 + 1,
style: BlockStyle::Fixed,
- render: diagnostic_block_renderer(diagnostic, true),
+ render: diagnostic_block_renderer(
+ diagnostic, None, true, true,
+ ),
disposition: BlockDisposition::Below,
});
}
@@ -798,7 +805,7 @@ impl Item for ProjectDiagnosticsEditor {
const DIAGNOSTIC_HEADER: &'static str = "diagnostic header";
fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
- let (message, code_ranges) = highlight_diagnostic_message(&diagnostic);
+ let (message, code_ranges) = highlight_diagnostic_message(&diagnostic, None);
let message: SharedString = message;
Box::new(move |cx| {
let highlight_style: HighlightStyle = cx.theme().colors().text_accent.into();
@@ -973,8 +973,8 @@ fn editor_blocks(
blocks.extend(
snapshot
.blocks_in_range(DisplayRow(0)..snapshot.max_point().row())
- .enumerate()
- .filter_map(|(ix, (row, block))| {
+ .filter_map(|(row, block)| {
+ let transform_block_id = block.id();
let name: SharedString = match block {
TransformBlock::Custom(block) => {
let mut element = block.render(&mut BlockContext {
@@ -984,7 +984,7 @@ fn editor_blocks(
line_height: px(0.),
em_width: px(0.),
max_width: px(0.),
- block_id: ix,
+ transform_block_id,
editor_style: &editor::EditorStyle::default(),
});
let element = element.downcast_mut::<Stateful<Div>>().unwrap();
@@ -0,0 +1,1419 @@
+use anyhow::Result;
+use collections::{BTreeMap, BTreeSet, HashMap, HashSet};
+use editor::{
+ diagnostic_block_renderer,
+ display_map::{
+ BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock,
+ TransformBlockId,
+ },
+ 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, Pane, 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();
+}
+
+struct GroupedDiagnosticsEditor {
+ project: Model<Project>,
+ workspace: WeakView<Workspace>,
+ focus_handle: FocusHandle,
+ editor: View<Editor>,
+ summary: DiagnosticSummary,
+ excerpts: Model<MultiBuffer>,
+ path_states: Vec<PathState>,
+ paths_to_update: BTreeSet<(ProjectPath, LanguageServerId)>,
+ include_warnings: bool,
+ context: u32,
+ update_paths_tx: UnboundedSender<(ProjectPath, Option<LanguageServerId>)>,
+ _update_excerpts_task: Task<Result<()>>,
+ _subscription: Subscription,
+}
+
+struct PathState {
+ path: ProjectPath,
+ first_excerpt_id: Option<ExcerptId>,
+ last_excerpt_id: Option<ExcerptId>,
+ diagnostics: Vec<(DiagnosticData, BlockId)>,
+}
+
+#[derive(Debug, Clone)]
+struct DiagnosticData {
+ language_server_id: LanguageServerId,
+ is_primary: bool,
+ entry: DiagnosticEntry<language::Anchor>,
+}
+
+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<EditorEvent> for GroupedDiagnosticsEditor {}
+
+impl Render for GroupedDiagnosticsEditor {
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> 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>) {
+ workspace.register_action(Self::deploy);
+ }
+
+ fn new_with_context(
+ context: u32,
+ project_handle: Model<Project>,
+ workspace: WeakView<Workspace>,
+ cx: &mut ViewContext<Self>,
+ ) -> 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<Project>,
+ workspace: WeakView<Workspace>,
+ cx: &mut ViewContext<Self>,
+ ) -> Self {
+ Self::new_with_context(
+ editor::DEFAULT_MULTIBUFFER_CONTEXT,
+ project_handle,
+ workspace,
+ cx,
+ )
+ }
+
+ fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
+ if let Some(existing) = workspace.item_of_type::<GroupedDiagnosticsEditor>(cx) {
+ workspace.activate_item(&existing, 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, cx);
+ }
+ }
+
+ fn toggle_warnings(&mut self, _: &ToggleWarnings, cx: &mut ViewContext<Self>) {
+ self.include_warnings = !self.include_warnings;
+ self.enqueue_update_all_excerpts(cx);
+ cx.notify();
+ }
+
+ fn focus_in(&mut self, cx: &mut ViewContext<Self>) {
+ 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<Self>) {
+ 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>) {
+ self.project.update(cx, |project, cx| {
+ let mut paths = project
+ .diagnostic_summaries(false, cx)
+ .map(|(path, _, _)| path)
+ .collect::<BTreeSet<_>>();
+ 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.
+ fn enqueue_update_stale_excerpts(&mut self, language_server_id: Option<LanguageServerId>) {
+ 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<LanguageServerId>,
+ buffer: Model<Buffer>,
+ cx: &mut ViewContext<Self>,
+ ) {
+ 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 kb change selections as in the old panel, to the next primary diagnostics
+ // TODO kb 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
+ }
+ };
+
+ // TODO kb when warnings are turned off, there's a lot of refresh for many paths happening, why?
+ 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<ExcerptId>, Option<ExcerptId>) {
+ 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<Self>) {
+ 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>) {
+ self.editor.update(cx, |editor, cx| editor.deactivated(cx));
+ }
+
+ fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
+ self.editor
+ .update(cx, |editor, cx| editor.navigate(data, cx))
+ }
+
+ fn tab_tooltip_text(&self, _: &AppContext) -> Option<SharedString> {
+ 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(if params.selected {
+ Color::Default
+ } else {
+ Color::Muted
+ })
+ .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(
+ if params.selected {
+ Color::Default
+ } else {
+ Color::Muted
+ },
+ )),
+ )
+ })
+ .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(
+ if params.selected {
+ Color::Default
+ } else {
+ Color::Muted
+ },
+ )),
+ )
+ })
+ .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>) {
+ self.editor.update(cx, |editor, _| {
+ editor.set_nav_history(Some(nav_history));
+ });
+ }
+
+ fn clone_on_split(
+ &self,
+ _workspace_id: Option<workspace::WorkspaceId>,
+ cx: &mut ViewContext<Self>,
+ ) -> Option<View<Self>>
+ 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<Project>,
+ cx: &mut ViewContext<Self>,
+ ) -> Task<Result<()>> {
+ self.editor.save(format, project, cx)
+ }
+
+ fn save_as(
+ &mut self,
+ _: Model<Project>,
+ _: ProjectPath,
+ _: &mut ViewContext<Self>,
+ ) -> Task<Result<()>> {
+ unreachable!()
+ }
+
+ fn reload(&mut self, project: Model<Project>, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
+ self.editor.reload(project, cx)
+ }
+
+ fn act_as_type<'a>(
+ &'a self,
+ type_id: TypeId,
+ self_handle: &'a View<Self>,
+ _: &'a AppContext,
+ ) -> Option<AnyView> {
+ if type_id == TypeId::of::<Self>() {
+ Some(self_handle.to_any())
+ } else if type_id == TypeId::of::<Editor>() {
+ Some(self.editor.to_any())
+ } else {
+ None
+ }
+ }
+
+ fn breadcrumb_location(&self) -> ToolbarItemLocation {
+ ToolbarItemLocation::PrimaryLeft
+ }
+
+ fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
+ self.editor.breadcrumbs(theme, cx)
+ }
+
+ fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
+ self.editor
+ .update(cx, |editor, cx| editor.added_to_workspace(workspace, cx));
+ }
+
+ fn serialized_item_kind() -> Option<&'static str> {
+ Some("diagnostics")
+ }
+
+ fn deserialize(
+ project: Model<Project>,
+ workspace: WeakView<Workspace>,
+ _workspace_id: workspace::WorkspaceId,
+ _item_id: workspace::ItemId,
+ cx: &mut ViewContext<Pane>,
+ ) -> Task<Result<View<Self>>> {
+ Task::ready(Ok(cx.new_view(|cx| Self::new(project, 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<language::Anchor>,
+ new: &DiagnosticEntry<language::Anchor>,
+ 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<language::Anchor>,
+ new: &Range<language::Anchor>,
+ 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 kb wrong? What to do here instead?
+fn compare_diagnostic_range_edges(
+ old: &Range<language::Anchor>,
+ new: &Range<language::Anchor>,
+ 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<ExcerptId>, Option<ExcerptId>),
+ latest_excerpt_id: ExcerptId,
+ new_diagnostics: Vec<(DiagnosticData, Option<BlockId>)>,
+ diagnostics_by_row_label: BTreeMap<MultiBufferRow, (editor::Anchor, Vec<usize>)>,
+ blocks_to_remove: HashSet<BlockId>,
+ unchanged_blocks: HashMap<usize, BlockId>,
+ excerpts_with_new_diagnostics: HashSet<ExcerptId>,
+ excerpts_to_remove: Vec<ExcerptId>,
+ excerpt_expands: HashMap<(ExpandExcerptDirection, u32), Vec<ExcerptId>>,
+ excerpts_to_add: HashMap<ExcerptId, Vec<Range<language::Anchor>>>,
+ first_excerpt_id: Option<ExcerptId>,
+ last_excerpt_id: Option<ExcerptId>,
+}
+
+impl PathUpdate {
+ fn new(
+ path_excerpts_borders: (Option<ExcerptId>, Option<ExcerptId>),
+ buffer_snapshot: &BufferSnapshot,
+ server_to_update: Option<LanguageServerId>,
+ 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::<Vec<_>>();
+ 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<Item = &'a (DiagnosticData, BlockId)> + 'a,
+ ) {
+ let mut current_diagnostics = current_diagnostics.fuse().peekable();
+ let mut excerpts_to_expand =
+ HashMap::<ExcerptId, HashMap<ExpandExcerptDirection, u32>>::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<Buffer>,
+ cx: &mut gpui::ModelContext<MultiBuffer>,
+ ) {
+ 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::<Vec<_>>();
+ 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<Editor>,
+ multi_buffer_snapshot: MultiBufferSnapshot,
+ ) -> Vec<BlockProperties<editor::Anchor>> {
+ 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::<Vec<_>>(),
+ );
+ 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<BlockId>) -> Vec<(DiagnosticData, BlockId)> {
+ 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<AtomicBool>,
+ diagnostics: Arc<Vec<language::Diagnostic>>,
+ editor_handle: View<Editor>,
+ folded_block_height: u8,
+) -> RenderBlock {
+ Box::new(move |cx: &mut BlockContext| {
+ let block_id = match cx.transform_block_id {
+ TransformBlockId::Block(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::<u8>();
+ let editor_handle = editor_handle.clone();
+ let mut parent = v_flex();
+ let mut diagnostics_iter = diagnostics.iter().fuse();
+ if let Some(first_diagnostic) = diagnostics_iter.next() {
+ let mut renderer = diagnostic_block_renderer(
+ first_diagnostic.clone(),
+ Some(folded_block_height),
+ false,
+ true,
+ );
+ parent = parent.child(
+ h_flex()
+ .when_some(toggle_expand_label, |parent, label| {
+ parent.child(Button::new(cx.transform_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,
+ )
+ });
+ }
+ }))
+ })
+ .child(renderer(cx)),
+ );
+ }
+ if expanded {
+ for diagnostic in diagnostics_iter {
+ let mut renderer = diagnostic_block_renderer(diagnostic.clone(), None, false, true);
+ parent = parent.child(renderer(cx));
+ }
+ }
+ parent.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<ExcerptId>,
+ before_excerpt_id: Option<ExcerptId>,
+ multi_buffer_snapshot: &editor::MultiBufferSnapshot,
+) -> impl Iterator<Item = (ExcerptId, &BufferSnapshot, ExcerptRange<language::Anchor>)> {
+ 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,
+ })
+}
@@ -30,6 +30,7 @@ use crate::{
pub use block_map::{
BlockBufferRows, BlockChunks as DisplayChunks, BlockContext, BlockDisposition, BlockId,
BlockMap, BlockPoint, BlockProperties, BlockStyle, RenderBlock, TransformBlock,
+ TransformBlockId,
};
use block_map::{BlockRow, BlockSnapshot};
use collections::{HashMap, HashSet};
@@ -4,7 +4,7 @@ use super::{
};
use crate::{EditorStyle, GutterDimensions};
use collections::{Bound, HashMap, HashSet};
-use gpui::{AnyElement, Pixels, WindowContext};
+use gpui::{AnyElement, EntityId, Pixels, WindowContext};
use language::{BufferSnapshot, Chunk, Patch, Point};
use multi_buffer::{Anchor, ExcerptId, ExcerptRange, MultiBufferRow, ToPoint as _};
use parking_lot::Mutex;
@@ -20,6 +20,7 @@ use std::{
};
use sum_tree::{Bias, SumTree};
use text::Edit;
+use ui::ElementId;
const NEWLINES: &[u8] = &[b'\n'; u8::MAX as usize];
@@ -53,6 +54,12 @@ pub struct BlockSnapshot {
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct BlockId(usize);
+impl Into<ElementId> for BlockId {
+ fn into(self) -> ElementId {
+ ElementId::Integer(self.0)
+ }
+}
+
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
pub struct BlockPoint(pub Point);
@@ -62,7 +69,7 @@ pub struct BlockRow(pub(super) u32);
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
struct WrapRow(u32);
-pub type RenderBlock = Box<dyn Send + Fn(&mut BlockContext) -> AnyElement>;
+pub type RenderBlock = Box<dyn Send + FnMut(&mut BlockContext) -> AnyElement>;
pub struct Block {
id: BlockId,
@@ -77,11 +84,22 @@ pub struct BlockProperties<P> {
pub position: P,
pub height: u8,
pub style: BlockStyle,
- pub render: Box<dyn Send + Fn(&mut BlockContext) -> AnyElement>,
+ pub render: RenderBlock,
pub disposition: BlockDisposition,
}
-#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
+impl<P: Debug> Debug for BlockProperties<P> {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("BlockProperties")
+ .field("position", &self.position)
+ .field("height", &self.height)
+ .field("style", &self.style)
+ .field("disposition", &self.disposition)
+ .finish()
+ }
+}
+
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
pub enum BlockStyle {
Fixed,
Flex,
@@ -95,10 +113,47 @@ pub struct BlockContext<'a, 'b> {
pub gutter_dimensions: &'b GutterDimensions,
pub em_width: Pixels,
pub line_height: Pixels,
- pub block_id: usize,
+ pub transform_block_id: TransformBlockId,
pub editor_style: &'b EditorStyle,
}
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub enum TransformBlockId {
+ Block(BlockId),
+ ExcerptHeader(ExcerptId),
+ ExcerptFooter(ExcerptId),
+}
+
+impl From<TransformBlockId> for EntityId {
+ fn from(value: TransformBlockId) -> Self {
+ match value {
+ TransformBlockId::Block(BlockId(id)) => EntityId::from(id as u64),
+ TransformBlockId::ExcerptHeader(id) => id.into(),
+ TransformBlockId::ExcerptFooter(id) => id.into(),
+ }
+ }
+}
+
+impl Into<ElementId> for TransformBlockId {
+ fn into(self) -> ElementId {
+ match self {
+ Self::Block(BlockId(id)) => ("Block", id).into(),
+ Self::ExcerptHeader(id) => ("ExcerptHeader", EntityId::from(id)).into(),
+ Self::ExcerptFooter(id) => ("ExcerptFooter", EntityId::from(id)).into(),
+ }
+ }
+}
+
+impl std::fmt::Display for TransformBlockId {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Self::Block(id) => write!(f, "Block({id:?})"),
+ Self::ExcerptHeader(id) => write!(f, "ExcerptHeader({id:?})"),
+ Self::ExcerptFooter(id) => write!(f, "ExcerptFooter({id:?})"),
+ }
+ }
+}
+
/// Whether the block should be considered above or below the anchor line
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum BlockDisposition {
@@ -157,6 +212,14 @@ impl BlockLike for TransformBlock {
}
impl TransformBlock {
+ pub fn id(&self) -> TransformBlockId {
+ match self {
+ TransformBlock::Custom(block) => TransformBlockId::Block(block.id),
+ TransformBlock::ExcerptHeader { id, .. } => TransformBlockId::ExcerptHeader(*id),
+ TransformBlock::ExcerptFooter { id, .. } => TransformBlockId::ExcerptFooter(*id),
+ }
+ }
+
fn disposition(&self) -> BlockDisposition {
match self {
TransformBlock::Custom(block) => block.disposition,
@@ -68,12 +68,12 @@ use git::diff_hunk_to_display;
use gpui::{
div, impl_actions, point, prelude::*, px, relative, size, uniform_list, Action, AnyElement,
AppContext, AsyncWindowContext, AvailableSpace, BackgroundExecutor, Bounds, ClipboardItem,
- Context, DispatchPhase, ElementId, EventEmitter, FocusHandle, FocusOutEvent, FocusableView,
- FontId, FontStyle, FontWeight, HighlightStyle, Hsla, InteractiveText, KeyContext,
- ListSizingBehavior, Model, MouseButton, PaintQuad, ParentElement, Pixels, Render, SharedString,
- Size, StrikethroughStyle, Styled, StyledText, Subscription, Task, TextStyle, UnderlineStyle,
- UniformListScrollHandle, View, ViewContext, ViewInputHandler, VisualContext, WeakFocusHandle,
- WeakView, WhiteSpace, WindowContext,
+ Context, DispatchPhase, ElementId, EntityId, EventEmitter, FocusHandle, FocusOutEvent,
+ FocusableView, FontId, FontStyle, FontWeight, HighlightStyle, Hsla, InteractiveText,
+ KeyContext, ListSizingBehavior, Model, MouseButton, PaintQuad, ParentElement, Pixels, Render,
+ SharedString, Size, StrikethroughStyle, Styled, StyledText, Subscription, Task, TextStyle,
+ UnderlineStyle, UniformListScrollHandle, View, ViewContext, ViewInputHandler, VisualContext,
+ WeakFocusHandle, WeakView, WhiteSpace, WindowContext,
};
use highlight_matching_bracket::refresh_matching_bracket_highlights;
use hover_popover::{hide_hover, HoverState};
@@ -9762,7 +9762,7 @@ impl Editor {
*block_id,
(
None,
- diagnostic_block_renderer(diagnostic.clone(), is_valid),
+ diagnostic_block_renderer(diagnostic.clone(), None, true, is_valid),
),
);
}
@@ -9815,7 +9815,7 @@ impl Editor {
style: BlockStyle::Fixed,
position: buffer.anchor_after(entry.range.start),
height: message_height,
- render: diagnostic_block_renderer(diagnostic, true),
+ render: diagnostic_block_renderer(diagnostic, None, true, true),
disposition: BlockDisposition::Below,
}
}),
@@ -12684,11 +12684,17 @@ impl InvalidationRegion for SnippetState {
}
}
-pub fn diagnostic_block_renderer(diagnostic: Diagnostic, _is_valid: bool) -> RenderBlock {
- let (text_without_backticks, code_ranges) = highlight_diagnostic_message(&diagnostic);
+pub fn diagnostic_block_renderer(
+ diagnostic: Diagnostic,
+ max_message_rows: Option<u8>,
+ allow_closing: bool,
+ _is_valid: bool,
+) -> RenderBlock {
+ let (text_without_backticks, code_ranges) =
+ highlight_diagnostic_message(&diagnostic, max_message_rows);
Box::new(move |cx: &mut BlockContext| {
- let group_id: SharedString = cx.block_id.to_string().into();
+ let group_id: SharedString = cx.transform_block_id.to_string().into();
let mut text_style = cx.text_style().clone();
text_style.color = diagnostic_style(diagnostic.severity, cx.theme().status());
@@ -12700,23 +12706,25 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, _is_valid: bool) -> Ren
let multi_line_diagnostic = diagnostic.message.contains('\n');
- let buttons = |diagnostic: &Diagnostic, block_id: usize| {
+ let buttons = |diagnostic: &Diagnostic, block_id: TransformBlockId| {
if multi_line_diagnostic {
v_flex()
} else {
h_flex()
}
- .children(diagnostic.is_primary.then(|| {
- IconButton::new(("close-block", block_id), IconName::XCircle)
- .icon_color(Color::Muted)
- .size(ButtonSize::Compact)
- .style(ButtonStyle::Transparent)
- .visible_on_hover(group_id.clone())
- .on_click(move |_click, cx| cx.dispatch_action(Box::new(Cancel)))
- .tooltip(|cx| Tooltip::for_action("Close Diagnostics", &Cancel, cx))
- }))
+ .when(allow_closing, |div| {
+ div.children(diagnostic.is_primary.then(|| {
+ IconButton::new(("close-block", EntityId::from(block_id)), IconName::XCircle)
+ .icon_color(Color::Muted)
+ .size(ButtonSize::Compact)
+ .style(ButtonStyle::Transparent)
+ .visible_on_hover(group_id.clone())
+ .on_click(move |_click, cx| cx.dispatch_action(Box::new(Cancel)))
+ .tooltip(|cx| Tooltip::for_action("Close Diagnostics", &Cancel, cx))
+ }))
+ })
.child(
- IconButton::new(("copy-block", block_id), IconName::Copy)
+ IconButton::new(("copy-block", EntityId::from(block_id)), IconName::Copy)
.icon_color(Color::Muted)
.size(ButtonSize::Compact)
.style(ButtonStyle::Transparent)
@@ -12729,12 +12737,12 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, _is_valid: bool) -> Ren
)
};
- let icon_size = buttons(&diagnostic, cx.block_id)
+ let icon_size = buttons(&diagnostic, cx.transform_block_id)
.into_any_element()
.layout_as_root(AvailableSpace::min_size(), cx);
h_flex()
- .id(cx.block_id)
+ .id(cx.transform_block_id)
.group(group_id.clone())
.relative()
.size_full()
@@ -12746,7 +12754,7 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, _is_valid: bool) -> Ren
.w(cx.anchor_x - cx.gutter_dimensions.width - icon_size.width)
.flex_shrink(),
)
- .child(buttons(&diagnostic, cx.block_id))
+ .child(buttons(&diagnostic, cx.transform_block_id))
.child(div().flex().flex_shrink_0().child(
StyledText::new(text_without_backticks.clone()).with_highlights(
&text_style,
@@ -12765,7 +12773,10 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, _is_valid: bool) -> Ren
})
}
-pub fn highlight_diagnostic_message(diagnostic: &Diagnostic) -> (SharedString, Vec<Range<usize>>) {
+pub fn highlight_diagnostic_message(
+ diagnostic: &Diagnostic,
+ mut max_message_rows: Option<u8>,
+) -> (SharedString, Vec<Range<usize>>) {
let mut text_without_backticks = String::new();
let mut code_ranges = Vec::new();
@@ -12777,18 +12788,45 @@ pub fn highlight_diagnostic_message(diagnostic: &Diagnostic) -> (SharedString, V
let mut prev_offset = 0;
let mut in_code_block = false;
+ let mut newline_indices = diagnostic
+ .message
+ .match_indices('\n')
+ .map(|(ix, _)| ix)
+ .fuse()
+ .peekable();
for (ix, _) in diagnostic
.message
.match_indices('`')
.chain([(diagnostic.message.len(), "")])
{
+ let mut trimmed_ix = ix;
+ while let Some(newline_index) = newline_indices.peek() {
+ if *newline_index < ix {
+ if let Some(rows_left) = &mut max_message_rows {
+ if *rows_left == 0 {
+ trimmed_ix = newline_index.saturating_sub(1);
+ break;
+ } else {
+ *rows_left -= 1;
+ }
+ }
+ let _ = newline_indices.next();
+ } else {
+ break;
+ }
+ }
let prev_len = text_without_backticks.len();
- text_without_backticks.push_str(&diagnostic.message[prev_offset..ix]);
- prev_offset = ix + 1;
+ let new_text = &diagnostic.message[prev_offset..trimmed_ix];
+ text_without_backticks.push_str(new_text);
if in_code_block {
code_ranges.push(prev_len..text_without_backticks.len());
}
+ prev_offset = trimmed_ix + 1;
in_code_block = !in_code_block;
+ if trimmed_ix != ix {
+ text_without_backticks.push_str("...");
+ break;
+ }
}
(text_without_backticks.into(), code_ranges)
@@ -1,4 +1,5 @@
use crate::editor_settings::ScrollBeyondLastLine;
+use crate::TransformBlockId;
use crate::{
blame_entry_tooltip::{blame_entry_relative_timestamp, BlameEntryTooltip},
display_map::{
@@ -31,7 +32,7 @@ use gpui::{
anchored, deferred, div, fill, outline, point, px, quad, relative, size, svg,
transparent_black, Action, AnchorCorner, AnyElement, AvailableSpace, Bounds, ClipboardItem,
ContentMask, Corners, CursorStyle, DispatchPhase, Edges, Element, ElementInputHandler, Entity,
- FontId, GlobalElementId, Hitbox, Hsla, InteractiveElement, IntoElement, Length,
+ EntityId, FontId, GlobalElementId, Hitbox, Hsla, InteractiveElement, IntoElement, Length,
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad,
ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, ShapedLine, SharedString, Size,
StatefulInteractiveElement, Style, Styled, TextRun, TextStyle, TextStyleRefinement, View,
@@ -1939,7 +1940,6 @@ impl EditorElement {
line_layouts: &[LineWithInvisibles],
cx: &mut WindowContext,
) -> Vec<BlockLayout> {
- let mut block_id = 0;
let (fixed_blocks, non_fixed_blocks) = snapshot
.blocks_in_range(rows.clone())
.partition::<Vec<_>, _>(|(_, block)| match block {
@@ -1950,7 +1950,7 @@ impl EditorElement {
let render_block = |block: &TransformBlock,
available_space: Size<AvailableSpace>,
- block_id: usize,
+ block_id: TransformBlockId,
block_row_start: DisplayRow,
cx: &mut WindowContext| {
let mut element = match block {
@@ -1974,7 +1974,7 @@ impl EditorElement {
gutter_dimensions,
line_height,
em_width,
- block_id,
+ transform_block_id: block_id,
max_width: text_hitbox.size.width.max(*scroll_width),
editor_style: &self.style,
})
@@ -2058,7 +2058,7 @@ impl EditorElement {
let header_padding = px(6.0);
v_flex()
- .id(("path excerpt header", block_id))
+ .id(("path excerpt header", EntityId::from(block_id)))
.size_full()
.p(header_padding)
.child(
@@ -2166,7 +2166,7 @@ impl EditorElement {
}))
} else {
v_flex()
- .id(("excerpt header", block_id))
+ .id(("excerpt header", EntityId::from(block_id)))
.size_full()
.child(
div()
@@ -2314,49 +2314,54 @@ impl EditorElement {
}
TransformBlock::ExcerptFooter { id, .. } => {
- let element = v_flex().id(("excerpt footer", block_id)).size_full().child(
- h_flex()
- .justify_end()
- .flex_none()
- .w(gutter_dimensions.width
- - (gutter_dimensions.left_padding + gutter_dimensions.margin))
- .h_full()
- .child(
- ButtonLike::new("expand-icon")
- .style(ButtonStyle::Transparent)
- .child(
- svg()
- .path(IconName::ArrowDownFromLine.path())
- .size(IconSize::XSmall.rems())
- .text_color(cx.theme().colors().editor_line_number)
- .group("")
- .hover(|style| {
- style.text_color(
- cx.theme().colors().editor_active_line_number,
+ let element = v_flex()
+ .id(("excerpt footer", EntityId::from(block_id)))
+ .size_full()
+ .child(
+ h_flex()
+ .justify_end()
+ .flex_none()
+ .w(gutter_dimensions.width
+ - (gutter_dimensions.left_padding + gutter_dimensions.margin))
+ .h_full()
+ .child(
+ ButtonLike::new("expand-icon")
+ .style(ButtonStyle::Transparent)
+ .child(
+ svg()
+ .path(IconName::ArrowDownFromLine.path())
+ .size(IconSize::XSmall.rems())
+ .text_color(cx.theme().colors().editor_line_number)
+ .group("")
+ .hover(|style| {
+ style.text_color(
+ cx.theme()
+ .colors()
+ .editor_active_line_number,
+ )
+ }),
+ )
+ .on_click(cx.listener_for(&self.editor, {
+ let id = *id;
+ move |editor, _, cx| {
+ editor.expand_excerpt(
+ id,
+ multi_buffer::ExpandExcerptDirection::Down,
+ cx,
+ );
+ }
+ }))
+ .tooltip({
+ move |cx| {
+ Tooltip::for_action(
+ "Expand Excerpt",
+ &ExpandExcerpts { lines: 0 },
+ cx,
)
- }),
- )
- .on_click(cx.listener_for(&self.editor, {
- let id = *id;
- move |editor, _, cx| {
- editor.expand_excerpt(
- id,
- multi_buffer::ExpandExcerptDirection::Down,
- cx,
- );
- }
- }))
- .tooltip({
- move |cx| {
- Tooltip::for_action(
- "Expand Excerpt",
- &ExpandExcerpts { lines: 0 },
- cx,
- )
- }
- }),
- ),
- );
+ }
+ }),
+ ),
+ );
element.into_any()
}
};
@@ -2372,8 +2377,8 @@ impl EditorElement {
AvailableSpace::MinContent,
AvailableSpace::Definite(block.height() as f32 * line_height),
);
+ let block_id = block.id();
let (element, element_size) = render_block(block, available_space, block_id, row, cx);
- block_id += 1;
fixed_block_max_width = fixed_block_max_width.max(element_size.width + em_width);
blocks.push(BlockLayout {
row,
@@ -2401,8 +2406,8 @@ impl EditorElement {
AvailableSpace::Definite(width),
AvailableSpace::Definite(block.height() as f32 * line_height),
);
+ let block_id = block.id();
let (element, _) = render_block(block, available_space, block_id, row, cx);
- block_id += 1;
blocks.push(BlockLayout {
row,
element,
@@ -34,6 +34,11 @@ impl FeatureFlag for TerminalInlineAssist {
const NAME: &'static str = "terminal-inline-assist";
}
+pub struct GroupedDiagnostics {}
+impl FeatureFlag for GroupedDiagnostics {
+ const NAME: &'static str = "grouped-diagnostics";
+}
+
pub trait FeatureFlagViewExt<V: 'static> {
fn observe_flag<T: FeatureFlag, F>(&mut self, callback: F) -> Subscription
where
@@ -6,7 +6,7 @@ use clock::ReplicaId;
use collections::{BTreeMap, Bound, HashMap, HashSet};
use futures::{channel::mpsc, SinkExt};
use git::diff::DiffHunk;
-use gpui::{AppContext, EventEmitter, Model, ModelContext};
+use gpui::{AppContext, EntityId, EventEmitter, Model, ModelContext};
use itertools::Itertools;
use language::{
char_kind,
@@ -49,6 +49,12 @@ const NEWLINES: &[u8] = &[b'\n'; u8::MAX as usize];
#[derive(Debug, Default, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct ExcerptId(usize);
+impl From<ExcerptId> for EntityId {
+ fn from(id: ExcerptId) -> Self {
+ EntityId::from(id.0 as u64)
+ }
+}
+
/// One or more [`Buffers`](Buffer) being edited in a single view.
///
/// See <https://zed.dev/features#multi-buffers>
@@ -302,6 +308,7 @@ struct ExcerptBytes<'a> {
reversed: bool,
}
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ExpandExcerptDirection {
Up,
Down,
@@ -4679,7 +4686,7 @@ impl ToPointUtf16 for PointUtf16 {
}
}
-fn build_excerpt_ranges<T>(
+pub fn build_excerpt_ranges<T>(
buffer: &BufferSnapshot,
ranges: &[Range<T>],
context_line_count: u32,
@@ -11720,7 +11720,7 @@ fn sort_search_matches(search_matches: &mut Vec<SearchMatchCandidate>, cx: &AppC
});
}
-fn compare_paths(
+pub fn compare_paths(
(path_a, a_is_file): (&Path, bool),
(path_b, b_is_file): (&Path, bool),
) -> cmp::Ordering {