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
@@ -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, |_| {});
|
||||
|
||||
Reference in New Issue
Block a user