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:
Bennet Bo Fenner
2024-05-23 15:50:59 +02:00
committed by GitHub
co-authored by Nate Butler Remco
parent 3eb0418bda
commit feea607bac
27 changed files with 1705 additions and 65 deletions
+1 -14
View File
@@ -845,20 +845,7 @@ impl DisplaySnapshot {
.buffer_line_for_row(buffer_row)
.unwrap();
let mut indent_size = 0;
let mut is_blank = false;
for c in buffer.chars_at(Point::new(range.start.row, 0)) {
if c == ' ' || c == '\t' {
indent_size += 1;
} else {
if c == '\n' {
is_blank = true;
}
break;
}
}
(indent_size, is_blank)
buffer.line_indent_for_row(range.start.row)
}
pub fn line_len(&self, row: DisplayRow) -> u32 {
+13
View File
@@ -26,6 +26,7 @@ mod git;
mod highlight_matching_bracket;
mod hover_links;
mod hover_popover;
mod indent_guides;
mod inline_completion_provider;
pub mod items;
mod mouse_context_menu;
@@ -76,6 +77,7 @@ use highlight_matching_bracket::refresh_matching_bracket_highlights;
use hover_popover::{hide_hover, HoverState};
use hunk_diff::ExpandedHunks;
pub(crate) use hunk_diff::HunkToExpand;
use indent_guides::ActiveIndentGuidesState;
use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy};
pub use inline_completion_provider::*;
pub use items::MAX_TAB_TITLE_LEN;
@@ -453,11 +455,13 @@ pub struct Editor {
show_git_diff_gutter: Option<bool>,
show_code_actions: Option<bool>,
show_wrap_guides: Option<bool>,
show_indent_guides: Option<bool>,
placeholder_text: Option<Arc<str>>,
highlight_order: usize,
highlighted_rows: HashMap<TypeId, Vec<RowHighlight>>,
background_highlights: TreeMap<TypeId, BackgroundHighlight>,
scrollbar_marker_state: ScrollbarMarkerState,
active_indent_guides_state: ActiveIndentGuidesState,
nav_history: Option<ItemNavHistory>,
context_menu: RwLock<Option<ContextMenu>>,
mouse_context_menu: Option<MouseContextMenu>,
@@ -1656,11 +1660,13 @@ impl Editor {
show_git_diff_gutter: None,
show_code_actions: None,
show_wrap_guides: None,
show_indent_guides: None,
placeholder_text: None,
highlight_order: 0,
highlighted_rows: HashMap::default(),
background_highlights: Default::default(),
scrollbar_marker_state: ScrollbarMarkerState::default(),
active_indent_guides_state: ActiveIndentGuidesState::default(),
nav_history: None,
context_menu: RwLock::new(None),
mouse_context_menu: None,
@@ -9440,6 +9446,7 @@ impl Editor {
cx.notify();
self.scrollbar_marker_state.dirty = true;
self.active_indent_guides_state.dirty = true;
}
}
@@ -9668,6 +9675,11 @@ impl Editor {
cx.notify();
}
pub fn set_show_indent_guides(&mut self, show_indent_guides: bool, cx: &mut ViewContext<Self>) {
self.show_indent_guides = Some(show_indent_guides);
cx.notify();
}
pub fn reveal_in_finder(&mut self, _: &RevealInFinder, cx: &mut ViewContext<Self>) {
if let Some(buffer) = self.buffer().read(cx).as_singleton() {
if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local()) {
@@ -10303,6 +10315,7 @@ impl Editor {
singleton_buffer_edited,
} => {
self.scrollbar_marker_state.dirty = true;
self.active_indent_guides_state.dirty = true;
self.refresh_active_diagnostics(cx);
self.refresh_code_actions(cx);
if self.has_active_inline_completion(cx) {
+502 -1
View File
@@ -17,8 +17,10 @@ use language::{
},
BracketPairConfig,
Capability::ReadWrite,
FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher, Override, Point,
FakeLspAdapter, IndentGuide, LanguageConfig, LanguageConfigOverride, LanguageMatcher, Override,
Point,
};
use multi_buffer::MultiBufferIndentGuide;
use parking_lot::Mutex;
use project::project_settings::{LspSettings, ProjectSettings};
use project::FakeFs;
@@ -11448,6 +11450,505 @@ async fn test_multiple_expanded_hunks_merge(
);
}
async fn setup_indent_guides_editor(
text: &str,
cx: &mut gpui::TestAppContext,
) -> (BufferId, EditorTestContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
let buffer_id = cx.update_editor(|editor, cx| {
editor.set_text(text, cx);
let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
let buffer_id = buffer_ids[0];
buffer_id
});
(buffer_id, cx)
}
fn assert_indent_guides(
range: Range<u32>,
expected: Vec<IndentGuide>,
active_indices: Option<Vec<usize>>,
cx: &mut EditorTestContext,
) {
let indent_guides = cx.update_editor(|editor, cx| {
let snapshot = editor.snapshot(cx).display_snapshot;
let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
MultiBufferRow(range.start)..MultiBufferRow(range.end),
&snapshot,
cx,
);
indent_guides.sort_by(|a, b| {
a.depth.cmp(&b.depth).then(
a.start_row
.cmp(&b.start_row)
.then(a.end_row.cmp(&b.end_row)),
)
});
indent_guides
});
if let Some(expected) = active_indices {
let active_indices = cx.update_editor(|editor, cx| {
let snapshot = editor.snapshot(cx).display_snapshot;
editor.find_active_indent_guide_indices(&indent_guides, &snapshot, cx)
});
assert_eq!(
active_indices.unwrap().into_iter().collect::<Vec<_>>(),
expected,
"Active indent guide indices do not match"
);
}
let expected: Vec<_> = expected
.into_iter()
.map(|guide| MultiBufferIndentGuide {
multibuffer_row_range: MultiBufferRow(guide.start_row)..MultiBufferRow(guide.end_row),
buffer: guide,
})
.collect();
assert_eq!(indent_guides, expected, "Indent guides do not match");
}
#[gpui::test]
async fn test_indent_guides_single_line(cx: &mut gpui::TestAppContext) {
let (buffer_id, mut cx) = setup_indent_guides_editor(
&"
fn main() {
let a = 1;
}"
.unindent(),
cx,
)
.await;
assert_indent_guides(
0..3,
vec![IndentGuide::new(buffer_id, 1, 1, 0, 4)],
None,
&mut cx,
);
}
#[gpui::test]
async fn test_indent_guides_simple_block(cx: &mut gpui::TestAppContext) {
let (buffer_id, mut cx) = setup_indent_guides_editor(
&"
fn main() {
let a = 1;
let b = 2;
}"
.unindent(),
cx,
)
.await;
assert_indent_guides(
0..4,
vec![IndentGuide::new(buffer_id, 1, 2, 0, 4)],
None,
&mut cx,
);
}
#[gpui::test]
async fn test_indent_guides_nested(cx: &mut gpui::TestAppContext) {
let (buffer_id, mut cx) = setup_indent_guides_editor(
&"
fn main() {
let a = 1;
if a == 3 {
let b = 2;
} else {
let c = 3;
}
}"
.unindent(),
cx,
)
.await;
assert_indent_guides(
0..8,
vec![
IndentGuide::new(buffer_id, 1, 6, 0, 4),
IndentGuide::new(buffer_id, 3, 3, 1, 4),
IndentGuide::new(buffer_id, 5, 5, 1, 4),
],
None,
&mut cx,
);
}
#[gpui::test]
async fn test_indent_guides_tab(cx: &mut gpui::TestAppContext) {
let (buffer_id, mut cx) = setup_indent_guides_editor(
&"
fn main() {
let a = 1;
let b = 2;
let c = 3;
}"
.unindent(),
cx,
)
.await;
assert_indent_guides(
0..5,
vec![
IndentGuide::new(buffer_id, 1, 3, 0, 4),
IndentGuide::new(buffer_id, 2, 2, 1, 4),
],
None,
&mut cx,
);
}
#[gpui::test]
async fn test_indent_guides_continues_on_empty_line(cx: &mut gpui::TestAppContext) {
let (buffer_id, mut cx) = setup_indent_guides_editor(
&"
fn main() {
let a = 1;
let c = 3;
}"
.unindent(),
cx,
)
.await;
assert_indent_guides(
0..5,
vec![IndentGuide::new(buffer_id, 1, 3, 0, 4)],
None,
&mut cx,
);
}
#[gpui::test]
async fn test_indent_guides_complex(cx: &mut gpui::TestAppContext) {
let (buffer_id, mut cx) = setup_indent_guides_editor(
&"
fn main() {
let a = 1;
let c = 3;
if a == 3 {
let b = 2;
} else {
let c = 3;
}
}"
.unindent(),
cx,
)
.await;
assert_indent_guides(
0..11,
vec![
IndentGuide::new(buffer_id, 1, 9, 0, 4),
IndentGuide::new(buffer_id, 6, 6, 1, 4),
IndentGuide::new(buffer_id, 8, 8, 1, 4),
],
None,
&mut cx,
);
}
#[gpui::test]
async fn test_indent_guides_starts_off_screen(cx: &mut gpui::TestAppContext) {
let (buffer_id, mut cx) = setup_indent_guides_editor(
&"
fn main() {
let a = 1;
let c = 3;
if a == 3 {
let b = 2;
} else {
let c = 3;
}
}"
.unindent(),
cx,
)
.await;
assert_indent_guides(
1..11,
vec![
IndentGuide::new(buffer_id, 1, 9, 0, 4),
IndentGuide::new(buffer_id, 6, 6, 1, 4),
IndentGuide::new(buffer_id, 8, 8, 1, 4),
],
None,
&mut cx,
);
}
#[gpui::test]
async fn test_indent_guides_ends_off_screen(cx: &mut gpui::TestAppContext) {
let (buffer_id, mut cx) = setup_indent_guides_editor(
&"
fn main() {
let a = 1;
let c = 3;
if a == 3 {
let b = 2;
} else {
let c = 3;
}
}"
.unindent(),
cx,
)
.await;
assert_indent_guides(
1..10,
vec![
IndentGuide::new(buffer_id, 1, 9, 0, 4),
IndentGuide::new(buffer_id, 6, 6, 1, 4),
IndentGuide::new(buffer_id, 8, 8, 1, 4),
],
None,
&mut cx,
);
}
#[gpui::test]
async fn test_indent_guides_without_brackets(cx: &mut gpui::TestAppContext) {
let (buffer_id, mut cx) = setup_indent_guides_editor(
&"
block1
block2
block3
block4
block2
block1
block1"
.unindent(),
cx,
)
.await;
assert_indent_guides(
1..10,
vec![
IndentGuide::new(buffer_id, 1, 4, 0, 4),
IndentGuide::new(buffer_id, 2, 3, 1, 4),
IndentGuide::new(buffer_id, 3, 3, 2, 4),
],
None,
&mut cx,
);
}
#[gpui::test]
async fn test_indent_guides_ends_before_empty_line(cx: &mut gpui::TestAppContext) {
let (buffer_id, mut cx) = setup_indent_guides_editor(
&"
block1
block2
block3
block1
block1"
.unindent(),
cx,
)
.await;
assert_indent_guides(
0..6,
vec![
IndentGuide::new(buffer_id, 1, 2, 0, 4),
IndentGuide::new(buffer_id, 2, 2, 1, 4),
],
None,
&mut cx,
);
}
#[gpui::test]
async fn test_indent_guides_continuing_off_screen(cx: &mut gpui::TestAppContext) {
let (buffer_id, mut cx) = setup_indent_guides_editor(
&"
block1
block2
"
.unindent(),
cx,
)
.await;
assert_indent_guides(
0..1,
vec![IndentGuide::new(buffer_id, 1, 1, 0, 4)],
None,
&mut cx,
);
}
#[gpui::test]
async fn test_active_indent_guides_single_line(cx: &mut gpui::TestAppContext) {
let (buffer_id, mut cx) = setup_indent_guides_editor(
&"
fn main() {
let a = 1;
}"
.unindent(),
cx,
)
.await;
cx.update_editor(|editor, cx| {
editor.change_selections(None, cx, |s| {
s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
});
});
assert_indent_guides(
0..3,
vec![IndentGuide::new(buffer_id, 1, 1, 0, 4)],
Some(vec![0]),
&mut cx,
);
}
#[gpui::test]
async fn test_active_indent_guides_respect_indented_range(cx: &mut gpui::TestAppContext) {
let (buffer_id, mut cx) = setup_indent_guides_editor(
&"
fn main() {
if 1 == 2 {
let a = 1;
}
}"
.unindent(),
cx,
)
.await;
cx.update_editor(|editor, cx| {
editor.change_selections(None, cx, |s| {
s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
});
});
assert_indent_guides(
0..4,
vec![
IndentGuide::new(buffer_id, 1, 3, 0, 4),
IndentGuide::new(buffer_id, 2, 2, 1, 4),
],
Some(vec![1]),
&mut cx,
);
cx.update_editor(|editor, cx| {
editor.change_selections(None, cx, |s| {
s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
});
});
assert_indent_guides(
0..4,
vec![
IndentGuide::new(buffer_id, 1, 3, 0, 4),
IndentGuide::new(buffer_id, 2, 2, 1, 4),
],
Some(vec![1]),
&mut cx,
);
cx.update_editor(|editor, cx| {
editor.change_selections(None, cx, |s| {
s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
});
});
assert_indent_guides(
0..4,
vec![
IndentGuide::new(buffer_id, 1, 3, 0, 4),
IndentGuide::new(buffer_id, 2, 2, 1, 4),
],
Some(vec![0]),
&mut cx,
);
}
#[gpui::test]
async fn test_active_indent_guides_empty_line(cx: &mut gpui::TestAppContext) {
let (buffer_id, mut cx) = setup_indent_guides_editor(
&"
fn main() {
let a = 1;
let b = 2;
}"
.unindent(),
cx,
)
.await;
cx.update_editor(|editor, cx| {
editor.change_selections(None, cx, |s| {
s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
});
});
assert_indent_guides(
0..5,
vec![IndentGuide::new(buffer_id, 1, 3, 0, 4)],
Some(vec![0]),
&mut cx,
);
}
#[gpui::test]
async fn test_active_indent_guides_non_matching_indent(cx: &mut gpui::TestAppContext) {
let (buffer_id, mut cx) = setup_indent_guides_editor(
&"
def m:
a = 1
pass"
.unindent(),
cx,
)
.await;
cx.update_editor(|editor, cx| {
editor.change_selections(None, cx, |s| {
s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
});
});
assert_indent_guides(
0..3,
vec![IndentGuide::new(buffer_id, 1, 2, 0, 4)],
Some(vec![0]),
&mut cx,
);
}
#[gpui::test]
fn test_flap_insertion_and_rendering(cx: &mut TestAppContext) {
init_test(cx, |_| {});
+227 -1
View File
@@ -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,
+164
View File
@@ -0,0 +1,164 @@
use std::{ops::Range, time::Duration};
use collections::HashSet;
use gpui::{AppContext, Task};
use language::BufferRow;
use multi_buffer::{MultiBufferIndentGuide, MultiBufferRow};
use text::{BufferId, Point};
use ui::ViewContext;
use util::ResultExt;
use crate::{DisplaySnapshot, Editor};
struct ActiveIndentedRange {
buffer_id: BufferId,
row_range: Range<BufferRow>,
indent: u32,
}
#[derive(Default)]
pub struct ActiveIndentGuidesState {
pub dirty: bool,
cursor_row: MultiBufferRow,
pending_refresh: Option<Task<()>>,
active_indent_range: Option<ActiveIndentedRange>,
}
impl ActiveIndentGuidesState {
pub fn should_refresh(&self, cursor_row: MultiBufferRow) -> bool {
self.pending_refresh.is_none() && (self.cursor_row != cursor_row || self.dirty)
}
}
impl Editor {
pub fn indent_guides(
&self,
visible_buffer_range: Range<MultiBufferRow>,
snapshot: &DisplaySnapshot,
cx: &AppContext,
) -> Option<Vec<MultiBufferIndentGuide>> {
if self.show_indent_guides == Some(false) {
return None;
}
let settings = self.buffer.read(cx).settings_at(0, cx);
if settings.indent_guides.enabled {
Some(indent_guides_in_range(visible_buffer_range, snapshot, cx))
} else {
None
}
}
pub fn find_active_indent_guide_indices(
&mut self,
indent_guides: &[MultiBufferIndentGuide],
snapshot: &DisplaySnapshot,
cx: &mut ViewContext<Editor>,
) -> Option<HashSet<usize>> {
let selection = self.selections.newest::<Point>(cx);
let cursor_row = MultiBufferRow(selection.head().row);
let state = &mut self.active_indent_guides_state;
if state.cursor_row != cursor_row {
state.cursor_row = cursor_row;
state.dirty = true;
}
if state.should_refresh(cursor_row) {
let snapshot = snapshot.clone();
state.dirty = false;
let task = cx
.background_executor()
.spawn(resolve_indented_range(snapshot, cursor_row));
// Try to resolve the indent in a short amount of time, otherwise move it to a background task.
match cx
.background_executor()
.block_with_timeout(Duration::from_micros(200), task)
{
Ok(result) => state.active_indent_range = result,
Err(future) => {
state.pending_refresh = Some(cx.spawn(|editor, mut cx| async move {
let result = cx.background_executor().spawn(future).await;
editor
.update(&mut cx, |editor, _| {
editor.active_indent_guides_state.active_indent_range = result;
editor.active_indent_guides_state.pending_refresh = None;
})
.log_err();
}));
return None;
}
}
}
let active_indent_range = state.active_indent_range.as_ref()?;
let candidates = indent_guides
.iter()
.enumerate()
.filter(|(_, indent_guide)| {
indent_guide.buffer_id == active_indent_range.buffer_id
&& indent_guide.indent_width() == active_indent_range.indent
});
let mut matches = HashSet::default();
for (i, indent) in candidates {
// Find matches that are either an exact match, partially on screen, or inside the enclosing indent
if active_indent_range.row_range.start <= indent.end_row
&& indent.start_row <= active_indent_range.row_range.end
{
matches.insert(i);
}
}
Some(matches)
}
}
pub fn indent_guides_in_range(
visible_buffer_range: Range<MultiBufferRow>,
snapshot: &DisplaySnapshot,
cx: &AppContext,
) -> Vec<MultiBufferIndentGuide> {
let start_anchor = snapshot
.buffer_snapshot
.anchor_before(Point::new(visible_buffer_range.start.0, 0));
let end_anchor = snapshot
.buffer_snapshot
.anchor_after(Point::new(visible_buffer_range.end.0, 0));
snapshot
.buffer_snapshot
.indent_guides_in_range(start_anchor..end_anchor, cx)
.into_iter()
.filter(|indent_guide| {
// Filter out indent guides that are inside a fold
!snapshot.is_line_folded(indent_guide.multibuffer_row_range.start)
})
.collect()
}
async fn resolve_indented_range(
snapshot: DisplaySnapshot,
buffer_row: MultiBufferRow,
) -> Option<ActiveIndentedRange> {
let (buffer_row, buffer_snapshot, buffer_id) =
if let Some((_, buffer_id, snapshot)) = snapshot.buffer_snapshot.as_singleton() {
(buffer_row.0, snapshot, buffer_id)
} else {
let (snapshot, point) = snapshot.buffer_snapshot.buffer_line_for_row(buffer_row)?;
let buffer_id = snapshot.remote_id();
(point.start.row, snapshot, buffer_id)
};
buffer_snapshot
.enclosing_indent(buffer_row)
.await
.map(|(row_range, indent)| ActiveIndentedRange {
row_range,
indent,
buffer_id,
})
}