From a82f4857f482f6ddf02659992988c0bbcbafd313 Mon Sep 17 00:00:00 2001 From: Felipe Date: Fri, 23 Feb 2024 00:37:13 -0300 Subject: [PATCH] Add settings to control gutter elements (#7665) The current gutter was a bit too big for my taste, so I added some settings to change which visual elements are shown, including being able to hide the gutter completely. This should help with the following issues: #4963, #4382, #7422 New settings: ```json5 "gutter": { "line_numbers": true, // Whether line numbers are shown "buttons": true // Whether the code action/folding buttons are shown } ``` The existing `git.git_gutter` setting is also taken into account when calculating the width of the gutter. We could also separate the display of the code action and folding buttons into separate settings, let me know if that is desirable. ## Screenshots: - Everything on (`gutter.line_numbers`, `gutter.buttons`, `git.git_gutter`): SCR-20240210-trfb - Only line numbers and git gutter (`gutter.line_numbers`, `git.git_gutter`): SCR-20240210-trhm - Only git gutter (`git.git_gutter`): SCR-20240210-trkb - Only git gutter and buttons (`git.git_gutter`, `gutter.buttons`): SCR-20240210-txyo - Nothing: SCR-20240210-trne ## Release Notes: - Added settings to control the display of gutter visual elements. `"gutter": {"line_numbers": true, "code_actions": true, "folds": true}` ([#8041](https://github.com/zed-industries/zed/issues/8041)) ([#7422](https://github.com/zed-industries/zed/issues/7422)) ``` --------- Co-authored-by: Conrad Irwin --- assets/settings/default.json | 8 ++ crates/assistant/src/assistant_panel.rs | 2 +- crates/diagnostics/src/diagnostics.rs | 5 +- crates/editor/src/display_map/block_map.rs | 5 +- crates/editor/src/editor.rs | 76 ++++++++---- crates/editor/src/editor_settings.rs | 27 +++++ crates/editor/src/element.rs | 133 ++++++++++++--------- 7 files changed, 175 insertions(+), 81 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index f8c5e4b15e..ea2e6ff99e 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -140,6 +140,14 @@ // Whether to show diagnostic indicators in the scrollbar. "diagnostics": true }, + "gutter": { + // Whether to show line numbers in the gutter. + "line_numbers": true, + // Whether to show code action buttons in the gutter. + "code_actions": true, + // Whether to show fold buttons in the gutter. + "folds": true + }, // The number of lines to keep above/below the cursor when scrolling. "vertical_scroll_margin": 3, "relative_line_numbers": false, diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index 4e861c9d3e..2ce3e4e551 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -365,7 +365,7 @@ impl AssistantPanel { move |cx: &mut BlockContext| { measurements.set(BlockMeasurements { anchor_x: cx.anchor_x, - gutter_width: cx.gutter_width, + gutter_width: cx.gutter_dimensions.width, }); inline_assistant.clone().into_any_element() } diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index e737b69717..98004360ee 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -885,7 +885,7 @@ mod tests { use super::*; use editor::{ display_map::{BlockContext, TransformBlock}, - DisplayPoint, + DisplayPoint, GutterDimensions, }; use gpui::{px, TestAppContext, VisualTestContext, WindowContext}; use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16, Unclipped}; @@ -1599,8 +1599,7 @@ mod tests { .render(&mut BlockContext { context: cx, anchor_x: px(0.), - gutter_padding: px(0.), - gutter_width: px(0.), + gutter_dimensions: &GutterDimensions::default(), line_height: px(0.), em_width: px(0.), max_width: px(0.), diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index a17dc4a6c6..3c2dc598a2 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -2,7 +2,7 @@ use super::{ wrap_map::{self, WrapEdit, WrapPoint, WrapSnapshot}, Highlights, }; -use crate::{Anchor, Editor, EditorStyle, ExcerptId, ExcerptRange, ToPoint as _}; +use crate::{Anchor, Editor, EditorStyle, ExcerptId, ExcerptRange, GutterDimensions, ToPoint as _}; use collections::{Bound, HashMap, HashSet}; use gpui::{AnyElement, ElementContext, Pixels, View}; use language::{BufferSnapshot, Chunk, Patch, Point}; @@ -88,8 +88,7 @@ pub struct BlockContext<'a, 'b> { pub view: View, pub anchor_x: Pixels, pub max_width: Pixels, - pub gutter_width: Pixels, - pub gutter_padding: Pixels, + pub gutter_dimensions: &'b GutterDimensions, pub em_width: Pixels, pub line_height: Pixels, pub block_id: usize, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 206bcc60a1..c1b01c195a 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -88,6 +88,7 @@ pub use multi_buffer::{ }; use ordered_float::OrderedFloat; use parking_lot::{Mutex, RwLock}; +use project::project_settings::{GitGutterSetting, ProjectSettings}; use project::{FormatTrigger, Location, Project, ProjectPath, ProjectTransaction}; use rand::prelude::*; use rpc::proto::*; @@ -443,7 +444,8 @@ pub struct EditorSnapshot { } pub struct GutterDimensions { - pub padding: Pixels, + pub left_padding: Pixels, + pub right_padding: Pixels, pub width: Pixels, pub margin: Pixels, } @@ -451,7 +453,8 @@ pub struct GutterDimensions { impl Default for GutterDimensions { fn default() -> Self { Self { - padding: Pixels::ZERO, + left_padding: Pixels::ZERO, + right_padding: Pixels::ZERO, width: Pixels::ZERO, margin: Pixels::ZERO, } @@ -4058,7 +4061,8 @@ impl Editor { if self.available_code_actions.is_some() { Some( IconButton::new("code_actions_indicator", ui::IconName::Bolt) - .icon_size(IconSize::Small) + .icon_size(IconSize::XSmall) + .size(ui::ButtonSize::None) .icon_color(Color::Muted) .selected(is_active) .on_click(cx.listener(|editor, _e, cx| { @@ -9636,23 +9640,50 @@ impl EditorSnapshot { max_line_number_width: Pixels, cx: &AppContext, ) -> GutterDimensions { - if self.show_gutter { - let descent = cx.text_system().descent(font_id, font_size); - let gutter_padding_factor = 4.0; - let gutter_padding = (em_width * gutter_padding_factor).round(); + if !self.show_gutter { + return GutterDimensions::default(); + } + let descent = cx.text_system().descent(font_id, font_size); + + let show_git_gutter = matches!( + ProjectSettings::get_global(cx).git.git_gutter, + Some(GitGutterSetting::TrackedFiles) + ); + let gutter_settings = EditorSettings::get_global(cx).gutter; + + let line_gutter_width = if gutter_settings.line_numbers { // Avoid flicker-like gutter resizes when the line number gains another digit and only resize the gutter on files with N*10^5 lines. let min_width_for_number_on_gutter = em_width * 4.0; - let gutter_width = - max_line_number_width.max(min_width_for_number_on_gutter) + gutter_padding * 2.0; - let gutter_margin = -descent; - - GutterDimensions { - padding: gutter_padding, - width: gutter_width, - margin: gutter_margin, - } + max_line_number_width.max(min_width_for_number_on_gutter) } else { - GutterDimensions::default() + 0.0.into() + }; + + let left_padding = if gutter_settings.code_actions { + em_width * 3.0 + } else if show_git_gutter && gutter_settings.line_numbers { + em_width * 2.0 + } else if show_git_gutter || gutter_settings.line_numbers { + em_width + } else { + px(0.) + }; + + let right_padding = if gutter_settings.folds && gutter_settings.line_numbers { + em_width * 4.0 + } else if gutter_settings.folds { + em_width * 3.0 + } else if gutter_settings.line_numbers { + em_width + } else { + px(0.) + }; + + GutterDimensions { + left_padding, + right_padding, + width: line_gutter_width + left_padding + right_padding, + margin: -descent, } } } @@ -10159,9 +10190,14 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, _is_valid: bool) -> Ren .group(group_id.clone()) .relative() .size_full() - .pl(cx.gutter_width) - .w(cx.max_width + cx.gutter_width) - .child(div().flex().w(cx.anchor_x - cx.gutter_width).flex_shrink()) + .pl(cx.gutter_dimensions.width) + .w(cx.max_width + cx.gutter_dimensions.width) + .child( + div() + .flex() + .w(cx.anchor_x - cx.gutter_dimensions.width) + .flex_shrink(), + ) .child(div().flex().flex_shrink_0().child( StyledText::new(text_without_backticks.clone()).with_highlights( &text_style, diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index 074003492f..cff68e689e 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -12,6 +12,7 @@ pub struct EditorSettings { pub use_on_type_format: bool, pub toolbar: Toolbar, pub scrollbar: Scrollbar, + pub gutter: Gutter, pub vertical_scroll_margin: f32, pub relative_line_numbers: bool, pub seed_search_query_from_cursor: SeedQuerySetting, @@ -45,6 +46,13 @@ pub struct Scrollbar { pub diagnostics: bool, } +#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +pub struct Gutter { + pub line_numbers: bool, + pub code_actions: bool, + pub folds: bool, +} + /// When to show the scrollbar in the editor. /// /// Default: auto @@ -97,6 +105,8 @@ pub struct EditorSettingsContent { pub toolbar: Option, /// Scrollbar related settings pub scrollbar: Option, + /// Gutter related settings + pub gutter: Option, /// The number of lines to keep above/below the cursor when auto-scrolling. /// @@ -157,6 +167,23 @@ pub struct ScrollbarContent { pub diagnostics: Option, } +/// Gutter related settings +#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +pub struct GutterContent { + /// Whether to show line numbers in the gutter. + /// + /// Default: true + pub line_numbers: Option, + /// Whether to show code action buttons in the gutter. + /// + /// Default: true + pub code_actions: Option, + /// Whether to show fold buttons in the gutter. + /// + /// Default: true + pub folds: Option, +} + impl Settings for EditorSettings { const KEY: Option<&'static str> = None; diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 6c5635400b..c965d3abdb 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -12,9 +12,9 @@ use crate::{ mouse_context_menu, scroll::scroll_amount::ScrollAmount, CursorShape, DisplayPoint, DocumentHighlightRead, DocumentHighlightWrite, Editor, EditorMode, - EditorSettings, EditorSnapshot, EditorStyle, HalfPageDown, HalfPageUp, HoveredCursor, LineDown, - LineUp, OpenExcerpts, PageDown, PageUp, Point, SelectPhase, Selection, SoftWrap, ToPoint, - CURSORS_VISIBLE_FOR, MAX_LINE_LEN, + EditorSettings, EditorSnapshot, EditorStyle, GutterDimensions, HalfPageDown, HalfPageUp, + HoveredCursor, LineDown, LineUp, OpenExcerpts, PageDown, PageUp, Point, SelectPhase, Selection, + SoftWrap, ToPoint, CURSORS_VISIBLE_FOR, MAX_LINE_LEN, }; use anyhow::Result; use collections::{BTreeMap, HashMap}; @@ -716,20 +716,22 @@ impl EditorElement { let scroll_position = layout.position_map.snapshot.scroll_position(); let scroll_top = scroll_position.y * line_height; - let show_gutter = matches!( + let show_git_gutter = matches!( ProjectSettings::get_global(cx).git.git_gutter, Some(GitGutterSetting::TrackedFiles) ); - if show_gutter { + if show_git_gutter { Self::paint_diff_hunks(bounds, layout, cx); } + let gutter_settings = EditorSettings::get_global(cx).gutter; + for (ix, line) in layout.line_numbers.iter().enumerate() { if let Some(line) = line { let line_origin = bounds.origin + point( - bounds.size.width - line.width - layout.gutter_padding, + bounds.size.width - line.width - layout.gutter_dimensions.right_padding, ix as f32 * line_height - (scroll_top % line_height), ); @@ -740,6 +742,7 @@ impl EditorElement { cx.with_z_index(1, |cx| { for (ix, fold_indicator) in layout.fold_indicators.drain(..).enumerate() { if let Some(fold_indicator) = fold_indicator { + debug_assert!(gutter_settings.folds); let mut fold_indicator = fold_indicator.into_any_element(); let available_space = size( AvailableSpace::MinContent, @@ -748,11 +751,12 @@ impl EditorElement { let fold_indicator_size = fold_indicator.measure(available_space, cx); let position = point( - bounds.size.width - layout.gutter_padding, + bounds.size.width - layout.gutter_dimensions.right_padding, ix as f32 * line_height - (scroll_top % line_height), ); let centering_offset = point( - (layout.gutter_padding + layout.gutter_margin - fold_indicator_size.width) + (layout.gutter_dimensions.right_padding + layout.gutter_dimensions.margin + - fold_indicator_size.width) / 2., (line_height - fold_indicator_size.height) / 2., ); @@ -762,6 +766,7 @@ impl EditorElement { } if let Some(indicator) = layout.code_actions_indicator.take() { + debug_assert!(gutter_settings.code_actions); let mut button = indicator.button.into_any_element(); let available_space = size( AvailableSpace::MinContent, @@ -772,7 +777,9 @@ impl EditorElement { let mut x = Pixels::ZERO; let mut y = indicator.row as f32 * line_height - scroll_top; // Center indicator. - x += ((layout.gutter_padding + layout.gutter_margin) - indicator_size.width) / 2.; + x += (layout.gutter_dimensions.margin + layout.gutter_dimensions.left_padding + - indicator_size.width) + / 2.; y += (line_height - indicator_size.height) / 2.; button.draw(bounds.origin + point(x, y), available_space, cx); @@ -887,7 +894,8 @@ impl EditorElement { cx: &mut ElementContext, ) { let start_row = layout.visible_display_row_range.start; - let content_origin = text_bounds.origin + point(layout.gutter_margin, Pixels::ZERO); + let content_origin = + text_bounds.origin + point(layout.gutter_dimensions.margin, Pixels::ZERO); let line_end_overshoot = 0.15 * layout.position_map.line_height; let whitespace_setting = self .editor @@ -1156,7 +1164,8 @@ impl EditorElement { layout: &LayoutState, cx: &mut ElementContext, ) { - let content_origin = text_bounds.origin + point(layout.gutter_margin, Pixels::ZERO); + let content_origin = + text_bounds.origin + point(layout.gutter_dimensions.margin, Pixels::ZERO); let line_end_overshoot = layout.line_end_overshoot(); // A softer than perfect black @@ -1182,7 +1191,8 @@ impl EditorElement { layout: &mut LayoutState, cx: &mut ElementContext, ) { - let content_origin = text_bounds.origin + point(layout.gutter_margin, Pixels::ZERO); + let content_origin = + text_bounds.origin + point(layout.gutter_dimensions.margin, Pixels::ZERO); let start_row = layout.visible_display_row_range.start; if let Some((position, mut context_menu)) = layout.context_menu.take() { let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent); @@ -1819,7 +1829,10 @@ impl EditorElement { Vec>, ) { let font_size = self.style.text.font_size.to_pixels(cx.rem_size()); - let include_line_numbers = snapshot.mode == EditorMode::Full; + let include_line_numbers = + EditorSettings::get_global(cx).gutter.line_numbers && snapshot.mode == EditorMode::Full; + let include_fold_statuses = + EditorSettings::get_global(cx).gutter.folds && snapshot.mode == EditorMode::Full; let mut shaped_line_numbers = Vec::with_capacity(rows.len()); let mut fold_statuses = Vec::with_capacity(rows.len()); let mut line_number = String::new(); @@ -1864,6 +1877,8 @@ impl EditorElement { .shape_line(line_number.clone().into(), font_size, &[run]) .unwrap(); shaped_line_numbers.push(Some(shaped_line)); + } + if include_fold_statuses { fold_statuses.push( is_singleton .then(|| { @@ -1960,7 +1975,13 @@ impl EditorElement { .unwrap() .width; - let gutter_dimensions = snapshot.gutter_dimensions(font_id, font_size, em_width, self.max_line_number_width(&snapshot, cx), cx); + let gutter_dimensions = snapshot.gutter_dimensions( + font_id, + font_size, + em_width, + self.max_line_number_width(&snapshot, cx), + cx, + ); editor.gutter_width = gutter_dimensions.width; @@ -2213,8 +2234,7 @@ impl EditorElement { bounds.size.width, scroll_width, text_width, - gutter_dimensions.padding, - gutter_dimensions.width, + &gutter_dimensions, em_width, gutter_dimensions.width + gutter_dimensions.margin, line_height, @@ -2251,6 +2271,8 @@ impl EditorElement { snapshot = editor.snapshot(cx); } + let gutter_settings = EditorSettings::get_global(cx).gutter; + let mut context_menu = None; let mut code_actions_indicator = None; if let Some(newest_selection_head) = newest_selection_head { @@ -2272,12 +2294,14 @@ impl EditorElement { Some(crate::ContextMenu::CodeActions(_)) ); - code_actions_indicator = editor - .render_code_actions_indicator(&style, active, cx) - .map(|element| CodeActionsIndicator { - row: newest_selection_head.row(), - button: element, - }); + if gutter_settings.code_actions { + code_actions_indicator = editor + .render_code_actions_indicator(&style, active, cx) + .map(|element| CodeActionsIndicator { + row: newest_selection_head.row(), + button: element, + }); + } } } @@ -2295,29 +2319,32 @@ impl EditorElement { None } else { editor.hover_state.render( - &snapshot, - &style, - visible_rows, - max_size, - editor.workspace.as_ref().map(|(w, _)| w.clone()), - cx, - ) + &snapshot, + &style, + visible_rows, + max_size, + editor.workspace.as_ref().map(|(w, _)| w.clone()), + cx, + ) }; let editor_view = cx.view().clone(); - let fold_indicators = cx.with_element_context(|cx| { - - cx.with_element_id(Some("gutter_fold_indicators"), |_cx| { - editor.render_fold_indicators( - fold_statuses, - &style, - editor.gutter_hovered, - line_height, - gutter_dimensions.margin, - editor_view, - ) - }) - }); + let fold_indicators = if gutter_settings.folds { + cx.with_element_context(|cx| { + cx.with_element_id(Some("gutter_fold_indicators"), |_cx| { + editor.render_fold_indicators( + fold_statuses, + &style, + editor.gutter_hovered, + line_height, + gutter_dimensions.margin, + editor_view, + ) + }) + }) + } else { + Vec::new() + }; let invisible_symbol_font_size = font_size / 2.; let tab_invisible = cx @@ -2370,13 +2397,12 @@ impl EditorElement { visible_display_row_range: start_row..end_row, wrap_guides, gutter_size, - gutter_padding: gutter_dimensions.padding, + gutter_dimensions, text_size, scrollbar_row_range, show_scrollbars, is_singleton, max_row, - gutter_margin: gutter_dimensions.margin, active_rows, highlighted_rows, highlighted_ranges, @@ -2403,8 +2429,7 @@ impl EditorElement { editor_width: Pixels, scroll_width: Pixels, text_width: Pixels, - gutter_padding: Pixels, - gutter_width: Pixels, + gutter_dimensions: &GutterDimensions, em_width: Pixels, text_x: Pixels, line_height: Pixels, @@ -2447,9 +2472,8 @@ impl EditorElement { block.render(&mut BlockContext { context: cx, anchor_x, - gutter_padding, + gutter_dimensions, line_height, - gutter_width, em_width, block_id, max_width: scroll_width.max(text_width), @@ -2553,12 +2577,14 @@ impl EditorElement { h_flex() .id(("collapsed context", block_id)) .size_full() - .gap(gutter_padding) + .gap(gutter_dimensions.left_padding + gutter_dimensions.right_padding) .child( h_flex() .justify_end() .flex_none() - .w(gutter_width - gutter_padding) + .w(gutter_dimensions.width + - (gutter_dimensions.left_padding + + gutter_dimensions.right_padding)) .h_full() .text_buffer(cx) .text_color(cx.theme().colors().editor_line_number) @@ -2619,7 +2645,7 @@ impl EditorElement { BlockStyle::Sticky => editor_width, BlockStyle::Flex => editor_width .max(fixed_block_max_width) - .max(gutter_width + scroll_width), + .max(gutter_dimensions.width + scroll_width), BlockStyle::Fixed => unreachable!(), }; let available_space = size( @@ -2636,7 +2662,7 @@ impl EditorElement { }); } ( - scroll_width.max(fixed_block_max_width - gutter_width), + scroll_width.max(fixed_block_max_width - gutter_dimensions.width), blocks, ) } @@ -3153,8 +3179,7 @@ type BufferRow = u32; pub struct LayoutState { position_map: Arc, gutter_size: Size, - gutter_padding: Pixels, - gutter_margin: Pixels, + gutter_dimensions: GutterDimensions, text_size: gpui::Size, mode: EditorMode, wrap_guides: SmallVec<[(Pixels, bool); 2]>,