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