Add an experimental, WIP diagnostics grouping panel (#14515)

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
This commit is contained in:
Kirill Bulatov 2024-07-15 22:58:18 +03:00 committed by GitHub
parent 2c6cb4ec16
commit d7a25c1696
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 1647 additions and 95 deletions

2
Cargo.lock generated
View File

@ -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",

View File

@ -2549,7 +2549,10 @@ fn render_slash_command_output_toggle(
fold: ToggleFold,
_cx: &mut WindowContext,
) -> AnyElement {
Disclosure::new(("slash-command-output-fold-indicator", row.0), !is_folded)
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()

View File

@ -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

View File

@ -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();

View File

@ -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();

File diff suppressed because it is too large Load Diff

View File

@ -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};

View File

@ -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,

View File

@ -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,14 +12706,15 @@ 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)
.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)
@ -12715,8 +12722,9 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, _is_valid: bool) -> Ren
.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)

View File

@ -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,7 +2314,10 @@ impl EditorElement {
}
TransformBlock::ExcerptFooter { id, .. } => {
let element = v_flex().id(("excerpt footer", block_id)).size_full().child(
let element = v_flex()
.id(("excerpt footer", EntityId::from(block_id)))
.size_full()
.child(
h_flex()
.justify_end()
.flex_none()
@ -2332,7 +2335,9 @@ impl EditorElement {
.group("")
.hover(|style| {
style.text_color(
cx.theme().colors().editor_active_line_number,
cx.theme()
.colors()
.editor_active_line_number,
)
}),
)
@ -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,

View File

@ -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

View File

@ -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,

View File

@ -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 {