Detailed changes
@@ -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
@@ -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",
@@ -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
});
@@ -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)
@@ -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,
+ })
+}
@@ -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);
@@ -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(
@@ -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")
@@ -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(),
@@ -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
}
@@ -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,13 +740,26 @@ impl<'a> Lines<'a> {
self.current_line.clear();
while let Some(chunk) = self.chunks.peek() {
- let mut lines = chunk.split('\n').peekable();
- while let Some(line) = lines.next() {
- self.current_line.push_str(line);
- if lines.peek().is_some() {
- self.chunks
- .seek(self.chunks.offset() + line.len() + "\n".len());
- return Some(&self.current_line);
+ 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() {
+ self.chunks
+ .seek(self.chunks.offset() + line.len() + "\n".len());
+ return Some(&self.current_line);
+ }
}
}
@@ -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)]
@@ -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())
@@ -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(),
@@ -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(),
},
}
}
@@ -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],
},
}
}
@@ -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(),
},
}
}));
@@ -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>,
@@ -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());
@@ -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::*;
@@ -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;
+ }
+ }
+}
@@ -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>,
}
@@ -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 {
@@ -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(),