mirror of
https://github.com/zed-industries/zed.git
synced 2026-07-09 16:00:35 +00:00
git: Allow showing inline blame in the status bar (#59696)
# Add status bar location for inline git blame ## Objective - Closes #12133 by implementing the VS Code-like behavior for status bar git blame ## Solution Adds a VS Code–style option to render the current-line git blame in the status bar instead of inline at the cursor. Whats new - A new location setting: Under the inline_blame configuration, you can now choose between two options: - `inline` (Default) — Keeps things exactly as they are, showing the blame details at the end of your current line. - `status_bar` — Moves the blame info down to the bottom status bar. - The Status Bar Item: When you choose the status bar option, a clean new button appears at the bottom left of your window. It features a Git icon, the author's name, how long ago the change was made, and the commit summary - Behaves as it does before, but displayed at a different location - Clicking the status bar item will instantly open up the full Git commit details like at vscode - Settings UI: - The "Location" control is now a `DynamicItem` nested under "Enabled", so it only appears when inline blame is enabled. - Registers a dropdown renderer for `InlineBlameLocation`. - Updates `all-settings.md` and `visual-customization.md` for the new option. ## Testing - Manually tested both modes by toggling `git.inline_blame.location` between `inline` and `status_bar`: - `inline` keeps the existing behavior unchanged. - `status_bar` shows the blame for the focused line in the status bar, hides the inline blame, updates on cursor movement, clears when no editor is focused, and opens the blame commit on click. - Verified the settings UI shows the "Location" dropdown only when inline blame is enabled. - Tested with `show_commit_summary` toggled when location is `status_bar` (for inline location, nothing is changed) Areas that could use more testing: - Remote (collab) editors ? Reviewers can test by setting the following in `settings.json` and moving the cursor across blamed lines: ```json { "git": { "inline_blame": { "enabled": true, "location": "status_bar" } } } ``` ## Showcase <details> <summary>Click to view showcase</summary> ### Video https://github.com/user-attachments/assets/6d9f490d-7bf3-473c-9562-8351546dfaf9 ### Screenshots <img width="894" height="749" alt="image" src="https://github.com/user-attachments/assets/c6a49008-899d-4a0c-9098-1ffb9e28252b" /> </details> Release Notes: - Added a `git.inline_blame.location` setting to render current-line git blame in the status bar instead of inline.
This commit is contained in:
parent
cd7f1a0fb1
commit
f2006b20ac
11 changed files with 274 additions and 43 deletions
|
|
@ -1643,13 +1643,15 @@
|
|||
///
|
||||
/// Default: 0
|
||||
"gutter_debounce": 0,
|
||||
// Control whether the git blame information is shown inline,
|
||||
// in the currently focused line.
|
||||
// Control whether the git blame information is shown for the currently
|
||||
// focused line, and where it is rendered.
|
||||
"inline_blame": {
|
||||
"enabled": true,
|
||||
// Sets a delay after which the inline blame information is shown.
|
||||
// Delay is restarted with every cursor movement.
|
||||
"delay_ms": 0,
|
||||
// Where to render the blame information when it is enabled.
|
||||
"location": "inline",
|
||||
// The amount of padding between the end of the source line and the start
|
||||
// of the inline blame in units of em widths.
|
||||
"padding": 7,
|
||||
|
|
|
|||
|
|
@ -195,6 +195,36 @@ impl Editor {
|
|||
self.blame.as_ref()
|
||||
}
|
||||
|
||||
pub fn active_git_blame_entry(&self, cx: &mut App) -> Option<BlameEntry> {
|
||||
if !self.show_git_blame_inline
|
||||
|| self.newest_selection_head_on_empty_line(cx)
|
||||
|| !self.has_blame_entries(cx)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let blame = self.blame.as_ref()?;
|
||||
let snapshot = self.display_snapshot(cx);
|
||||
let cursor = self.selections.newest::<Point>(&snapshot).head();
|
||||
let (buffer, point) = snapshot.buffer_snapshot().point_to_buffer_point(cursor)?;
|
||||
|
||||
blame
|
||||
.update(cx, |blame, cx| {
|
||||
blame
|
||||
.blame_for_rows(
|
||||
&[RowInfo {
|
||||
buffer_id: Some(buffer.remote_id()),
|
||||
buffer_row: Some(point.row),
|
||||
..Default::default()
|
||||
}],
|
||||
cx,
|
||||
)
|
||||
.next()
|
||||
})
|
||||
.flatten()
|
||||
.map(|(_, entry)| entry)
|
||||
}
|
||||
|
||||
pub fn show_git_blame_gutter(&self) -> bool {
|
||||
self.show_git_blame_gutter
|
||||
}
|
||||
|
|
@ -1603,7 +1633,9 @@ impl Editor {
|
|||
}
|
||||
|
||||
pub(super) fn render_git_blame_inline(&self, window: &Window, cx: &App) -> bool {
|
||||
self.show_git_blame_inline
|
||||
ProjectSettings::get_global(cx).git.inline_blame.location
|
||||
== project::project_settings::InlineBlameLocation::Inline
|
||||
&& self.show_git_blame_inline
|
||||
&& (self.focus_handle.is_focused(window) || self.inline_blame_popover.is_some())
|
||||
&& !self.newest_selection_head_on_empty_line(cx)
|
||||
&& self.has_blame_entries(cx)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,10 @@ use gpui::{
|
|||
TextStyleRefinement, UnderlineStyle, WeakEntity, prelude::*,
|
||||
};
|
||||
use markdown::{Markdown, MarkdownElement};
|
||||
use project::{git_store::Repository, project_settings::ProjectSettings};
|
||||
use project::{
|
||||
git_store::Repository,
|
||||
project_settings::{InlineBlameLocation, ProjectSettings},
|
||||
};
|
||||
use settings::Settings as _;
|
||||
use theme_settings::ThemeSettings;
|
||||
use time::OffsetDateTime;
|
||||
|
|
@ -20,6 +23,104 @@ const GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED: usize = 20;
|
|||
|
||||
pub struct GitBlameRenderer;
|
||||
|
||||
fn format_blame_text(blame_entry: &BlameEntry, cx: &App) -> String {
|
||||
let relative_timestamp = blame_entry_relative_timestamp(blame_entry);
|
||||
let author = blame_entry.author.as_deref().unwrap_or_default();
|
||||
let summary_enabled = ProjectSettings::get_global(cx)
|
||||
.git
|
||||
.inline_blame
|
||||
.show_commit_summary;
|
||||
|
||||
match blame_entry.summary.as_ref() {
|
||||
Some(summary) if summary_enabled => {
|
||||
format!("{author}, {relative_timestamp} - {summary}")
|
||||
}
|
||||
_ => format!("{author}, {relative_timestamp}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct GitBlameStatus {
|
||||
text: Option<SharedString>,
|
||||
active_editor: Option<Entity<Editor>>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl GitBlameStatus {
|
||||
fn update(&mut self, editor: Entity<Editor>, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
let inline_blame = ProjectSettings::get_global(cx).git.inline_blame;
|
||||
let text =
|
||||
if inline_blame.enabled && inline_blame.location == InlineBlameLocation::StatusBar {
|
||||
editor
|
||||
.update(cx, |editor, cx| editor.active_git_blame_entry(cx))
|
||||
.map(|blame_entry| SharedString::from(format_blame_text(&blame_entry, cx)))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if text != self.text {
|
||||
self.text = text;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for GitBlameStatus {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let inline_blame = ProjectSettings::get_global(cx).git.inline_blame;
|
||||
if !inline_blame.enabled || inline_blame.location != InlineBlameLocation::StatusBar {
|
||||
return div();
|
||||
}
|
||||
|
||||
div().when_some(self.text.clone(), |el, text| {
|
||||
el.child(
|
||||
Button::new("git-blame-status", text.clone())
|
||||
.label_size(LabelSize::Small)
|
||||
.start_icon(
|
||||
Icon::new(IconName::FileGit)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Hint),
|
||||
)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
if let Some(editor) = this.active_editor.clone() {
|
||||
let focus_handle = gpui::Focusable::focus_handle(editor.read(cx), cx);
|
||||
focus_handle.dispatch_action(
|
||||
&editor::actions::OpenGitBlameCommit,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}))
|
||||
.tooltip(ui::Tooltip::text(text)),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl workspace::StatusItemView for GitBlameStatus {
|
||||
fn set_active_pane_item(
|
||||
&mut self,
|
||||
active_pane_item: Option<&dyn workspace::item::ItemHandle>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Some(editor) = active_pane_item.and_then(|item| item.act_as::<Editor>(cx)) {
|
||||
self.active_editor = Some(editor.clone());
|
||||
self._subscriptions = vec![cx.observe_in(&editor, window, Self::update)];
|
||||
self.update(editor, window, cx);
|
||||
} else {
|
||||
self.text = None;
|
||||
self.active_editor = None;
|
||||
self._subscriptions.clear();
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn hide_setting(&self, _: &App) -> Option<workspace::HideStatusItem> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl BlameRenderer for GitBlameRenderer {
|
||||
fn max_author_length(&self) -> usize {
|
||||
GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED
|
||||
|
|
@ -144,19 +245,7 @@ impl BlameRenderer for GitBlameRenderer {
|
|||
blame_entry: BlameEntry,
|
||||
cx: &mut App,
|
||||
) -> Option<AnyElement> {
|
||||
let relative_timestamp = blame_entry_relative_timestamp(&blame_entry);
|
||||
let author = blame_entry.author.as_deref().unwrap_or_default();
|
||||
let summary_enabled = ProjectSettings::get_global(cx)
|
||||
.git
|
||||
.inline_blame
|
||||
.show_commit_summary;
|
||||
|
||||
let text = match blame_entry.summary.as_ref() {
|
||||
Some(summary) if summary_enabled => {
|
||||
format!("{}, {} - {}", author, relative_timestamp, summary)
|
||||
}
|
||||
_ => format!("{}, {}", author, relative_timestamp),
|
||||
};
|
||||
let text = format_blame_text(&blame_entry, cx);
|
||||
|
||||
Some(
|
||||
h_flex()
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ pub mod worktree_names;
|
|||
pub mod worktree_picker;
|
||||
pub mod worktree_service;
|
||||
|
||||
pub use blame_ui::GitBlameStatus;
|
||||
pub use conflict_view::MergeConflictIndicator;
|
||||
|
||||
pub fn get_provider_icon(name: &str) -> IconName {
|
||||
|
|
|
|||
|
|
@ -524,6 +524,22 @@ impl From<settings::GitPathStyle> for GitPathStyle {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum InlineBlameLocation {
|
||||
#[default]
|
||||
Inline,
|
||||
StatusBar,
|
||||
}
|
||||
|
||||
impl From<settings::InlineBlameLocation> for InlineBlameLocation {
|
||||
fn from(location: settings::InlineBlameLocation) -> Self {
|
||||
match location {
|
||||
settings::InlineBlameLocation::Inline => InlineBlameLocation::Inline,
|
||||
settings::InlineBlameLocation::StatusBar => InlineBlameLocation::StatusBar,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct InlineBlameSettings {
|
||||
/// Whether or not to show git blame data inline in
|
||||
|
|
@ -536,6 +552,10 @@ pub struct InlineBlameSettings {
|
|||
///
|
||||
/// Default: 0
|
||||
pub delay_ms: settings::DelayMs,
|
||||
/// Where to render the blame information when enabled.
|
||||
///
|
||||
/// Default: inline
|
||||
pub location: InlineBlameLocation,
|
||||
/// The amount of padding between the end of the source line and the start
|
||||
/// of the inline blame in units of columns.
|
||||
///
|
||||
|
|
@ -664,6 +684,7 @@ impl Settings for ProjectSettings {
|
|||
InlineBlameSettings {
|
||||
enabled: inline.enabled.unwrap(),
|
||||
delay_ms: inline.delay_ms.unwrap(),
|
||||
location: inline.location.unwrap().into(),
|
||||
padding: inline.padding.unwrap(),
|
||||
min_column: inline.min_column.unwrap(),
|
||||
show_commit_summary: inline.show_commit_summary.unwrap(),
|
||||
|
|
|
|||
|
|
@ -616,6 +616,28 @@ pub enum GitGutterSetting {
|
|||
Hide,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone,
|
||||
Copy,
|
||||
Debug,
|
||||
PartialEq,
|
||||
Default,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
JsonSchema,
|
||||
MergeFrom,
|
||||
strum::VariantArray,
|
||||
strum::VariantNames,
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum InlineBlameLocation {
|
||||
/// Show git blame inline at the current line.
|
||||
#[default]
|
||||
Inline,
|
||||
/// Show git blame in the status bar at the bottom of the window.
|
||||
StatusBar,
|
||||
}
|
||||
|
||||
#[with_fallible_options]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
|
|
@ -630,6 +652,10 @@ pub struct InlineBlameSettings {
|
|||
///
|
||||
/// Default: 0
|
||||
pub delay_ms: Option<DelayMs>,
|
||||
/// Where to render the blame information when enabled.
|
||||
///
|
||||
/// Default: inline
|
||||
pub location: Option<InlineBlameLocation>,
|
||||
/// The amount of padding between the end of the source line and the start
|
||||
/// of the inline blame in units of columns.
|
||||
///
|
||||
|
|
|
|||
|
|
@ -7518,32 +7518,75 @@ fn version_control_page() -> SettingsPage {
|
|||
fn inline_git_blame_section() -> [SettingsPageItem; 6] {
|
||||
[
|
||||
SettingsPageItem::SectionHeader("Inline Git Blame"),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "Enabled",
|
||||
description: "Whether or not to show Git blame data inline in the currently focused line.",
|
||||
field: Box::new(SettingField {
|
||||
organization_override: None,
|
||||
json_path: Some("git.inline_blame.enabled"),
|
||||
pick: |settings_content| {
|
||||
settings_content
|
||||
SettingsPageItem::DynamicItem(DynamicItem {
|
||||
discriminant: SettingItem {
|
||||
title: "Enabled",
|
||||
description: "Whether or not to show Git blame data for the currently focused line.",
|
||||
field: Box::new(SettingField {
|
||||
organization_override: None,
|
||||
json_path: Some("git.inline_blame.enabled"),
|
||||
pick: |settings_content| {
|
||||
settings_content
|
||||
.git
|
||||
.as_ref()?
|
||||
.inline_blame
|
||||
.as_ref()?
|
||||
.enabled
|
||||
.as_ref()
|
||||
},
|
||||
write: |settings_content, value, _| {
|
||||
settings_content
|
||||
.git
|
||||
.get_or_insert_default()
|
||||
.inline_blame
|
||||
.get_or_insert_default()
|
||||
.enabled = value;
|
||||
},
|
||||
}),
|
||||
metadata: None,
|
||||
files: USER,
|
||||
},
|
||||
pick_discriminant: |settings_content| {
|
||||
Some(
|
||||
*settings_content
|
||||
.git
|
||||
.as_ref()?
|
||||
.inline_blame
|
||||
.as_ref()?
|
||||
.enabled
|
||||
.as_ref()
|
||||
},
|
||||
write: |settings_content, value, _| {
|
||||
settings_content
|
||||
.git
|
||||
.get_or_insert_default()
|
||||
.inline_blame
|
||||
.get_or_insert_default()
|
||||
.enabled = value;
|
||||
},
|
||||
}),
|
||||
metadata: None,
|
||||
files: USER,
|
||||
.as_ref()? as usize,
|
||||
)
|
||||
},
|
||||
fields: vec![
|
||||
vec![],
|
||||
vec![SettingItem {
|
||||
title: "Location",
|
||||
description: "Where to render Git blame when it is enabled.",
|
||||
field: Box::new(SettingField {
|
||||
organization_override: None,
|
||||
json_path: Some("git.inline_blame.location"),
|
||||
pick: |settings_content| {
|
||||
settings_content
|
||||
.git
|
||||
.as_ref()?
|
||||
.inline_blame
|
||||
.as_ref()?
|
||||
.location
|
||||
.as_ref()
|
||||
},
|
||||
write: |settings_content, value, _| {
|
||||
settings_content
|
||||
.git
|
||||
.get_or_insert_default()
|
||||
.inline_blame
|
||||
.get_or_insert_default()
|
||||
.location = value;
|
||||
},
|
||||
}),
|
||||
metadata: None,
|
||||
files: USER,
|
||||
}],
|
||||
],
|
||||
}),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "Delay",
|
||||
|
|
|
|||
|
|
@ -545,6 +545,7 @@ fn init_renderers(cx: &mut App) {
|
|||
.add_basic_renderer::<settings::GitGutterSetting>(render_dropdown)
|
||||
.add_basic_renderer::<settings::GitHunkStyleSetting>(render_dropdown)
|
||||
.add_basic_renderer::<settings::GitPathStyle>(render_dropdown)
|
||||
.add_basic_renderer::<settings::InlineBlameLocation>(render_dropdown)
|
||||
.add_basic_renderer::<settings::DiagnosticSeverityContent>(render_dropdown)
|
||||
.add_basic_renderer::<settings::SeedQuerySetting>(render_dropdown)
|
||||
.add_basic_renderer::<settings::DoubleClickInMultibuffer>(render_dropdown)
|
||||
|
|
|
|||
|
|
@ -599,6 +599,7 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut App) {
|
|||
cx.new(|_| go_to_line::cursor_position::CursorPosition::new(workspace));
|
||||
let line_ending_indicator =
|
||||
cx.new(|_| line_ending_selector::LineEndingIndicator::default());
|
||||
let git_blame_status = cx.new(|_| git_ui::GitBlameStatus::default());
|
||||
let merge_conflict_indicator =
|
||||
cx.new(|cx| git_ui::MergeConflictIndicator::new(workspace, cx));
|
||||
workspace.status_bar().update(cx, |status_bar, cx| {
|
||||
|
|
@ -606,6 +607,7 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut App) {
|
|||
status_bar.add_left_item(lsp_button, window, cx);
|
||||
status_bar.add_left_item(diagnostic_summary, window, cx);
|
||||
status_bar.add_left_item(active_file_name, window, cx);
|
||||
status_bar.add_left_item(git_blame_status, window, cx);
|
||||
status_bar.add_left_item(merge_conflict_indicator, window, cx);
|
||||
status_bar.add_left_item(activity_indicator, window, cx);
|
||||
status_bar.add_right_item(edit_prediction_ui, window, cx);
|
||||
|
|
|
|||
|
|
@ -2325,7 +2325,8 @@ Example:
|
|||
{
|
||||
"git": {
|
||||
"inline_blame": {
|
||||
"enabled": true
|
||||
"enabled": true,
|
||||
"location": "inline"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2357,7 +2358,19 @@ Example:
|
|||
}
|
||||
```
|
||||
|
||||
3. Show a commit summary next to the commit date and author:
|
||||
3. Show git blame in the status bar at the bottom of the window:
|
||||
|
||||
```json [settings]
|
||||
{
|
||||
"git": {
|
||||
"inline_blame": {
|
||||
"location": "status_bar"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
4. Show a commit summary next to the commit date and author:
|
||||
|
||||
```json [settings]
|
||||
{
|
||||
|
|
@ -2369,7 +2382,7 @@ Example:
|
|||
}
|
||||
```
|
||||
|
||||
4. Use this as the minimum column at which to display inline blame information:
|
||||
5. Use this as the minimum column at which to display inline blame information:
|
||||
|
||||
```json [settings]
|
||||
{
|
||||
|
|
@ -2381,7 +2394,7 @@ Example:
|
|||
}
|
||||
```
|
||||
|
||||
5. Set the padding between the end of the line and the inline blame hint, in ems:
|
||||
6. Set the padding between the end of the line and the inline blame hint, in ems:
|
||||
|
||||
```json [settings]
|
||||
{
|
||||
|
|
|
|||
|
|
@ -247,7 +247,8 @@ TBD: Centered layout related settings
|
|||
"git": {
|
||||
"inline_blame": {
|
||||
"enabled": true, // Show/hide inline blame
|
||||
"delay_ms": 0, // Show after delay (ms)
|
||||
"delay_ms": 0, // Show after delay (ms)
|
||||
"location": "inline", // inline, status_bar
|
||||
"min_column": 0, // Minimum column to inline display blame
|
||||
"padding": 7, // Padding between code and inline blame (em)
|
||||
"show_commit_summary": false // Show/hide commit summary
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue