Indent guides (#11503)
Builds on top of existing work from #2249, but here's a showcase: https://github.com/zed-industries/zed/assets/53836821/4b346965-6654-496c-b379-75425d9b493f TODO: - [x] handle line wrapping - [x] implement handling in multibuffer (crashes currently) - [x] add configuration option - [x] new theme properties? What colors to use? - [x] Possibly support indents with different colors or background colors - [x] investigate edge cases (e.g. indent guides and folds continue on empty lines even if the next indent is different) - [x] add more tests (also test `find_active_indent_index`) - [x] docs (will do in a follow up PR) - [x] benchmark performance impact Release Notes: - Added indent guides ([#5373](https://github.com/zed-industries/zed/issues/5373)) --------- Co-authored-by: Nate Butler <1714999+iamnbutler@users.noreply.github.com> Co-authored-by: Remco <djsmits12@gmail.com>
This commit is contained in:
co-authored by
Nate Butler
Remco
parent
3eb0418bda
commit
feea607bac
@@ -38,7 +38,9 @@ use gpui::{
|
||||
ViewContext, WeakView, WindowContext,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use language::language_settings::ShowWhitespaceSetting;
|
||||
use language::language_settings::{
|
||||
IndentGuideBackgroundColoring, IndentGuideColoring, ShowWhitespaceSetting,
|
||||
};
|
||||
use lsp::DiagnosticSeverity;
|
||||
use multi_buffer::{Anchor, MultiBufferPoint, MultiBufferRow};
|
||||
use project::{
|
||||
@@ -1460,6 +1462,118 @@ impl EditorElement {
|
||||
Some(shaped_lines)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_indent_guides(
|
||||
&self,
|
||||
content_origin: gpui::Point<Pixels>,
|
||||
text_origin: gpui::Point<Pixels>,
|
||||
visible_buffer_range: Range<MultiBufferRow>,
|
||||
scroll_pixel_position: gpui::Point<Pixels>,
|
||||
line_height: Pixels,
|
||||
snapshot: &DisplaySnapshot,
|
||||
cx: &mut WindowContext,
|
||||
) -> Option<Vec<IndentGuideLayout>> {
|
||||
let indent_guides =
|
||||
self.editor
|
||||
.read(cx)
|
||||
.indent_guides(visible_buffer_range, snapshot, cx)?;
|
||||
|
||||
let active_indent_guide_indices = self.editor.update(cx, |editor, cx| {
|
||||
editor
|
||||
.find_active_indent_guide_indices(&indent_guides, snapshot, cx)
|
||||
.unwrap_or_default()
|
||||
});
|
||||
|
||||
Some(
|
||||
indent_guides
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.filter_map(|(i, indent_guide)| {
|
||||
let indent_size = self.column_pixels(indent_guide.indent_size as usize, cx);
|
||||
let total_width = indent_size * px(indent_guide.depth as f32);
|
||||
|
||||
let start_x = content_origin.x + total_width - scroll_pixel_position.x;
|
||||
if start_x >= text_origin.x {
|
||||
let (offset_y, length) = Self::calculate_indent_guide_bounds(
|
||||
indent_guide.multibuffer_row_range.clone(),
|
||||
line_height,
|
||||
snapshot,
|
||||
);
|
||||
|
||||
let start_y = content_origin.y + offset_y - scroll_pixel_position.y;
|
||||
|
||||
Some(IndentGuideLayout {
|
||||
origin: point(start_x, start_y),
|
||||
length,
|
||||
indent_size,
|
||||
depth: indent_guide.depth,
|
||||
active: active_indent_guide_indices.contains(&i),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
fn calculate_indent_guide_bounds(
|
||||
row_range: Range<MultiBufferRow>,
|
||||
line_height: Pixels,
|
||||
snapshot: &DisplaySnapshot,
|
||||
) -> (gpui::Pixels, gpui::Pixels) {
|
||||
let start_point = Point::new(row_range.start.0, 0);
|
||||
let end_point = Point::new(row_range.end.0, 0);
|
||||
|
||||
let row_range = start_point.to_display_point(snapshot).row()
|
||||
..end_point.to_display_point(snapshot).row();
|
||||
|
||||
let mut prev_line = start_point;
|
||||
prev_line.row = prev_line.row.saturating_sub(1);
|
||||
let prev_line = prev_line.to_display_point(snapshot).row();
|
||||
|
||||
let mut cons_line = end_point;
|
||||
cons_line.row += 1;
|
||||
let cons_line = cons_line.to_display_point(snapshot).row();
|
||||
|
||||
let mut offset_y = row_range.start.0 as f32 * line_height;
|
||||
let mut length = (cons_line.0.saturating_sub(row_range.start.0)) as f32 * line_height;
|
||||
|
||||
// If there is a block (e.g. diagnostic) in between the start of the indent guide and the line above,
|
||||
// we want to extend the indent guide to the start of the block.
|
||||
let mut block_height = 0;
|
||||
let mut block_offset = 0;
|
||||
let mut found_excerpt_header = false;
|
||||
for (_, block) in snapshot.blocks_in_range(prev_line..row_range.start) {
|
||||
if matches!(block, TransformBlock::ExcerptHeader { .. }) {
|
||||
found_excerpt_header = true;
|
||||
break;
|
||||
}
|
||||
block_offset += block.height();
|
||||
block_height += block.height();
|
||||
}
|
||||
if !found_excerpt_header {
|
||||
offset_y -= block_offset as f32 * line_height;
|
||||
length += block_height as f32 * line_height;
|
||||
}
|
||||
|
||||
// If there is a block (e.g. diagnostic) at the end of an multibuffer excerpt,
|
||||
// we want to ensure that the indent guide stops before the excerpt header.
|
||||
let mut block_height = 0;
|
||||
let mut found_excerpt_header = false;
|
||||
for (_, block) in snapshot.blocks_in_range(row_range.end..cons_line) {
|
||||
if matches!(block, TransformBlock::ExcerptHeader { .. }) {
|
||||
found_excerpt_header = true;
|
||||
}
|
||||
block_height += block.height();
|
||||
}
|
||||
if found_excerpt_header {
|
||||
length -= block_height as f32 * line_height;
|
||||
}
|
||||
|
||||
(offset_y, length)
|
||||
}
|
||||
|
||||
fn layout_run_indicators(
|
||||
&self,
|
||||
line_height: Pixels,
|
||||
@@ -2500,6 +2614,91 @@ impl EditorElement {
|
||||
})
|
||||
}
|
||||
|
||||
fn paint_indent_guides(&mut self, layout: &mut EditorLayout, cx: &mut WindowContext) {
|
||||
let Some(indent_guides) = &layout.indent_guides else {
|
||||
return;
|
||||
};
|
||||
|
||||
let settings = self
|
||||
.editor
|
||||
.read(cx)
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.settings_at(0, cx)
|
||||
.indent_guides;
|
||||
|
||||
let faded_color = |color: Hsla, alpha: f32| {
|
||||
let mut faded = color;
|
||||
faded.a = alpha;
|
||||
faded
|
||||
};
|
||||
|
||||
for indent_guide in indent_guides {
|
||||
let indent_accent_colors = cx.theme().accents().color_for_index(indent_guide.depth);
|
||||
|
||||
// TODO fixed for now, expose them through themes later
|
||||
const INDENT_AWARE_ALPHA: f32 = 0.2;
|
||||
const INDENT_AWARE_ACTIVE_ALPHA: f32 = 0.4;
|
||||
const INDENT_AWARE_BACKGROUND_ALPHA: f32 = 0.1;
|
||||
const INDENT_AWARE_BACKGROUND_ACTIVE_ALPHA: f32 = 0.2;
|
||||
|
||||
let line_color = match (&settings.coloring, indent_guide.active) {
|
||||
(IndentGuideColoring::Disabled, _) => None,
|
||||
(IndentGuideColoring::Fixed, false) => {
|
||||
Some(cx.theme().colors().editor_indent_guide)
|
||||
}
|
||||
(IndentGuideColoring::Fixed, true) => {
|
||||
Some(cx.theme().colors().editor_indent_guide_active)
|
||||
}
|
||||
(IndentGuideColoring::IndentAware, false) => {
|
||||
Some(faded_color(indent_accent_colors, INDENT_AWARE_ALPHA))
|
||||
}
|
||||
(IndentGuideColoring::IndentAware, true) => {
|
||||
Some(faded_color(indent_accent_colors, INDENT_AWARE_ACTIVE_ALPHA))
|
||||
}
|
||||
};
|
||||
|
||||
let background_color = match (&settings.background_coloring, indent_guide.active) {
|
||||
(IndentGuideBackgroundColoring::Disabled, _) => None,
|
||||
(IndentGuideBackgroundColoring::IndentAware, false) => Some(faded_color(
|
||||
indent_accent_colors,
|
||||
INDENT_AWARE_BACKGROUND_ALPHA,
|
||||
)),
|
||||
(IndentGuideBackgroundColoring::IndentAware, true) => Some(faded_color(
|
||||
indent_accent_colors,
|
||||
INDENT_AWARE_BACKGROUND_ACTIVE_ALPHA,
|
||||
)),
|
||||
};
|
||||
|
||||
let requested_line_width = settings.line_width.clamp(1, 10);
|
||||
let mut line_indicator_width = 0.;
|
||||
if let Some(color) = line_color {
|
||||
cx.paint_quad(fill(
|
||||
Bounds {
|
||||
origin: indent_guide.origin,
|
||||
size: size(px(requested_line_width as f32), indent_guide.length),
|
||||
},
|
||||
color,
|
||||
));
|
||||
line_indicator_width = requested_line_width as f32;
|
||||
}
|
||||
|
||||
if let Some(color) = background_color {
|
||||
let width = indent_guide.indent_size - px(line_indicator_width);
|
||||
cx.paint_quad(fill(
|
||||
Bounds {
|
||||
origin: point(
|
||||
indent_guide.origin.x + px(line_indicator_width),
|
||||
indent_guide.origin.y,
|
||||
),
|
||||
size: size(width, indent_guide.length),
|
||||
},
|
||||
color,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_gutter(&mut self, layout: &mut EditorLayout, cx: &mut WindowContext) {
|
||||
let line_height = layout.position_map.line_height;
|
||||
|
||||
@@ -4146,6 +4345,21 @@ impl Element for EditorElement {
|
||||
scroll_position.y * line_height,
|
||||
);
|
||||
|
||||
let start_buffer_row =
|
||||
MultiBufferRow(start_anchor.to_point(&snapshot.buffer_snapshot).row);
|
||||
let end_buffer_row =
|
||||
MultiBufferRow(end_anchor.to_point(&snapshot.buffer_snapshot).row);
|
||||
|
||||
let indent_guides = self.layout_indent_guides(
|
||||
content_origin,
|
||||
text_hitbox.origin,
|
||||
start_buffer_row..end_buffer_row,
|
||||
scroll_pixel_position,
|
||||
line_height,
|
||||
&snapshot,
|
||||
cx,
|
||||
);
|
||||
|
||||
let flap_trailers = cx.with_element_namespace("flap_trailers", |cx| {
|
||||
self.prepaint_flap_trailers(
|
||||
flap_trailers,
|
||||
@@ -4403,6 +4617,7 @@ impl Element for EditorElement {
|
||||
}),
|
||||
visible_display_row_range: start_row..end_row,
|
||||
wrap_guides,
|
||||
indent_guides,
|
||||
hitbox,
|
||||
text_hitbox,
|
||||
gutter_hitbox,
|
||||
@@ -4492,6 +4707,7 @@ impl Element for EditorElement {
|
||||
cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
|
||||
self.paint_mouse_listeners(layout, hovered_hunk, cx);
|
||||
self.paint_background(layout, cx);
|
||||
self.paint_indent_guides(layout, cx);
|
||||
if layout.gutter_hitbox.size.width > Pixels::ZERO {
|
||||
self.paint_gutter(layout, cx)
|
||||
}
|
||||
@@ -4530,6 +4746,7 @@ pub struct EditorLayout {
|
||||
scrollbar_layout: Option<ScrollbarLayout>,
|
||||
mode: EditorMode,
|
||||
wrap_guides: SmallVec<[(Pixels, bool); 2]>,
|
||||
indent_guides: Option<Vec<IndentGuideLayout>>,
|
||||
visible_display_row_range: Range<DisplayRow>,
|
||||
active_rows: BTreeMap<DisplayRow, bool>,
|
||||
highlighted_rows: BTreeMap<DisplayRow, Hsla>,
|
||||
@@ -4795,6 +5012,15 @@ fn layout_line(
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct IndentGuideLayout {
|
||||
origin: gpui::Point<Pixels>,
|
||||
length: Pixels,
|
||||
indent_size: Pixels,
|
||||
depth: u32,
|
||||
active: bool,
|
||||
}
|
||||
|
||||
pub struct CursorLayout {
|
||||
origin: gpui::Point<Pixels>,
|
||||
block_width: Pixels,
|
||||
|
||||
Reference in New Issue
Block a user