Allow viewing past commits in Zed (#27636)

This PR adds functionality for loading the diff for an arbitrary git
commit, and displaying it in a tab. To retrieve the diff for the commit,
I'm using a single `git cat-file --batch` invocation to efficiently load
both the old and new versions of each file that was changed in the
commit.

Todo

* Features
* [x] Open the commit view when clicking the most recent commit message
in the commit panel
  * [x] Open the commit view when clicking a SHA in a git blame column
  * [x] Open the commit view when clicking a SHA in a commit tooltip
  * [x] Make it work over RPC
  * [x] Allow buffer search in commit view
* [x] Command palette action to open the commit for the current blame
line
* Styling
* [x] Add a header that shows the author, timestamp, and the full commit
message
  * [x] Remove stage/unstage buttons in commit view
  * [x] Truncate the commit message in the tab
* Bugs
  * [x] Dedup commit tabs within a pane
  * [x] Add a tooltip to the tab

Release Notes:

- Added the ability to show past commits in Zed. You can view the most
recent commit by clicking its message in the commit panel. And when
viewing a git blame, you can show any commit by clicking its sha.
This commit is contained in:
Max Brunsfeld 2025-03-31 16:26:47 -07:00 committed by GitHub
parent 33912011b7
commit 8546dc101d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 1742 additions and 603 deletions

View file

@ -16,7 +16,6 @@ pub mod actions;
mod blink_manager;
mod clangd_ext;
mod code_context_menus;
pub mod commit_tooltip;
pub mod display_map;
mod editor_settings;
mod editor_settings_controls;
@ -82,19 +81,21 @@ use code_context_menus::{
AvailableCodeAction, CodeActionContents, CodeActionsItem, CodeActionsMenu, CodeContextMenu,
CompletionsMenu, ContextMenuOrigin,
};
use git::blame::GitBlame;
use git::blame::{GitBlame, GlobalBlameRenderer};
use gpui::{
Action, Animation, AnimationExt, AnyElement, App, AppContext, AsyncWindowContext,
AvailableSpace, Background, Bounds, ClickEvent, ClipboardEntry, ClipboardItem, Context,
DispatchPhase, Edges, Entity, EntityInputHandler, EventEmitter, FocusHandle, FocusOutEvent,
Focusable, FontId, FontWeight, Global, HighlightStyle, Hsla, KeyContext, Modifiers,
MouseButton, MouseDownEvent, PaintQuad, ParentElement, Pixels, Render, SharedString, Size,
Stateful, Styled, StyledText, Subscription, Task, TextStyle, TextStyleRefinement,
UTF16Selection, UnderlineStyle, UniformListScrollHandle, WeakEntity, WeakFocusHandle, Window,
div, impl_actions, point, prelude::*, pulsating_between, px, relative, size,
Action, Animation, AnimationExt, AnyElement, AnyWeakEntity, App, AppContext,
AsyncWindowContext, AvailableSpace, Background, Bounds, ClickEvent, ClipboardEntry,
ClipboardItem, Context, DispatchPhase, Edges, Entity, EntityInputHandler, EventEmitter,
FocusHandle, FocusOutEvent, Focusable, FontId, FontWeight, Global, HighlightStyle, Hsla,
KeyContext, Modifiers, MouseButton, MouseDownEvent, PaintQuad, ParentElement, Pixels, Render,
SharedString, Size, Stateful, Styled, StyledText, Subscription, Task, TextStyle,
TextStyleRefinement, UTF16Selection, UnderlineStyle, UniformListScrollHandle, WeakEntity,
WeakFocusHandle, Window, div, impl_actions, point, prelude::*, pulsating_between, px, relative,
size,
};
use highlight_matching_bracket::refresh_matching_bracket_highlights;
use hover_links::{HoverLink, HoveredLinkState, InlayHighlight, find_file};
pub use hover_popover::hover_markdown_style;
use hover_popover::{HoverState, hide_hover};
use indent_guides::ActiveIndentGuidesState;
use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy};
@ -124,6 +125,7 @@ use project::{
},
};
pub use git::blame::BlameRenderer;
pub use proposed_changes_editor::{
ProposedChangeLocation, ProposedChangesEditor, ProposedChangesEditorToolbar,
};
@ -187,8 +189,8 @@ use theme::{
observe_buffer_font_size_adjustment,
};
use ui::{
ButtonSize, ButtonStyle, Disclosure, IconButton, IconButtonShape, IconName, IconSize, Key,
Tooltip, h_flex, prelude::*,
ButtonSize, ButtonStyle, ContextMenu, Disclosure, IconButton, IconButtonShape, IconName,
IconSize, Key, Tooltip, h_flex, prelude::*,
};
use util::{RangeExt, ResultExt, TryFutureExt, maybe, post_inc};
use workspace::{
@ -302,6 +304,8 @@ pub fn init_settings(cx: &mut App) {
pub fn init(cx: &mut App) {
init_settings(cx);
cx.set_global(GlobalBlameRenderer(Arc::new(())));
workspace::register_project_item::<Editor>(cx);
workspace::FollowableViewRegistry::register::<Editor>(cx);
workspace::register_serializable_item::<Editor>(cx);
@ -347,6 +351,10 @@ pub fn init(cx: &mut App) {
});
}
pub fn set_blame_renderer(renderer: impl BlameRenderer + 'static, cx: &mut App) {
cx.set_global(GlobalBlameRenderer(Arc::new(renderer)));
}
pub struct SearchWithinRange;
trait InvalidationRegion {
@ -766,7 +774,7 @@ pub struct Editor {
show_git_blame_gutter: bool,
show_git_blame_inline: bool,
show_git_blame_inline_delay_task: Option<Task<()>>,
git_blame_inline_tooltip: Option<WeakEntity<crate::commit_tooltip::CommitTooltip>>,
pub git_blame_inline_tooltip: Option<AnyWeakEntity>,
git_blame_inline_enabled: bool,
render_diff_hunk_controls: RenderDiffHunkControlsFn,
serialize_dirty_buffers: bool,
@ -848,8 +856,6 @@ pub struct EditorSnapshot {
gutter_hovered: bool,
}
const GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED: usize = 20;
#[derive(Default, Debug, Clone, Copy)]
pub struct GutterDimensions {
pub left_padding: Pixels,
@ -1643,6 +1649,21 @@ impl Editor {
this
}
pub fn deploy_mouse_context_menu(
&mut self,
position: gpui::Point<Pixels>,
context_menu: Entity<ContextMenu>,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.mouse_context_menu = Some(MouseContextMenu::new(
crate::mouse_context_menu::MenuPosition::PinnedToScreen(position),
context_menu,
window,
cx,
));
}
pub fn mouse_menu_is_focused(&self, window: &Window, cx: &App) -> bool {
self.mouse_context_menu
.as_ref()
@ -14922,6 +14943,13 @@ impl Editor {
self.display_map.read(cx).folded_buffers()
}
pub fn disable_header_for_buffer(&mut self, buffer_id: BufferId, cx: &mut Context<Self>) {
self.display_map.update(cx, |display_map, cx| {
display_map.disable_header_for_buffer(buffer_id, cx);
});
cx.notify();
}
/// Removes any folds with the given ranges.
pub fn remove_folds_with_type<T: ToOffset + Clone>(
&mut self,
@ -15861,6 +15889,45 @@ impl Editor {
cx.notify();
}
pub fn open_git_blame_commit(
&mut self,
_: &OpenGitBlameCommit,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.open_git_blame_commit_internal(window, cx);
}
fn open_git_blame_commit_internal(
&mut self,
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<()> {
let blame = self.blame.as_ref()?;
let snapshot = self.snapshot(window, cx);
let cursor = self.selections.newest::<Point>(cx).head();
let (buffer, point, _) = snapshot.buffer_snapshot.point_to_buffer_point(cursor)?;
let blame_entry = 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()?;
let renderer = cx.global::<GlobalBlameRenderer>().0.clone();
let repo = blame.read(cx).repository(cx)?;
let workspace = self.workspace()?.downgrade();
renderer.open_blame_commit(blame_entry, repo, workspace, window, cx);
None
}
pub fn git_blame_inline_enabled(&self) -> bool {
self.git_blame_inline_enabled
}
@ -17794,7 +17861,9 @@ fn get_uncommitted_diff_for_buffer(
let mut tasks = Vec::new();
project.update(cx, |project, cx| {
for buffer in buffers {
tasks.push(project.open_uncommitted_diff(buffer.clone(), cx))
if project::File::from_dyn(buffer.read(cx).file()).is_some() {
tasks.push(project.open_uncommitted_diff(buffer.clone(), cx))
}
}
});
cx.spawn(async move |cx| {
@ -18911,13 +18980,13 @@ impl EditorSnapshot {
let git_blame_entries_width =
self.git_blame_gutter_max_author_length
.map(|max_author_length| {
let renderer = cx.global::<GlobalBlameRenderer>().0.clone();
const MAX_RELATIVE_TIMESTAMP: &str = "60 minutes ago";
/// The number of characters to dedicate to gaps and margins.
const SPACING_WIDTH: usize = 4;
let max_char_count = max_author_length
.min(GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED)
let max_char_count = max_author_length.min(renderer.max_author_length())
+ ::git::SHORT_SHA_LENGTH
+ MAX_RELATIVE_TIMESTAMP.len()
+ SPACING_WIDTH;