indent guides: Respect language specific settings in multibuffers (#12528)

Bennet Bo Fenner created

Indent guides can be configured per language, meaning that in a multi
buffer we can get excerpts where indent guides should be
disabled/enabled/styled differently than other excerpts.

Imagine the following scenario, i have indent guides disabled in my
settings, but want to enable them for JS and Python. I also want to use
a different line width for python files. Something like this is now
supported:

<img width="445" alt="image"
src="https://github.com/zed-industries/zed/assets/53836821/0c91411c-145c-4210-a883-4c469d5cb828">

And the relevant settings for the example above:
```json
"indent_guides": {
  "enabled": false
},
"languages": {
  "JavaScript": {
    "indent_guides": {
      "enabled": true
    }
  },
  "Python": {
    "indent_guides": {
      "enabled": true,
      "line_width": 5
    }
  }
}
```



Release Notes:

- Respect language specific settings when showing indent guides in a
multibuffer
- Fixes an issue where indent guide specific settings were not
recognized when specified in local settings

Change summary

crates/editor/src/editor.rs             |  16 ++--
crates/editor/src/editor_tests.rs       | 105 +++++++++++++-------------
crates/editor/src/element.rs            |  17 +--
crates/editor/src/indent_guides.rs      |  35 ++++++--
crates/language/src/buffer.rs           |  28 ++----
crates/multi_buffer/src/multi_buffer.rs |  13 ++
6 files changed, 113 insertions(+), 101 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -9770,19 +9770,19 @@ impl Editor {
     }
 
     pub fn toggle_indent_guides(&mut self, _: &ToggleIndentGuides, cx: &mut ViewContext<Self>) {
-        let currently_enabled = self.should_show_indent_guides(cx);
-        self.show_indent_guides = Some(!currently_enabled);
-        cx.notify();
-    }
-
-    fn should_show_indent_guides(&self, cx: &mut ViewContext<Self>) -> bool {
-        self.show_indent_guides.unwrap_or_else(|| {
+        let currently_enabled = self.should_show_indent_guides().unwrap_or_else(|| {
             self.buffer
                 .read(cx)
                 .settings_at(0, cx)
                 .indent_guides
                 .enabled
-        })
+        });
+        self.show_indent_guides = Some(!currently_enabled);
+        cx.notify();
+    }
+
+    fn should_show_indent_guides(&self) -> Option<bool> {
+        self.show_indent_guides
     }
 
     pub fn toggle_line_numbers(&mut self, _: &ToggleLineNumbers, cx: &mut ViewContext<Self>) {

crates/editor/src/editor_tests.rs 🔗

@@ -20,6 +20,7 @@ use language::{
     FakeLspAdapter, IndentGuide, LanguageConfig, LanguageConfigOverride, LanguageMatcher, Override,
     Point,
 };
+use language_settings::IndentGuideSettings;
 use multi_buffer::MultiBufferIndentGuide;
 use parking_lot::Mutex;
 use project::project_settings::{LspSettings, ProjectSettings};
@@ -11505,6 +11506,7 @@ fn assert_indent_guides(
         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),
+            true,
             &snapshot,
             cx,
         );
@@ -11543,6 +11545,21 @@ fn assert_indent_guides(
     assert_eq!(indent_guides, expected, "Indent guides do not match");
 }
 
+fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
+    IndentGuide {
+        buffer_id,
+        start_row,
+        end_row,
+        depth,
+        tab_size: 4,
+        settings: IndentGuideSettings {
+            enabled: true,
+            line_width: 1,
+            ..Default::default()
+        },
+    }
+}
+
 #[gpui::test]
 async fn test_indent_guide_single_line(cx: &mut gpui::TestAppContext) {
     let (buffer_id, mut cx) = setup_indent_guides_editor(
@@ -11555,12 +11572,7 @@ async fn test_indent_guide_single_line(cx: &mut gpui::TestAppContext) {
     )
     .await;
 
-    assert_indent_guides(
-        0..3,
-        vec![IndentGuide::new(buffer_id, 1, 1, 0, 4)],
-        None,
-        &mut cx,
-    );
+    assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
 }
 
 #[gpui::test]
@@ -11576,12 +11588,7 @@ async fn test_indent_guide_simple_block(cx: &mut gpui::TestAppContext) {
     )
     .await;
 
-    assert_indent_guides(
-        0..4,
-        vec![IndentGuide::new(buffer_id, 1, 2, 0, 4)],
-        None,
-        &mut cx,
-    );
+    assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
 }
 
 #[gpui::test]
@@ -11604,9 +11611,9 @@ async fn test_indent_guide_nested(cx: &mut gpui::TestAppContext) {
     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),
+            indent_guide(buffer_id, 1, 6, 0),
+            indent_guide(buffer_id, 3, 3, 1),
+            indent_guide(buffer_id, 5, 5, 1),
         ],
         None,
         &mut cx,
@@ -11630,8 +11637,8 @@ async fn test_indent_guide_tab(cx: &mut gpui::TestAppContext) {
     assert_indent_guides(
         0..5,
         vec![
-            IndentGuide::new(buffer_id, 1, 3, 0, 4),
-            IndentGuide::new(buffer_id, 2, 2, 1, 4),
+            indent_guide(buffer_id, 1, 3, 0),
+            indent_guide(buffer_id, 2, 2, 1),
         ],
         None,
         &mut cx,
@@ -11652,12 +11659,7 @@ async fn test_indent_guide_continues_on_empty_line(cx: &mut gpui::TestAppContext
     )
     .await;
 
-    assert_indent_guides(
-        0..5,
-        vec![IndentGuide::new(buffer_id, 1, 3, 0, 4)],
-        None,
-        &mut cx,
-    );
+    assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
 }
 
 #[gpui::test]
@@ -11683,9 +11685,9 @@ async fn test_indent_guide_complex(cx: &mut gpui::TestAppContext) {
     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),
+            indent_guide(buffer_id, 1, 9, 0),
+            indent_guide(buffer_id, 6, 6, 1),
+            indent_guide(buffer_id, 8, 8, 1),
         ],
         None,
         &mut cx,
@@ -11715,9 +11717,9 @@ async fn test_indent_guide_starts_off_screen(cx: &mut gpui::TestAppContext) {
     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),
+            indent_guide(buffer_id, 1, 9, 0),
+            indent_guide(buffer_id, 6, 6, 1),
+            indent_guide(buffer_id, 8, 8, 1),
         ],
         None,
         &mut cx,
@@ -11747,9 +11749,9 @@ async fn test_indent_guide_ends_off_screen(cx: &mut gpui::TestAppContext) {
     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),
+            indent_guide(buffer_id, 1, 9, 0),
+            indent_guide(buffer_id, 6, 6, 1),
+            indent_guide(buffer_id, 8, 8, 1),
         ],
         None,
         &mut cx,
@@ -11775,9 +11777,9 @@ async fn test_indent_guide_without_brackets(cx: &mut gpui::TestAppContext) {
     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),
+            indent_guide(buffer_id, 1, 4, 0),
+            indent_guide(buffer_id, 2, 3, 1),
+            indent_guide(buffer_id, 3, 3, 2),
         ],
         None,
         &mut cx,
@@ -11802,8 +11804,8 @@ async fn test_indent_guide_ends_before_empty_line(cx: &mut gpui::TestAppContext)
     assert_indent_guides(
         0..6,
         vec![
-            IndentGuide::new(buffer_id, 1, 2, 0, 4),
-            IndentGuide::new(buffer_id, 2, 2, 1, 4),
+            indent_guide(buffer_id, 1, 2, 0),
+            indent_guide(buffer_id, 2, 2, 1),
         ],
         None,
         &mut cx,
@@ -11825,12 +11827,7 @@ async fn test_indent_guide_continuing_off_screen(cx: &mut gpui::TestAppContext)
     )
     .await;
 
-    assert_indent_guides(
-        0..1,
-        vec![IndentGuide::new(buffer_id, 1, 1, 0, 4)],
-        None,
-        &mut cx,
-    );
+    assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
 }
 
 #[gpui::test]
@@ -11852,8 +11849,8 @@ async fn test_indent_guide_tabs(cx: &mut gpui::TestAppContext) {
     assert_indent_guides(
         0..6,
         vec![
-            IndentGuide::new(buffer_id, 1, 6, 0, 4),
-            IndentGuide::new(buffer_id, 3, 4, 1, 4),
+            indent_guide(buffer_id, 1, 6, 0),
+            indent_guide(buffer_id, 3, 4, 1),
         ],
         None,
         &mut cx,
@@ -11880,7 +11877,7 @@ async fn test_active_indent_guide_single_line(cx: &mut gpui::TestAppContext) {
 
     assert_indent_guides(
         0..3,
-        vec![IndentGuide::new(buffer_id, 1, 1, 0, 4)],
+        vec![indent_guide(buffer_id, 1, 1, 0)],
         Some(vec![0]),
         &mut cx,
     );
@@ -11909,8 +11906,8 @@ async fn test_active_indent_guide_respect_indented_range(cx: &mut gpui::TestAppC
     assert_indent_guides(
         0..4,
         vec![
-            IndentGuide::new(buffer_id, 1, 3, 0, 4),
-            IndentGuide::new(buffer_id, 2, 2, 1, 4),
+            indent_guide(buffer_id, 1, 3, 0),
+            indent_guide(buffer_id, 2, 2, 1),
         ],
         Some(vec![1]),
         &mut cx,
@@ -11925,8 +11922,8 @@ async fn test_active_indent_guide_respect_indented_range(cx: &mut gpui::TestAppC
     assert_indent_guides(
         0..4,
         vec![
-            IndentGuide::new(buffer_id, 1, 3, 0, 4),
-            IndentGuide::new(buffer_id, 2, 2, 1, 4),
+            indent_guide(buffer_id, 1, 3, 0),
+            indent_guide(buffer_id, 2, 2, 1),
         ],
         Some(vec![1]),
         &mut cx,
@@ -11941,8 +11938,8 @@ async fn test_active_indent_guide_respect_indented_range(cx: &mut gpui::TestAppC
     assert_indent_guides(
         0..4,
         vec![
-            IndentGuide::new(buffer_id, 1, 3, 0, 4),
-            IndentGuide::new(buffer_id, 2, 2, 1, 4),
+            indent_guide(buffer_id, 1, 3, 0),
+            indent_guide(buffer_id, 2, 2, 1),
         ],
         Some(vec![0]),
         &mut cx,
@@ -11971,7 +11968,7 @@ async fn test_active_indent_guide_empty_line(cx: &mut gpui::TestAppContext) {
 
     assert_indent_guides(
         0..5,
-        vec![IndentGuide::new(buffer_id, 1, 3, 0, 4)],
+        vec![indent_guide(buffer_id, 1, 3, 0)],
         Some(vec![0]),
         &mut cx,
     );
@@ -11997,7 +11994,7 @@ async fn test_active_indent_guide_non_matching_indent(cx: &mut gpui::TestAppCont
 
     assert_indent_guides(
         0..3,
-        vec![IndentGuide::new(buffer_id, 1, 2, 0, 4)],
+        vec![indent_guide(buffer_id, 1, 2, 0)],
         Some(vec![0]),
         &mut cx,
     );

crates/editor/src/element.rs 🔗

@@ -38,7 +38,7 @@ use gpui::{
 };
 use itertools::Itertools;
 use language::language_settings::{
-    IndentGuideBackgroundColoring, IndentGuideColoring, ShowWhitespaceSetting,
+    IndentGuideBackgroundColoring, IndentGuideColoring, IndentGuideSettings, ShowWhitespaceSetting,
 };
 use lsp::DiagnosticSeverity;
 use multi_buffer::{Anchor, MultiBufferPoint, MultiBufferRow};
@@ -1438,6 +1438,7 @@ impl EditorElement {
                             single_indent_width,
                             depth: indent_guide.depth,
                             active: active_indent_guide_indices.contains(&i),
+                            settings: indent_guide.settings,
                         })
                     } else {
                         None
@@ -2730,14 +2731,6 @@ impl EditorElement {
             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;
@@ -2746,6 +2739,7 @@ impl EditorElement {
 
         for indent_guide in indent_guides {
             let indent_accent_colors = cx.theme().accents().color_for_index(indent_guide.depth);
+            let settings = indent_guide.settings;
 
             // TODO fixed for now, expose them through themes later
             const INDENT_AWARE_ALPHA: f32 = 0.2;
@@ -2753,7 +2747,7 @@ impl EditorElement {
             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) {
+            let line_color = match (settings.coloring, indent_guide.active) {
                 (IndentGuideColoring::Disabled, _) => None,
                 (IndentGuideColoring::Fixed, false) => {
                     Some(cx.theme().colors().editor_indent_guide)
@@ -2769,7 +2763,7 @@ impl EditorElement {
                 }
             };
 
-            let background_color = match (&settings.background_coloring, indent_guide.active) {
+            let background_color = match (settings.background_coloring, indent_guide.active) {
                 (IndentGuideBackgroundColoring::Disabled, _) => None,
                 (IndentGuideBackgroundColoring::IndentAware, false) => Some(faded_color(
                     indent_accent_colors,
@@ -5286,6 +5280,7 @@ pub struct IndentGuideLayout {
     single_indent_width: Pixels,
     depth: u32,
     active: bool,
+    settings: IndentGuideSettings,
 }
 
 pub struct CursorLayout {

crates/editor/src/indent_guides.rs 🔗

@@ -2,7 +2,7 @@ use std::{ops::Range, time::Duration};
 
 use collections::HashSet;
 use gpui::{AppContext, Task};
-use language::BufferRow;
+use language::{language_settings::language_settings, BufferRow};
 use multi_buffer::{MultiBufferIndentGuide, MultiBufferRow};
 use text::{BufferId, LineIndent, Point};
 use ui::ViewContext;
@@ -37,13 +37,26 @@ impl Editor {
         snapshot: &DisplaySnapshot,
         cx: &mut ViewContext<Editor>,
     ) -> Option<Vec<MultiBufferIndentGuide>> {
-        let enabled = self.should_show_indent_guides(cx);
+        let show_indent_guides = self.should_show_indent_guides().unwrap_or_else(|| {
+            if let Some(buffer) = self.buffer().read(cx).as_singleton() {
+                language_settings(buffer.read(cx).language(), buffer.read(cx).file(), cx)
+                    .indent_guides
+                    .enabled
+            } else {
+                true
+            }
+        });
 
-        if enabled {
-            Some(indent_guides_in_range(visible_buffer_range, snapshot, cx))
-        } else {
-            None
+        if !show_indent_guides {
+            return None;
         }
+
+        Some(indent_guides_in_range(
+            visible_buffer_range,
+            self.should_show_indent_guides() == Some(true),
+            snapshot,
+            cx,
+        ))
     }
 
     pub fn find_active_indent_guide_indices(
@@ -77,9 +90,14 @@ impl Editor {
 
         if state.should_refresh() {
             state.cursor_row = cursor_row;
-            let snapshot = snapshot.clone();
             state.dirty = false;
 
+            if indent_guides.is_empty() {
+                return None;
+            }
+
+            let snapshot = snapshot.clone();
+
             let task = cx
                 .background_executor()
                 .spawn(resolve_indented_range(snapshot, cursor_row));
@@ -131,6 +149,7 @@ impl Editor {
 
 pub fn indent_guides_in_range(
     visible_buffer_range: Range<MultiBufferRow>,
+    ignore_disabled_for_language: bool,
     snapshot: &DisplaySnapshot,
     cx: &AppContext,
 ) -> Vec<MultiBufferIndentGuide> {
@@ -143,7 +162,7 @@ pub fn indent_guides_in_range(
 
     snapshot
         .buffer_snapshot
-        .indent_guides_in_range(start_anchor..end_anchor, cx)
+        .indent_guides_in_range(start_anchor..end_anchor, ignore_disabled_for_language, cx)
         .into_iter()
         .filter(|indent_guide| {
             // Filter out indent guides that are inside a fold

crates/language/src/buffer.rs 🔗

@@ -6,7 +6,7 @@ pub use crate::{
 };
 use crate::{
     diagnostic_set::{DiagnosticEntry, DiagnosticGroup},
-    language_settings::{language_settings, LanguageSettings},
+    language_settings::{language_settings, IndentGuideSettings, LanguageSettings},
     markdown::parse_markdown,
     outline::OutlineItem,
     syntax_map::{
@@ -542,25 +542,10 @@ pub struct IndentGuide {
     pub end_row: BufferRow,
     pub depth: u32,
     pub tab_size: u32,
+    pub settings: IndentGuideSettings,
 }
 
 impl IndentGuide {
-    pub fn new(
-        buffer_id: BufferId,
-        start_row: BufferRow,
-        end_row: BufferRow,
-        depth: u32,
-        tab_size: u32,
-    ) -> Self {
-        Self {
-            buffer_id,
-            start_row,
-            end_row,
-            depth,
-            tab_size,
-        }
-    }
-
     pub fn indent_level(&self) -> u32 {
         self.depth * self.tab_size
     }
@@ -3151,9 +3136,15 @@ impl BufferSnapshot {
     pub fn indent_guides_in_range(
         &self,
         range: Range<Anchor>,
+        ignore_disabled_for_language: bool,
         cx: &AppContext,
     ) -> Vec<IndentGuide> {
-        let tab_size = language_settings(self.language(), None, cx).tab_size.get() as u32;
+        let language_settings = language_settings(self.language(), self.file.as_ref(), cx);
+        let settings = language_settings.indent_guides;
+        if !ignore_disabled_for_language && !settings.enabled {
+            return Vec::new();
+        }
+        let tab_size = language_settings.tab_size.get() as u32;
 
         let start_row = range.start.to_point(self).row;
         let end_row = range.end.to_point(self).row;
@@ -3234,6 +3225,7 @@ impl BufferSnapshot {
                         end_row: last_row,
                         depth: next_depth,
                         tab_size,
+                        settings,
                     });
                 }
             }

crates/multi_buffer/src/multi_buffer.rs 🔗

@@ -3289,12 +3289,17 @@ impl MultiBufferSnapshot {
     pub fn indent_guides_in_range(
         &self,
         range: Range<Anchor>,
+        ignore_disabled_for_language: bool,
         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)
+                .indent_guides_in_range(
+                    range.start.text_anchor..range.end.text_anchor,
+                    ignore_disabled_for_language,
+                    cx,
+                )
                 .into_iter()
                 .map(|guide| MultiBufferIndentGuide {
                     multibuffer_row_range: MultiBufferRow(guide.start_row)
@@ -3314,7 +3319,11 @@ impl MultiBufferSnapshot {
 
                 excerpt
                     .buffer
-                    .indent_guides_in_range(excerpt.range.context.clone(), cx)
+                    .indent_guides_in_range(
+                        excerpt.range.context.clone(),
+                        ignore_disabled_for_language,
+                        cx,
+                    )
                     .into_iter()
                     .map(move |indent_guide| {
                         let start_row = excerpt_offset_row