git: Add hunk_style setting (#26038)

This PR adds the `git.hunk_style` setting, allowing setting an alternate
style for hunks – specifically the rendering of unstaged hunks.

It has 2 options:

- `transparent` (unstaged hunks are more transparent/less opaque than
staged hunks)
- `pattern (unstaged hunks are indicated by a visual pattern)

We'll possibly explore a VSCode-style "don't show staged hunks", but the
complexity it adds is a bit out of scope for now.

Transparent:

![CleanShot 2025-03-04 at 09 07
09@2x](https://github.com/user-attachments/assets/a74c4286-8264-48a2-bd58-0c582efb4e22)

Pattern:

![CleanShot 2025-03-04 at 09 10
12@2x](https://github.com/user-attachments/assets/4dd3040e-fb36-4670-9279-fcc7a4f12ced)

Release Notes:

- Git Beta: Added `git.hunk_style` setting to allow toggling between git
hunk visual styles.
This commit is contained in:
Nate Butler 2025-03-04 11:10:39 -05:00 committed by GitHub
parent 0ec15d6b02
commit 6cdd7b7390
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 81 additions and 29 deletions

View file

@ -837,7 +837,15 @@
// //
// The minimum column number to show the inline blame information at // The minimum column number to show the inline blame information at
// "min_column": 0 // "min_column": 0
} },
// How git hunks are displayed visually in the editor.
// This setting can take two values:
//
// 1. Show unstaged hunks with a transparent background (default):
// "hunk_style": "transparent"
// 2. Show unstaged hunks with a pattern background:
// "hunk_style": "pattern"
"hunk_style": "transparent"
}, },
// Configuration for how direnv configuration should be loaded. May take 2 values: // Configuration for how direnv configuration should be loaded. May take 2 values:
// 1. Load direnv configuration using `direnv export json` directly. // 1. Load direnv configuration using `direnv export json` directly.
@ -851,15 +859,7 @@
// Any addition to this list will be merged with the default list. // Any addition to this list will be merged with the default list.
// Globs are matched relative to the worktree root, // Globs are matched relative to the worktree root,
// except when starting with a slash (/) or equivalent in Windows. // except when starting with a slash (/) or equivalent in Windows.
"disabled_globs": [ "disabled_globs": ["**/.env*", "**/*.pem", "**/*.key", "**/*.cert", "**/*.crt", "**/.dev.vars", "**/secrets.yml"],
"**/.env*",
"**/*.pem",
"**/*.key",
"**/*.cert",
"**/*.crt",
"**/.dev.vars",
"**/secrets.yml"
],
// When to show edit predictions previews in buffer. // When to show edit predictions previews in buffer.
// This setting takes two possible values: // This setting takes two possible values:
// 1. Display predictions inline when there are no language server completions available. // 1. Display predictions inline when there are no language server completions available.

View file

@ -32,14 +32,15 @@ use collections::{BTreeMap, HashMap, HashSet};
use file_icons::FileIcons; use file_icons::FileIcons;
use git::{blame::BlameEntry, status::FileStatus, Oid}; use git::{blame::BlameEntry, status::FileStatus, Oid};
use gpui::{ use gpui::{
anchored, deferred, div, fill, linear_color_stop, linear_gradient, outline, point, px, quad, anchored, deferred, div, fill, linear_color_stop, linear_gradient, outline, pattern_slash,
relative, size, svg, transparent_black, Action, AnyElement, App, AvailableSpace, Axis, Bounds, point, px, quad, relative, size, solid_background, svg, transparent_black, Action, AnyElement,
ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners, CursorStyle, DispatchPhase, App, AvailableSpace, Axis, Bounds, ClickEvent, ClipboardItem, ContentMask, Context, Corner,
Edges, Element, ElementInputHandler, Entity, Focusable as _, FontId, GlobalElementId, Hitbox, Corners, CursorStyle, DispatchPhase, Edges, Element, ElementInputHandler, Entity,
Hsla, InteractiveElement, IntoElement, Keystroke, Length, ModifiersChangedEvent, MouseButton, Focusable as _, FontId, GlobalElementId, Hitbox, Hsla, InteractiveElement, IntoElement,
MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta, Keystroke, Length, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent,
ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, ShapedLine,
Subscription, TextRun, TextStyleRefinement, Window, SharedString, Size, StatefulInteractiveElement, Style, Styled, Subscription, TextRun,
TextStyleRefinement, Window,
}; };
use itertools::Itertools; use itertools::Itertools;
use language::{ use language::{
@ -54,7 +55,7 @@ use multi_buffer::{
Anchor, ExcerptId, ExcerptInfo, ExpandExcerptDirection, MultiBufferPoint, MultiBufferRow, Anchor, ExcerptId, ExcerptInfo, ExpandExcerptDirection, MultiBufferPoint, MultiBufferRow,
RowInfo, RowInfo,
}; };
use project::project_settings::{self, GitGutterSetting, ProjectSettings}; use project::project_settings::{self, GitGutterSetting, GitHunkStyleSetting, ProjectSettings};
use settings::Settings; use settings::Settings;
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use std::{ use std::{
@ -4346,7 +4347,7 @@ impl EditorElement {
} }
} }
fn paint_diff_hunks(layout: &mut EditorLayout, window: &mut Window, cx: &mut App) { fn paint_gutter_diff_hunks(layout: &mut EditorLayout, window: &mut Window, cx: &mut App) {
let is_light = cx.theme().appearance().is_light(); let is_light = cx.theme().appearance().is_light();
if layout.display_hunks.is_empty() { if layout.display_hunks.is_empty() {
@ -4416,10 +4417,19 @@ impl EditorElement {
background_color = background_color =
background_color.opacity(if is_light { 0.2 } else { 0.32 }); background_color.opacity(if is_light { 0.2 } else { 0.32 });
} }
// Flatten the background color with the editor color to prevent
// elements below transparent hunks from showing through
let flattened_background_color = cx
.theme()
.colors()
.editor_background
.blend(background_color);
window.paint_quad(quad( window.paint_quad(quad(
hunk_bounds, hunk_bounds,
corner_radii, corner_radii,
background_color, flattened_background_color,
Edges::default(), Edges::default(),
transparent_black(), transparent_black(),
)); ));
@ -4547,7 +4557,7 @@ impl EditorElement {
) )
}); });
if show_git_gutter { if show_git_gutter {
Self::paint_diff_hunks(layout, window, cx) Self::paint_gutter_diff_hunks(layout, window, cx)
} }
let highlight_width = 0.275 * layout.position_map.line_height; let highlight_width = 0.275 * layout.position_map.line_height;
@ -6711,15 +6721,16 @@ impl Element for EditorElement {
.update(cx, |editor, cx| editor.highlighted_display_rows(window, cx)); .update(cx, |editor, cx| editor.highlighted_display_rows(window, cx));
let is_light = cx.theme().appearance().is_light(); let is_light = cx.theme().appearance().is_light();
let use_pattern = ProjectSettings::get_global(cx)
.git
.hunk_style
.map_or(false, |style| matches!(style, GitHunkStyleSetting::Pattern));
for (ix, row_info) in row_infos.iter().enumerate() { for (ix, row_info) in row_infos.iter().enumerate() {
let Some(diff_status) = row_info.diff_status else { let Some(diff_status) = row_info.diff_status else {
continue; continue;
}; };
let staged_opacity = if is_light { 0.14 } else { 0.10 };
let unstaged_opacity = 0.04;
let background_color = match diff_status.kind { let background_color = match diff_status.kind {
DiffHunkStatusKind::Added => cx.theme().colors().version_control_added, DiffHunkStatusKind::Added => cx.theme().colors().version_control_added,
DiffHunkStatusKind::Deleted => { DiffHunkStatusKind::Deleted => {
@ -6730,15 +6741,34 @@ impl Element for EditorElement {
continue; continue;
} }
}; };
let background_color = if diff_status.has_secondary_hunk() {
background_color.opacity(unstaged_opacity) let unstaged = diff_status.has_secondary_hunk();
let hunk_opacity = if is_light { 0.16 } else { 0.12 };
let staged_background =
solid_background(background_color.opacity(hunk_opacity));
let unstaged_background = if use_pattern {
pattern_slash(
background_color.opacity(hunk_opacity),
window.rem_size().0 * 1.125, // ~18 by default
)
} else { } else {
background_color.opacity(staged_opacity) solid_background(background_color.opacity(if is_light {
0.08
} else {
0.04
}))
};
let background = if unstaged {
unstaged_background
} else {
staged_background
}; };
highlighted_rows highlighted_rows
.entry(start_row + DisplayRow(ix as u32)) .entry(start_row + DisplayRow(ix as u32))
.or_insert(background_color.into()); .or_insert(background);
} }
let highlighted_ranges = self.editor.read(cx).background_highlights_in_range( let highlighted_ranges = self.editor.read(cx).background_highlights_in_range(

View file

@ -670,6 +670,14 @@ pub fn pattern_slash(color: Hsla, thickness: f32) -> Background {
} }
} }
/// Creates a solid background color.
pub fn solid_background(color: impl Into<Hsla>) -> Background {
Background {
solid: color.into(),
..Default::default()
}
}
/// Creates a LinearGradient background color. /// Creates a LinearGradient background color.
/// ///
/// The gradient line's angle of direction. A value of `0.` is equivalent to to top; increasing values rotate clockwise from there. /// The gradient line's angle of direction. A value of `0.` is equivalent to to top; increasing values rotate clockwise from there.

View file

@ -168,6 +168,10 @@ pub struct GitSettings {
/// ///
/// Default: on /// Default: on
pub inline_blame: Option<InlineBlameSettings>, pub inline_blame: Option<InlineBlameSettings>,
/// How hunks are displayed visually in the editor.
///
/// Default: transparent
pub hunk_style: Option<GitHunkStyleSetting>,
} }
impl GitSettings { impl GitSettings {
@ -200,6 +204,16 @@ impl GitSettings {
} }
} }
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum GitHunkStyleSetting {
/// Show unstaged hunks with a transparent background
#[default]
Transparent,
/// Show unstaged hunks with a pattern background
Pattern,
}
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)] #[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum GitGutterSetting { pub enum GitGutterSetting {