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
+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, |_| {});