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
parent 3eb0418bda
commit feea607bac
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 1705 additions and 65 deletions

View File

@ -216,6 +216,25 @@
// Whether to show fold buttons in the gutter.
"folds": true
},
"indent_guides": {
/// Whether to show indent guides in the editor.
"enabled": true,
/// The width of the indent guides in pixels, between 1 and 10.
"line_width": 1,
/// Determines how indent guides are colored.
/// This setting can take the following three values:
///
/// 1. "disabled"
/// 2. "fixed"
/// 3. "indent_aware"
"coloring": "fixed",
/// Determines how indent guide backgrounds are colored.
/// This setting can take the following two values:
///
/// 1. "disabled"
/// 2. "indent_aware"
"background_coloring": "disabled"
},
// The number of lines to keep above/below the cursor when scrolling.
"vertical_scroll_margin": 3,
// Scroll sensitivity multiplier. This multiplier is applied

View File

@ -5,6 +5,15 @@
{
"name": "Gruvbox Dark",
"appearance": "dark",
"accents": [
"#cc241dff",
"#98971aff",
"#d79921ff",
"#458588ff",
"#b16286ff",
"#689d6aff",
"#d65d0eff"
],
"style": {
"border": "#5b534dff",
"border.variant": "#494340ff",
@ -379,6 +388,15 @@
{
"name": "Gruvbox Dark Hard",
"appearance": "dark",
"accents": [
"#cc241dff",
"#98971aff",
"#d79921ff",
"#458588ff",
"#b16286ff",
"#689d6aff",
"#d65d0eff"
],
"style": {
"border": "#5b534dff",
"border.variant": "#494340ff",
@ -753,6 +771,15 @@
{
"name": "Gruvbox Dark Soft",
"appearance": "dark",
"accents": [
"#cc241dff",
"#98971aff",
"#d79921ff",
"#458588ff",
"#b16286ff",
"#689d6aff",
"#d65d0eff"
],
"style": {
"border": "#5b534dff",
"border.variant": "#494340ff",
@ -1127,6 +1154,15 @@
{
"name": "Gruvbox Light",
"appearance": "light",
"accents": [
"#cc241dff",
"#98971aff",
"#d79921ff",
"#458588ff",
"#b16286ff",
"#689d6aff",
"#d65d0eff"
],
"style": {
"border": "#c8b899ff",
"border.variant": "#ddcca7ff",
@ -1501,6 +1537,15 @@
{
"name": "Gruvbox Light Hard",
"appearance": "light",
"accents": [
"#cc241dff",
"#98971aff",
"#d79921ff",
"#458588ff",
"#b16286ff",
"#689d6aff",
"#d65d0eff"
],
"style": {
"border": "#c8b899ff",
"border.variant": "#ddcca7ff",
@ -1875,6 +1920,15 @@
{
"name": "Gruvbox Light Soft",
"appearance": "light",
"accents": [
"#cc241dff",
"#98971aff",
"#d79921ff",
"#458588ff",
"#b16286ff",
"#689d6aff",
"#d65d0eff"
],
"style": {
"border": "#c8b899ff",
"border.variant": "#ddcca7ff",

View File

@ -2744,6 +2744,7 @@ impl ConversationEditor {
editor.set_show_git_diff_gutter(false, cx);
editor.set_show_code_actions(false, cx);
editor.set_show_wrap_guides(false, cx);
editor.set_show_indent_guides(false, cx);
editor.set_completion_provider(Box::new(completion_provider));
editor
});

View File

@ -100,6 +100,9 @@ impl MessageEditor {
editor.update(cx, |editor, cx| {
editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
editor.set_use_autoclose(false);
editor.set_show_gutter(false, cx);
editor.set_show_wrap_guides(false, cx);
editor.set_show_indent_guides(false, cx);
editor.set_completion_provider(Box::new(MessageEditorCompletionProvider(this)));
editor.set_auto_replace_emoji_shortcode(
MessageEditorSettings::get_global(cx)

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 {

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) {

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

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,

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,
})
}

View File

@ -185,6 +185,7 @@ impl FeedbackModal {
cx,
);
editor.set_show_gutter(false, cx);
editor.set_show_indent_guides(false, cx);
editor.set_show_inline_completions(false);
editor.set_vertical_scroll_margin(5, cx);
editor.set_use_modal_editing(false);

View File

@ -512,6 +512,37 @@ pub struct Runnable {
pub buffer: BufferId,
}
#[derive(Clone, Debug, PartialEq)]
pub struct IndentGuide {
pub buffer_id: BufferId,
pub start_row: BufferRow,
pub end_row: BufferRow,
pub depth: u32,
pub indent_size: u32,
}
impl IndentGuide {
pub fn new(
buffer_id: BufferId,
start_row: BufferRow,
end_row: BufferRow,
depth: u32,
indent_size: u32,
) -> Self {
Self {
buffer_id,
start_row,
end_row,
depth,
indent_size,
}
}
pub fn indent_width(&self) -> u32 {
self.indent_size * self.depth
}
}
impl Buffer {
/// Create a new buffer with the given base text.
pub fn local<T: Into<String>>(base_text: T, cx: &mut ModelContext<Self>) -> Self {
@ -3059,6 +3090,236 @@ impl BufferSnapshot {
})
}
pub fn indent_guides_in_range(
&self,
range: Range<Anchor>,
cx: &AppContext,
) -> Vec<IndentGuide> {
fn indent_size_for_row(this: &BufferSnapshot, row: BufferRow, cx: &AppContext) -> u32 {
let language = this.language_at(Point::new(row, 0));
language_settings(language, None, cx).tab_size.get() as u32
}
let start_row = range.start.to_point(self).row;
let end_row = range.end.to_point(self).row;
let row_range = start_row..end_row + 1;
let mut row_indents = self.line_indents_in_row_range(row_range.clone());
let mut result_vec = Vec::new();
let mut indent_stack = SmallVec::<[IndentGuide; 8]>::new();
// TODO: This should be calculated for every row but it is pretty expensive
let indent_size = indent_size_for_row(self, start_row, cx);
while let Some((first_row, mut line_indent, empty)) = row_indents.next() {
let current_depth = indent_stack.len() as u32;
// When encountering empty, continue until found useful line indent
// then add to the indent stack with the depth found
let mut found_indent = false;
let mut last_row = first_row;
if empty {
let mut trailing_row = end_row;
while !found_indent {
let (target_row, new_line_indent, empty) =
if let Some(display_row) = row_indents.next() {
display_row
} else {
// This means we reached the end of the given range and found empty lines at the end.
// We need to traverse further until we find a non-empty line to know if we need to add
// an indent guide for the last visible indent.
trailing_row += 1;
const TRAILING_ROW_SEARCH_LIMIT: u32 = 25;
if trailing_row > self.max_point().row
|| trailing_row > end_row + TRAILING_ROW_SEARCH_LIMIT
{
break;
}
let (new_line_indent, empty) = self.line_indent_for_row(trailing_row);
(trailing_row, new_line_indent, empty)
};
if empty {
continue;
}
last_row = target_row.min(end_row);
line_indent = new_line_indent;
found_indent = true;
break;
}
} else {
found_indent = true
}
let depth = if found_indent {
line_indent / indent_size + ((line_indent % indent_size) > 0) as u32
} else {
current_depth
};
if depth < current_depth {
for _ in 0..(current_depth - depth) {
let mut indent = indent_stack.pop().unwrap();
if last_row != first_row {
// In this case, we landed on an empty row, had to seek forward,
// and discovered that the indent we where on is ending.
// This means that the last display row must
// be on line that ends this indent range, so we
// should display the range up to the first non-empty line
indent.end_row = first_row.saturating_sub(1);
}
result_vec.push(indent)
}
} else if depth > current_depth {
for next_depth in current_depth..depth {
indent_stack.push(IndentGuide {
buffer_id: self.remote_id(),
start_row: first_row,
end_row: last_row,
depth: next_depth,
indent_size,
});
}
}
for indent in indent_stack.iter_mut() {
indent.end_row = last_row;
}
}
result_vec.extend(indent_stack);
result_vec
}
pub async fn enclosing_indent(
&self,
mut buffer_row: BufferRow,
) -> Option<(Range<BufferRow>, u32)> {
let max_row = self.max_point().row;
if buffer_row >= max_row {
return None;
}
let (mut target_indent_size, is_blank) = self.line_indent_for_row(buffer_row);
// If the current row is at the start of an indented block, we want to return this
// block as the enclosing indent.
if !is_blank && buffer_row < max_row {
let (next_line_indent, is_blank) = self.line_indent_for_row(buffer_row + 1);
if !is_blank && target_indent_size < next_line_indent {
target_indent_size = next_line_indent;
buffer_row += 1;
}
}
const SEARCH_ROW_LIMIT: u32 = 25000;
const SEARCH_WHITESPACE_ROW_LIMIT: u32 = 2500;
const YIELD_INTERVAL: u32 = 100;
let mut accessed_row_counter = 0;
// If there is a blank line at the current row, search for the next non indented lines
if is_blank {
let start = buffer_row.saturating_sub(SEARCH_WHITESPACE_ROW_LIMIT);
let end = (max_row + 1).min(buffer_row + SEARCH_WHITESPACE_ROW_LIMIT);
let mut non_empty_line_above = None;
for (row, indent_size, is_blank) in self
.text
.reversed_line_indents_in_row_range(start..buffer_row)
{
accessed_row_counter += 1;
if accessed_row_counter == YIELD_INTERVAL {
accessed_row_counter = 0;
yield_now().await;
}
if !is_blank {
non_empty_line_above = Some((row, indent_size));
break;
}
}
let mut non_empty_line_below = None;
for (row, indent_size, is_blank) in
self.text.line_indents_in_row_range((buffer_row + 1)..end)
{
accessed_row_counter += 1;
if accessed_row_counter == YIELD_INTERVAL {
accessed_row_counter = 0;
yield_now().await;
}
if !is_blank {
non_empty_line_below = Some((row, indent_size));
break;
}
}
let (row, indent_size) = match (non_empty_line_above, non_empty_line_below) {
(Some((above_row, above_indent)), Some((below_row, below_indent))) => {
if above_indent >= below_indent {
(above_row, above_indent)
} else {
(below_row, below_indent)
}
}
(Some(above), None) => above,
(None, Some(below)) => below,
_ => return None,
};
target_indent_size = indent_size;
buffer_row = row;
}
let start = buffer_row.saturating_sub(SEARCH_ROW_LIMIT);
let end = (max_row + 1).min(buffer_row + SEARCH_ROW_LIMIT);
let mut start_indent = None;
for (row, indent_size, is_blank) in self
.text
.reversed_line_indents_in_row_range(start..buffer_row)
{
accessed_row_counter += 1;
if accessed_row_counter == YIELD_INTERVAL {
accessed_row_counter = 0;
yield_now().await;
}
if !is_blank && indent_size < target_indent_size {
start_indent = Some((row, indent_size));
break;
}
}
let (start_row, start_indent_size) = start_indent?;
let mut end_indent = (end, None);
for (row, indent_size, is_blank) in
self.text.line_indents_in_row_range((buffer_row + 1)..end)
{
accessed_row_counter += 1;
if accessed_row_counter == YIELD_INTERVAL {
accessed_row_counter = 0;
yield_now().await;
}
if !is_blank && indent_size < target_indent_size {
end_indent = (row.saturating_sub(1), Some(indent_size));
break;
}
}
let (end_row, end_indent_size) = end_indent;
let indent_size = if let Some(end_indent_size) = end_indent_size {
start_indent_size.max(end_indent_size)
} else {
start_indent_size
};
Some((start_row..end_row, indent_size))
}
/// Returns selections for remote peers intersecting the given range.
#[allow(clippy::type_complexity)]
pub fn remote_selections_in_range(

View File

@ -2052,6 +2052,71 @@ fn test_serialization(cx: &mut gpui::AppContext) {
assert_eq!(buffer2.read(cx).text(), "abcDF");
}
#[gpui::test]
async fn test_find_matching_indent(cx: &mut TestAppContext) {
cx.update(|cx| init_settings(cx, |_| {}));
async fn enclosing_indent(
text: impl Into<String>,
buffer_row: u32,
cx: &mut TestAppContext,
) -> Option<(Range<u32>, u32)> {
let buffer = cx.new_model(|cx| Buffer::local(text, cx));
let snapshot = cx.read(|cx| buffer.read(cx).snapshot());
snapshot.enclosing_indent(buffer_row).await
}
assert_eq!(
enclosing_indent(
"
fn b() {
if c {
let d = 2;
}
}"
.unindent(),
1,
cx,
)
.await,
Some((1..2, 4))
);
assert_eq!(
enclosing_indent(
"
fn b() {
if c {
let d = 2;
}
}"
.unindent(),
2,
cx,
)
.await,
Some((1..2, 4))
);
assert_eq!(
enclosing_indent(
"
fn b() {
if c {
let d = 2;
let e = 5;
}
}"
.unindent(),
3,
cx,
)
.await,
Some((1..4, 4))
);
}
#[gpui::test(iterations = 100)]
fn test_random_collaboration(cx: &mut AppContext, mut rng: StdRng) {
let min_peers = env::var("MIN_PEERS")

View File

@ -78,6 +78,8 @@ pub struct LanguageSettings {
pub show_wrap_guides: bool,
/// Character counts at which to show wrap guides in the editor.
pub wrap_guides: Vec<usize>,
/// Indent guide related settings.
pub indent_guides: IndentGuideSettings,
/// Whether or not to perform a buffer format before saving.
pub format_on_save: FormatOnSave,
/// Whether or not to remove any trailing whitespace from lines of a buffer
@ -242,6 +244,9 @@ pub struct LanguageSettingsContent {
/// Default: []
#[serde(default)]
pub wrap_guides: Option<Vec<usize>>,
/// Indent guide related settings.
#[serde(default)]
pub indent_guides: Option<IndentGuideSettings>,
/// Whether or not to perform a buffer format before saving.
///
/// Default: on
@ -411,6 +416,59 @@ pub enum Formatter {
CodeActions(HashMap<String, bool>),
}
/// The settings for indent guides.
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct IndentGuideSettings {
/// Whether to display indent guides in the editor.
///
/// Default: true
#[serde(default = "default_true")]
pub enabled: bool,
/// The width of the indent guides in pixels, between 1 and 10.
///
/// Default: 1
#[serde(default = "line_width")]
pub line_width: u32,
/// Determines how indent guides are colored.
///
/// Default: Fixed
#[serde(default)]
pub coloring: IndentGuideColoring,
/// Determines how indent guide backgrounds are colored.
///
/// Default: Disabled
#[serde(default)]
pub background_coloring: IndentGuideBackgroundColoring,
}
fn line_width() -> u32 {
1
}
/// Determines how indent guides are colored.
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum IndentGuideColoring {
/// Do not render any lines for indent guides.
Disabled,
/// Use the same color for all indentation levels.
#[default]
Fixed,
/// Use a different color for each indentation level.
IndentAware,
}
/// Determines how indent guide backgrounds are colored.
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum IndentGuideBackgroundColoring {
/// Do not render any background for indent guides.
#[default]
Disabled,
/// Use a different color for each indentation level.
IndentAware,
}
/// The settings for inlay hints.
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub struct InlayHintSettings {
@ -715,6 +773,7 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent
);
merge(&mut settings.show_wrap_guides, src.show_wrap_guides);
merge(&mut settings.wrap_guides, src.wrap_guides.clone());
merge(&mut settings.indent_guides, src.indent_guides);
merge(
&mut settings.code_actions_on_format,
src.code_actions_on_format.clone(),

View File

@ -12,9 +12,9 @@ use language::{
char_kind,
language_settings::{language_settings, LanguageSettings},
AutoindentMode, Buffer, BufferChunks, BufferRow, BufferSnapshot, Capability, CharKind, Chunk,
CursorShape, DiagnosticEntry, File, IndentSize, Language, LanguageScope, OffsetRangeExt,
OffsetUtf16, Outline, OutlineItem, Point, PointUtf16, Selection, TextDimension, ToOffset as _,
ToOffsetUtf16 as _, ToPoint as _, ToPointUtf16 as _, TransactionId, Unclipped,
CursorShape, DiagnosticEntry, File, IndentGuide, IndentSize, Language, LanguageScope,
OffsetRangeExt, OffsetUtf16, Outline, OutlineItem, Point, PointUtf16, Selection, TextDimension,
ToOffset as _, ToOffsetUtf16 as _, ToPoint as _, ToPointUtf16 as _, TransactionId, Unclipped,
};
use smallvec::SmallVec;
use std::{
@ -281,6 +281,20 @@ struct ExcerptBytes<'a> {
reversed: bool,
}
#[derive(Clone, Debug, PartialEq)]
pub struct MultiBufferIndentGuide {
pub multibuffer_row_range: Range<MultiBufferRow>,
pub buffer: IndentGuide,
}
impl std::ops::Deref for MultiBufferIndentGuide {
type Target = IndentGuide;
fn deref(&self) -> &Self::Target {
&self.buffer
}
}
impl MultiBuffer {
pub fn new(replica_id: ReplicaId, capability: Capability) -> Self {
Self {
@ -1255,6 +1269,15 @@ impl MultiBuffer {
excerpts
}
pub fn excerpt_buffer_ids(&self) -> Vec<BufferId> {
self.snapshot
.borrow()
.excerpts
.iter()
.map(|entry| entry.buffer_id)
.collect()
}
pub fn excerpt_ids(&self) -> Vec<ExcerptId> {
self.snapshot
.borrow()
@ -3182,6 +3205,52 @@ impl MultiBufferSnapshot {
})
}
pub fn indent_guides_in_range(
&self,
range: Range<Anchor>,
cx: &AppContext,
) -> Vec<MultiBufferIndentGuide> {
// Fast path for singleton buffers, we can skip the conversion between offsets.
if let Some((_, _, snapshot)) = self.as_singleton() {
return snapshot
.indent_guides_in_range(range.start.text_anchor..range.end.text_anchor, cx)
.into_iter()
.map(|guide| MultiBufferIndentGuide {
multibuffer_row_range: MultiBufferRow(guide.start_row)
..MultiBufferRow(guide.end_row),
buffer: guide,
})
.collect();
}
let range = range.start.to_offset(self)..range.end.to_offset(self);
self.excerpts_for_range(range.clone())
.flat_map(move |(excerpt, excerpt_offset)| {
let excerpt_buffer_start_row =
excerpt.range.context.start.to_point(&excerpt.buffer).row;
let excerpt_offset_row = crate::ToPoint::to_point(&excerpt_offset, self).row;
excerpt
.buffer
.indent_guides_in_range(excerpt.range.context.clone(), cx)
.into_iter()
.map(move |indent_guide| {
let start_row = excerpt_offset_row
+ (indent_guide.start_row - excerpt_buffer_start_row);
let end_row =
excerpt_offset_row + (indent_guide.end_row - excerpt_buffer_start_row);
MultiBufferIndentGuide {
multibuffer_row_range: MultiBufferRow(start_row)
..MultiBufferRow(end_row),
buffer: indent_guide,
}
})
})
.collect()
}
pub fn diagnostics_update_count(&self) -> usize {
self.diagnostics_update_count
}

View File

@ -619,10 +619,12 @@ impl<'a> Chunks<'a> {
}
pub fn lines(self) -> Lines<'a> {
let reversed = self.reversed;
Lines {
chunks: self,
current_line: String::new(),
done: false,
reversed,
}
}
}
@ -726,6 +728,7 @@ pub struct Lines<'a> {
chunks: Chunks<'a>,
current_line: String,
done: bool,
reversed: bool,
}
impl<'a> Lines<'a> {
@ -737,7 +740,19 @@ impl<'a> Lines<'a> {
self.current_line.clear();
while let Some(chunk) = self.chunks.peek() {
let mut lines = chunk.split('\n').peekable();
let lines = chunk.split('\n');
if self.reversed {
let mut lines = lines.rev().peekable();
while let Some(line) = lines.next() {
self.current_line.insert_str(0, line);
if lines.peek().is_some() {
self.chunks
.seek(self.chunks.offset() - line.len() - "\n".len());
return Some(&self.current_line);
}
}
} else {
let mut lines = lines.peekable();
while let Some(line) = lines.next() {
self.current_line.push_str(line);
if lines.peek().is_some() {
@ -746,6 +761,7 @@ impl<'a> Lines<'a> {
return Some(&self.current_line);
}
}
}
self.chunks.next();
}
@ -1355,6 +1371,21 @@ mod tests {
assert_eq!(lines.next(), Some("hi"));
assert_eq!(lines.next(), Some(""));
assert_eq!(lines.next(), None);
let rope = Rope::from("abc\ndefg\nhi");
let mut lines = rope.reversed_chunks_in_range(0..rope.len()).lines();
assert_eq!(lines.next(), Some("hi"));
assert_eq!(lines.next(), Some("defg"));
assert_eq!(lines.next(), Some("abc"));
assert_eq!(lines.next(), None);
let rope = Rope::from("abc\ndefg\nhi\n");
let mut lines = rope.reversed_chunks_in_range(0..rope.len()).lines();
assert_eq!(lines.next(), Some(""));
assert_eq!(lines.next(), Some("hi"));
assert_eq!(lines.next(), Some("defg"));
assert_eq!(lines.next(), Some("abc"));
assert_eq!(lines.next(), None);
}
#[gpui::test(iterations = 100)]

View File

@ -1865,6 +1865,87 @@ impl BufferSnapshot {
(row_end_offset - row_start_offset) as u32
}
pub fn line_indents_in_row_range(
&self,
row_range: Range<u32>,
) -> impl Iterator<Item = (u32, u32, bool)> + '_ {
let start = Point::new(row_range.start, 0).to_offset(self);
let end = Point::new(row_range.end, 0).to_offset(self);
let mut lines = self.as_rope().chunks_in_range(start..end).lines();
let mut row = row_range.start;
std::iter::from_fn(move || {
if let Some(line) = lines.next() {
let mut indent_size = 0;
let mut is_blank = true;
for c in line.chars() {
is_blank = false;
if c == ' ' || c == '\t' {
indent_size += 1;
} else {
break;
}
}
row += 1;
Some((row - 1, indent_size, is_blank))
} else {
None
}
})
}
pub fn reversed_line_indents_in_row_range(
&self,
row_range: Range<u32>,
) -> impl Iterator<Item = (u32, u32, bool)> + '_ {
let start = Point::new(row_range.start, 0).to_offset(self);
let end = Point::new(row_range.end, 0)
.to_offset(self)
.saturating_sub(1);
let mut lines = self.as_rope().reversed_chunks_in_range(start..end).lines();
let mut row = row_range.end;
std::iter::from_fn(move || {
if let Some(line) = lines.next() {
let mut indent_size = 0;
let mut is_blank = true;
for c in line.chars() {
is_blank = false;
if c == ' ' || c == '\t' {
indent_size += 1;
} else {
break;
}
}
row = row.saturating_sub(1);
Some((row, indent_size, is_blank))
} else {
None
}
})
}
pub fn line_indent_for_row(&self, row: u32) -> (u32, bool) {
let mut indent_size = 0;
let mut is_blank = false;
for c in self.chars_at(Point::new(row, 0)) {
if c == ' ' || c == '\t' {
indent_size += 1;
} else {
if c == '\n' {
is_blank = true;
}
break;
}
}
(indent_size, is_blank)
}
pub fn is_line_blank(&self, row: u32) -> bool {
self.text_for_range(Point::new(row, 0)..Point::new(row, self.line_len(row)))
.all(|chunk| chunk.matches(|c: char| !c.is_whitespace()).next().is_none())

View File

@ -75,6 +75,8 @@ impl ThemeColors {
editor_invisible: neutral().light().step_10(),
editor_wrap_guide: neutral().light_alpha().step_7(),
editor_active_wrap_guide: neutral().light_alpha().step_8(),
editor_indent_guide: neutral().light_alpha().step_5(),
editor_indent_guide_active: neutral().light_alpha().step_6(),
editor_document_highlight_read_background: neutral().light_alpha().step_3(),
editor_document_highlight_write_background: neutral().light_alpha().step_4(),
terminal_background: neutral().light().step_1(),
@ -170,6 +172,8 @@ impl ThemeColors {
editor_invisible: neutral().dark_alpha().step_4(),
editor_wrap_guide: neutral().dark_alpha().step_4(),
editor_active_wrap_guide: neutral().dark_alpha().step_4(),
editor_indent_guide: neutral().dark_alpha().step_4(),
editor_indent_guide_active: neutral().dark_alpha().step_6(),
editor_document_highlight_read_background: neutral().dark_alpha().step_4(),
editor_document_highlight_write_background: neutral().dark_alpha().step_4(),
terminal_background: neutral().dark().step_1(),

View File

@ -2,7 +2,7 @@ use std::sync::Arc;
use gpui::WindowBackgroundAppearance;
use crate::prelude::*;
use crate::AccentColors;
use crate::{
default_color_scales,
@ -23,21 +23,7 @@ fn zed_pro_daylight() -> Theme {
status: StatusColors::light(),
player: PlayerColors::light(),
syntax: Arc::new(SyntaxTheme::default()),
accents: vec![
blue().light().step_9(),
orange().light().step_9(),
pink().light().step_9(),
lime().light().step_9(),
purple().light().step_9(),
amber().light().step_9(),
jade().light().step_9(),
tomato().light().step_9(),
cyan().light().step_9(),
gold().light().step_9(),
grass().light().step_9(),
indigo().light().step_9(),
iris().light().step_9(),
],
accents: AccentColors::light(),
},
}
}
@ -54,21 +40,7 @@ pub(crate) fn zed_pro_moonlight() -> Theme {
status: StatusColors::dark(),
player: PlayerColors::dark(),
syntax: Arc::new(SyntaxTheme::default()),
accents: vec![
blue().dark().step_9(),
orange().dark().step_9(),
pink().dark().step_9(),
lime().dark().step_9(),
purple().dark().step_9(),
amber().dark().step_9(),
jade().dark().step_9(),
tomato().dark().step_9(),
cyan().dark().step_9(),
gold().dark().step_9(),
grass().dark().step_9(),
indigo().dark().step_9(),
iris().dark().step_9(),
],
accents: AccentColors::dark(),
},
}
}

View File

@ -3,8 +3,8 @@ use std::sync::Arc;
use gpui::{hsla, FontStyle, FontWeight, HighlightStyle, WindowBackgroundAppearance};
use crate::{
default_color_scales, Appearance, PlayerColors, StatusColors, SyntaxTheme, SystemColors, Theme,
ThemeColors, ThemeFamily, ThemeStyles,
default_color_scales, AccentColors, Appearance, PlayerColors, StatusColors, SyntaxTheme,
SystemColors, Theme, ThemeColors, ThemeFamily, ThemeStyles,
};
// Note: This theme family is not the one you see in Zed at the moment.
@ -42,6 +42,7 @@ pub(crate) fn one_dark() -> Theme {
styles: ThemeStyles {
window_background_appearance: WindowBackgroundAppearance::Opaque,
system: SystemColors::default(),
accents: AccentColors(vec![blue, orange, purple, teal, red, green, yellow]),
colors: ThemeColors {
border: hsla(225. / 360., 13. / 100., 12. / 100., 1.),
border_variant: hsla(228. / 360., 8. / 100., 25. / 100., 1.),
@ -91,6 +92,8 @@ pub(crate) fn one_dark() -> Theme {
editor_invisible: hsla(222.0 / 360., 11.5 / 100., 34.1 / 100., 1.0),
editor_wrap_guide: hsla(228. / 360., 8. / 100., 25. / 100., 1.),
editor_active_wrap_guide: hsla(228. / 360., 8. / 100., 25. / 100., 1.),
editor_indent_guide: hsla(228. / 360., 8. / 100., 25. / 100., 1.),
editor_indent_guide_active: hsla(225. / 360., 13. / 100., 12. / 100., 1.),
editor_document_highlight_read_background: hsla(
207.8 / 360.,
81. / 100.,
@ -249,7 +252,6 @@ pub(crate) fn one_dark() -> Theme {
("variant".into(), HighlightStyle::default()),
],
}),
accents: vec![blue, orange, purple, teal],
},
}
}

View File

@ -12,8 +12,9 @@ use refineable::Refineable;
use util::ResultExt;
use crate::{
try_parse_color, Appearance, AppearanceContent, PlayerColors, StatusColors, SyntaxTheme,
SystemColors, Theme, ThemeColors, ThemeContent, ThemeFamily, ThemeFamilyContent, ThemeStyles,
try_parse_color, AccentColors, Appearance, AppearanceContent, PlayerColors, StatusColors,
SyntaxTheme, SystemColors, Theme, ThemeColors, ThemeContent, ThemeFamily, ThemeFamilyContent,
ThemeStyles,
};
#[derive(Debug, Clone)]
@ -118,6 +119,12 @@ impl ThemeRegistry {
};
player_colors.merge(&user_theme.style.players);
let mut accent_colors = match user_theme.appearance {
AppearanceContent::Light => AccentColors::light(),
AppearanceContent::Dark => AccentColors::dark(),
};
accent_colors.merge(&user_theme.style.accents);
let syntax_highlights = user_theme
.style
.syntax
@ -156,11 +163,11 @@ impl ThemeRegistry {
styles: ThemeStyles {
system: SystemColors::default(),
window_background_appearance,
accents: accent_colors,
colors: theme_colors,
status: status_colors,
player: player_colors,
syntax: syntax_theme,
accents: Vec::new(),
},
}
}));

View File

@ -75,6 +75,9 @@ pub struct ThemeStyleContent {
#[serde(default, rename = "background.appearance")]
pub window_background_appearance: Option<WindowBackgroundContent>,
#[serde(default)]
pub accents: Vec<AccentContent>,
#[serde(flatten, default)]
pub colors: ThemeColorsContent,
@ -381,6 +384,12 @@ pub struct ThemeColorsContent {
#[serde(rename = "editor.active_wrap_guide")]
pub editor_active_wrap_guide: Option<String>,
#[serde(rename = "editor.indent_guide")]
pub editor_indent_guide: Option<String>,
#[serde(rename = "editor.indent_guide_active")]
pub editor_indent_guide_active: Option<String>,
/// Read-access of a symbol, like reading a variable.
///
/// A document highlight is a range inside a text document which deserves
@ -747,6 +756,14 @@ impl ThemeColorsContent {
.editor_active_wrap_guide
.as_ref()
.and_then(|color| try_parse_color(color).ok()),
editor_indent_guide: self
.editor_indent_guide
.as_ref()
.and_then(|color| try_parse_color(color).ok()),
editor_indent_guide_active: self
.editor_indent_guide_active
.as_ref()
.and_then(|color| try_parse_color(color).ok()),
editor_document_highlight_read_background: self
.editor_document_highlight_read_background
.as_ref()
@ -1196,6 +1213,9 @@ impl StatusColorsContent {
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct AccentContent(pub Option<String>);
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct PlayerColorContent {
pub cursor: Option<String>,

View File

@ -325,6 +325,7 @@ impl ThemeSettings {
.status
.refine(&theme_overrides.status_colors_refinement());
base_theme.styles.player.merge(&theme_overrides.players);
base_theme.styles.accents.merge(&theme_overrides.accents);
base_theme.styles.syntax =
SyntaxTheme::merge(base_theme.styles.syntax, theme_overrides.syntax_overrides());

View File

@ -1,3 +1,4 @@
mod accents;
mod colors;
mod players;
mod status;
@ -7,6 +8,7 @@ mod system;
#[cfg(feature = "stories")]
mod stories;
pub use accents::*;
pub use colors::*;
pub use players::*;
pub use status::*;

View File

@ -0,0 +1,85 @@
use gpui::Hsla;
use serde_derive::Deserialize;
use crate::{
amber, blue, cyan, gold, grass, indigo, iris, jade, lime, orange, pink, purple, tomato,
try_parse_color, AccentContent,
};
/// A collection of colors that are used to color indent aware lines in the editor.
#[derive(Clone, Deserialize)]
pub struct AccentColors(pub Vec<Hsla>);
impl Default for AccentColors {
/// Don't use this!
/// We have to have a default to be `[refineable::Refinable]`.
/// TODO "Find a way to not need this for Refinable"
fn default() -> Self {
Self::dark()
}
}
impl AccentColors {
pub fn dark() -> Self {
Self(vec![
blue().dark().step_9(),
orange().dark().step_9(),
pink().dark().step_9(),
lime().dark().step_9(),
purple().dark().step_9(),
amber().dark().step_9(),
jade().dark().step_9(),
tomato().dark().step_9(),
cyan().dark().step_9(),
gold().dark().step_9(),
grass().dark().step_9(),
indigo().dark().step_9(),
iris().dark().step_9(),
])
}
pub fn light() -> Self {
Self(vec![
blue().light().step_9(),
orange().light().step_9(),
pink().light().step_9(),
lime().light().step_9(),
purple().light().step_9(),
amber().light().step_9(),
jade().light().step_9(),
tomato().light().step_9(),
cyan().light().step_9(),
gold().light().step_9(),
grass().light().step_9(),
indigo().light().step_9(),
iris().light().step_9(),
])
}
}
impl AccentColors {
pub fn color_for_index(&self, index: u32) -> Hsla {
self.0[index as usize % self.0.len()]
}
/// Merges the given accent colors into this [`AccentColors`] instance.
pub fn merge(&mut self, accent_colors: &[AccentContent]) {
if accent_colors.is_empty() {
return;
}
let colors = accent_colors
.iter()
.filter_map(|accent_color| {
accent_color
.0
.as_ref()
.and_then(|color| try_parse_color(color).ok())
})
.collect::<Vec<_>>();
if !colors.is_empty() {
self.0 = colors;
}
}
}

View File

@ -2,7 +2,9 @@ use gpui::{Hsla, WindowBackgroundAppearance};
use refineable::Refineable;
use std::sync::Arc;
use crate::{PlayerColors, StatusColors, StatusColorsRefinement, SyntaxTheme, SystemColors};
use crate::{
AccentColors, PlayerColors, StatusColors, StatusColorsRefinement, SyntaxTheme, SystemColors,
};
#[derive(Refineable, Clone, Debug)]
#[refineable(Debug, serde::Deserialize)]
@ -154,6 +156,8 @@ pub struct ThemeColors {
pub editor_invisible: Hsla,
pub editor_wrap_guide: Hsla,
pub editor_active_wrap_guide: Hsla,
pub editor_indent_guide: Hsla,
pub editor_indent_guide_active: Hsla,
/// Read-access of a symbol, like reading a variable.
///
/// A document highlight is a range inside a text document which deserves
@ -242,7 +246,7 @@ pub struct ThemeStyles {
/// An array of colors used for theme elements that iterate through a series of colors.
///
/// Example: Player colors, rainbow brackets and indent guides, etc.
pub accents: Vec<Hsla>,
pub accents: AccentColors,
#[refineable]
pub colors: ThemeColors,
@ -251,6 +255,7 @@ pub struct ThemeStyles {
pub status: StatusColors,
pub player: PlayerColors,
pub syntax: Arc<SyntaxTheme>,
}

View File

@ -125,6 +125,12 @@ impl Theme {
&self.styles.system
}
/// Returns the [`AccentColors`] for the theme.
#[inline(always)]
pub fn accents(&self) -> &AccentColors {
&self.styles.accents
}
/// Returns the [`PlayerColors`] for the theme.
#[inline(always)]
pub fn players(&self) -> &PlayerColors {

View File

@ -57,6 +57,7 @@ impl VsCodeThemeConverter {
appearance,
style: ThemeStyleContent {
window_background_appearance: Some(theme::WindowBackgroundContent::Opaque),
accents: Vec::new(), //TODO can we read this from the theme?
colors: theme_colors,
status: status_colors,
players: Vec::new(),