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:
parent
33912011b7
commit
8546dc101d
28 changed files with 1742 additions and 603 deletions
|
@ -419,6 +419,7 @@ actions!(
|
|||
EditLogBreakpoint,
|
||||
ToggleAutoSignatureHelp,
|
||||
ToggleGitBlameInline,
|
||||
OpenGitBlameCommit,
|
||||
ToggleIndentGuides,
|
||||
ToggleInlayHints,
|
||||
ToggleInlineDiagnostics,
|
||||
|
|
|
@ -1,343 +0,0 @@
|
|||
use futures::Future;
|
||||
use git::PullRequest;
|
||||
use git::blame::BlameEntry;
|
||||
use gpui::{
|
||||
App, Asset, ClipboardItem, Element, Entity, MouseButton, ParentElement, Render, ScrollHandle,
|
||||
StatefulInteractiveElement,
|
||||
};
|
||||
use markdown::Markdown;
|
||||
use settings::Settings;
|
||||
use std::hash::Hash;
|
||||
use theme::ThemeSettings;
|
||||
use time::{OffsetDateTime, UtcOffset};
|
||||
use time_format::format_local_timestamp;
|
||||
use ui::{Avatar, Divider, IconButtonShape, prelude::*, tooltip_container};
|
||||
use url::Url;
|
||||
|
||||
use crate::git::blame::GitRemote;
|
||||
use crate::hover_popover::hover_markdown_style;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CommitDetails {
|
||||
pub sha: SharedString,
|
||||
pub author_name: SharedString,
|
||||
pub author_email: SharedString,
|
||||
pub commit_time: OffsetDateTime,
|
||||
pub message: Option<ParsedCommitMessage>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct ParsedCommitMessage {
|
||||
pub message: SharedString,
|
||||
pub permalink: Option<Url>,
|
||||
pub pull_request: Option<PullRequest>,
|
||||
pub remote: Option<GitRemote>,
|
||||
}
|
||||
|
||||
struct CommitAvatar<'a> {
|
||||
commit: &'a CommitDetails,
|
||||
}
|
||||
|
||||
impl<'a> CommitAvatar<'a> {
|
||||
fn new(details: &'a CommitDetails) -> Self {
|
||||
Self { commit: details }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> CommitAvatar<'a> {
|
||||
fn render(
|
||||
&'a self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<CommitTooltip>,
|
||||
) -> Option<impl IntoElement + use<>> {
|
||||
let remote = self
|
||||
.commit
|
||||
.message
|
||||
.as_ref()
|
||||
.and_then(|details| details.remote.as_ref())
|
||||
.filter(|remote| remote.host_supports_avatars())?;
|
||||
|
||||
let avatar_url = CommitAvatarAsset::new(remote.clone(), self.commit.sha.clone());
|
||||
|
||||
let element = match window.use_asset::<CommitAvatarAsset>(&avatar_url, cx) {
|
||||
// Loading or no avatar found
|
||||
None | Some(None) => Icon::new(IconName::Person)
|
||||
.color(Color::Muted)
|
||||
.into_element()
|
||||
.into_any(),
|
||||
// Found
|
||||
Some(Some(url)) => Avatar::new(url.to_string()).into_element().into_any(),
|
||||
};
|
||||
Some(element)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct CommitAvatarAsset {
|
||||
sha: SharedString,
|
||||
remote: GitRemote,
|
||||
}
|
||||
|
||||
impl Hash for CommitAvatarAsset {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.sha.hash(state);
|
||||
self.remote.host.name().hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl CommitAvatarAsset {
|
||||
fn new(remote: GitRemote, sha: SharedString) -> Self {
|
||||
Self { remote, sha }
|
||||
}
|
||||
}
|
||||
|
||||
impl Asset for CommitAvatarAsset {
|
||||
type Source = Self;
|
||||
type Output = Option<SharedString>;
|
||||
|
||||
fn load(
|
||||
source: Self::Source,
|
||||
cx: &mut App,
|
||||
) -> impl Future<Output = Self::Output> + Send + 'static {
|
||||
let client = cx.http_client();
|
||||
|
||||
async move {
|
||||
source
|
||||
.remote
|
||||
.avatar_url(source.sha, client)
|
||||
.await
|
||||
.map(|url| SharedString::from(url.to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CommitTooltip {
|
||||
commit: CommitDetails,
|
||||
scroll_handle: ScrollHandle,
|
||||
markdown: Entity<Markdown>,
|
||||
}
|
||||
|
||||
impl CommitTooltip {
|
||||
pub fn blame_entry(
|
||||
blame: &BlameEntry,
|
||||
details: Option<ParsedCommitMessage>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let commit_time = blame
|
||||
.committer_time
|
||||
.and_then(|t| OffsetDateTime::from_unix_timestamp(t).ok())
|
||||
.unwrap_or(OffsetDateTime::now_utc());
|
||||
|
||||
Self::new(
|
||||
CommitDetails {
|
||||
sha: blame.sha.to_string().into(),
|
||||
commit_time,
|
||||
author_name: blame
|
||||
.author
|
||||
.clone()
|
||||
.unwrap_or("<no name>".to_string())
|
||||
.into(),
|
||||
author_email: blame.author_mail.clone().unwrap_or("".to_string()).into(),
|
||||
message: details,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn new(commit: CommitDetails, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let mut style = hover_markdown_style(window, cx);
|
||||
if let Some(code_block) = &style.code_block.text {
|
||||
style.base_text_style.refine(code_block);
|
||||
}
|
||||
let markdown = cx.new(|cx| {
|
||||
Markdown::new(
|
||||
commit
|
||||
.message
|
||||
.as_ref()
|
||||
.map(|message| message.message.clone())
|
||||
.unwrap_or_default(),
|
||||
style,
|
||||
None,
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
Self {
|
||||
commit,
|
||||
scroll_handle: ScrollHandle::new(),
|
||||
markdown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for CommitTooltip {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let avatar = CommitAvatar::new(&self.commit).render(window, cx);
|
||||
|
||||
let author = self.commit.author_name.clone();
|
||||
|
||||
let author_email = self.commit.author_email.clone();
|
||||
|
||||
let short_commit_id = self
|
||||
.commit
|
||||
.sha
|
||||
.get(0..8)
|
||||
.map(|sha| sha.to_string().into())
|
||||
.unwrap_or_else(|| self.commit.sha.clone());
|
||||
let full_sha = self.commit.sha.to_string().clone();
|
||||
let absolute_timestamp = format_local_timestamp(
|
||||
self.commit.commit_time,
|
||||
OffsetDateTime::now_utc(),
|
||||
time_format::TimestampFormat::MediumAbsolute,
|
||||
);
|
||||
|
||||
let message = self
|
||||
.commit
|
||||
.message
|
||||
.as_ref()
|
||||
.map(|_| self.markdown.clone().into_any_element())
|
||||
.unwrap_or("<no commit message>".into_any());
|
||||
|
||||
let pull_request = self
|
||||
.commit
|
||||
.message
|
||||
.as_ref()
|
||||
.and_then(|details| details.pull_request.clone());
|
||||
|
||||
let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx);
|
||||
let message_max_height = window.line_height() * 12 + (ui_font_size / 0.4);
|
||||
|
||||
tooltip_container(window, cx, move |this, _, cx| {
|
||||
this.occlude()
|
||||
.on_mouse_move(|_, _, cx| cx.stop_propagation())
|
||||
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
|
||||
.child(
|
||||
v_flex()
|
||||
.w(gpui::rems(30.))
|
||||
.gap_4()
|
||||
.child(
|
||||
h_flex()
|
||||
.pb_1p5()
|
||||
.gap_x_2()
|
||||
.overflow_x_hidden()
|
||||
.flex_wrap()
|
||||
.children(avatar)
|
||||
.child(author)
|
||||
.when(!author_email.is_empty(), |this| {
|
||||
this.child(
|
||||
div()
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.child(author_email),
|
||||
)
|
||||
})
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border_variant),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("inline-blame-commit-message")
|
||||
.child(message)
|
||||
.max_h(message_max_height)
|
||||
.overflow_y_scroll()
|
||||
.track_scroll(&self.scroll_handle),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.pt_1p5()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.child(absolute_timestamp)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.when_some(pull_request, |this, pr| {
|
||||
this.child(
|
||||
Button::new(
|
||||
"pull-request-button",
|
||||
format!("#{}", pr.number),
|
||||
)
|
||||
.color(Color::Muted)
|
||||
.icon(IconName::PullRequest)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_position(IconPosition::Start)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.on_click(move |_, _, cx| {
|
||||
cx.stop_propagation();
|
||||
cx.open_url(pr.url.as_str())
|
||||
}),
|
||||
)
|
||||
})
|
||||
.child(Divider::vertical())
|
||||
.child(
|
||||
Button::new(
|
||||
"commit-sha-button",
|
||||
short_commit_id.clone(),
|
||||
)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.color(Color::Muted)
|
||||
.icon(IconName::FileGit)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_position(IconPosition::Start)
|
||||
.disabled(
|
||||
self.commit
|
||||
.message
|
||||
.as_ref()
|
||||
.map_or(true, |details| {
|
||||
details.permalink.is_none()
|
||||
}),
|
||||
)
|
||||
.when_some(
|
||||
self.commit
|
||||
.message
|
||||
.as_ref()
|
||||
.and_then(|details| details.permalink.clone()),
|
||||
|this, url| {
|
||||
this.on_click(move |_, _, cx| {
|
||||
cx.stop_propagation();
|
||||
cx.open_url(url.as_str())
|
||||
})
|
||||
},
|
||||
),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("copy-sha-button", IconName::Copy)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.on_click(move |_, _, cx| {
|
||||
cx.stop_propagation();
|
||||
cx.write_to_clipboard(
|
||||
ClipboardItem::new_string(full_sha.clone()),
|
||||
)
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn blame_entry_timestamp(blame_entry: &BlameEntry, format: time_format::TimestampFormat) -> String {
|
||||
match blame_entry.author_offset_date_time() {
|
||||
Ok(timestamp) => {
|
||||
let local = chrono::Local::now().offset().local_minus_utc();
|
||||
time_format::format_localized_timestamp(
|
||||
timestamp,
|
||||
time::OffsetDateTime::now_utc(),
|
||||
UtcOffset::from_whole_seconds(local).unwrap(),
|
||||
format,
|
||||
)
|
||||
}
|
||||
Err(_) => "Error parsing date".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn blame_entry_relative_timestamp(blame_entry: &BlameEntry) -> String {
|
||||
blame_entry_timestamp(blame_entry, time_format::TimestampFormat::Relative)
|
||||
}
|
|
@ -321,6 +321,20 @@ impl DisplayMap {
|
|||
block_map.remove_intersecting_replace_blocks(offset_ranges, inclusive);
|
||||
}
|
||||
|
||||
pub fn disable_header_for_buffer(&mut self, buffer_id: BufferId, cx: &mut Context<Self>) {
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
let edits = self.buffer_subscription.consume().into_inner();
|
||||
let tab_size = Self::tab_size(&self.buffer, cx);
|
||||
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
|
||||
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
|
||||
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
|
||||
let (snapshot, edits) = self
|
||||
.wrap_map
|
||||
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
|
||||
let mut block_map = self.block_map.write(snapshot, edits);
|
||||
block_map.disable_header_for_buffer(buffer_id)
|
||||
}
|
||||
|
||||
pub fn fold_buffers(
|
||||
&mut self,
|
||||
buffer_ids: impl IntoIterator<Item = language::BufferId>,
|
||||
|
|
|
@ -40,6 +40,7 @@ pub struct BlockMap {
|
|||
buffer_header_height: u32,
|
||||
excerpt_header_height: u32,
|
||||
pub(super) folded_buffers: HashSet<BufferId>,
|
||||
buffers_with_disabled_headers: HashSet<BufferId>,
|
||||
}
|
||||
|
||||
pub struct BlockMapReader<'a> {
|
||||
|
@ -422,6 +423,7 @@ impl BlockMap {
|
|||
custom_blocks: Vec::new(),
|
||||
custom_blocks_by_id: TreeMap::default(),
|
||||
folded_buffers: HashSet::default(),
|
||||
buffers_with_disabled_headers: HashSet::default(),
|
||||
transforms: RefCell::new(transforms),
|
||||
wrap_snapshot: RefCell::new(wrap_snapshot.clone()),
|
||||
buffer_header_height,
|
||||
|
@ -642,11 +644,8 @@ impl BlockMap {
|
|||
);
|
||||
|
||||
if buffer.show_headers() {
|
||||
blocks_in_edit.extend(BlockMap::header_and_footer_blocks(
|
||||
self.buffer_header_height,
|
||||
self.excerpt_header_height,
|
||||
blocks_in_edit.extend(self.header_and_footer_blocks(
|
||||
buffer,
|
||||
&self.folded_buffers,
|
||||
(start_bound, end_bound),
|
||||
wrap_snapshot,
|
||||
));
|
||||
|
@ -714,10 +713,8 @@ impl BlockMap {
|
|||
}
|
||||
|
||||
fn header_and_footer_blocks<'a, R, T>(
|
||||
buffer_header_height: u32,
|
||||
excerpt_header_height: u32,
|
||||
&'a self,
|
||||
buffer: &'a multi_buffer::MultiBufferSnapshot,
|
||||
folded_buffers: &'a HashSet<BufferId>,
|
||||
range: R,
|
||||
wrap_snapshot: &'a WrapSnapshot,
|
||||
) -> impl Iterator<Item = (BlockPlacement<WrapRow>, Block)> + 'a
|
||||
|
@ -728,73 +725,78 @@ impl BlockMap {
|
|||
let mut boundaries = buffer.excerpt_boundaries_in_range(range).peekable();
|
||||
|
||||
std::iter::from_fn(move || {
|
||||
let excerpt_boundary = boundaries.next()?;
|
||||
let wrap_row = wrap_snapshot
|
||||
.make_wrap_point(Point::new(excerpt_boundary.row.0, 0), Bias::Left)
|
||||
.row();
|
||||
loop {
|
||||
let excerpt_boundary = boundaries.next()?;
|
||||
let wrap_row = wrap_snapshot
|
||||
.make_wrap_point(Point::new(excerpt_boundary.row.0, 0), Bias::Left)
|
||||
.row();
|
||||
|
||||
let new_buffer_id = match (&excerpt_boundary.prev, &excerpt_boundary.next) {
|
||||
(None, next) => Some(next.buffer_id),
|
||||
(Some(prev), next) => {
|
||||
if prev.buffer_id != next.buffer_id {
|
||||
Some(next.buffer_id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let mut height = 0;
|
||||
|
||||
if let Some(new_buffer_id) = new_buffer_id {
|
||||
let first_excerpt = excerpt_boundary.next.clone();
|
||||
if folded_buffers.contains(&new_buffer_id) {
|
||||
let mut last_excerpt_end_row = first_excerpt.end_row;
|
||||
|
||||
while let Some(next_boundary) = boundaries.peek() {
|
||||
if next_boundary.next.buffer_id == new_buffer_id {
|
||||
last_excerpt_end_row = next_boundary.next.end_row;
|
||||
let new_buffer_id = match (&excerpt_boundary.prev, &excerpt_boundary.next) {
|
||||
(None, next) => Some(next.buffer_id),
|
||||
(Some(prev), next) => {
|
||||
if prev.buffer_id != next.buffer_id {
|
||||
Some(next.buffer_id)
|
||||
} else {
|
||||
break;
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let mut height = 0;
|
||||
|
||||
if let Some(new_buffer_id) = new_buffer_id {
|
||||
let first_excerpt = excerpt_boundary.next.clone();
|
||||
if self.buffers_with_disabled_headers.contains(&new_buffer_id) {
|
||||
continue;
|
||||
}
|
||||
if self.folded_buffers.contains(&new_buffer_id) {
|
||||
let mut last_excerpt_end_row = first_excerpt.end_row;
|
||||
|
||||
while let Some(next_boundary) = boundaries.peek() {
|
||||
if next_boundary.next.buffer_id == new_buffer_id {
|
||||
last_excerpt_end_row = next_boundary.next.end_row;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
|
||||
boundaries.next();
|
||||
}
|
||||
|
||||
boundaries.next();
|
||||
let wrap_end_row = wrap_snapshot
|
||||
.make_wrap_point(
|
||||
Point::new(
|
||||
last_excerpt_end_row.0,
|
||||
buffer.line_len(last_excerpt_end_row),
|
||||
),
|
||||
Bias::Right,
|
||||
)
|
||||
.row();
|
||||
|
||||
return Some((
|
||||
BlockPlacement::Replace(WrapRow(wrap_row)..=WrapRow(wrap_end_row)),
|
||||
Block::FoldedBuffer {
|
||||
height: height + self.buffer_header_height,
|
||||
first_excerpt,
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
let wrap_end_row = wrap_snapshot
|
||||
.make_wrap_point(
|
||||
Point::new(
|
||||
last_excerpt_end_row.0,
|
||||
buffer.line_len(last_excerpt_end_row),
|
||||
),
|
||||
Bias::Right,
|
||||
)
|
||||
.row();
|
||||
|
||||
return Some((
|
||||
BlockPlacement::Replace(WrapRow(wrap_row)..=WrapRow(wrap_end_row)),
|
||||
Block::FoldedBuffer {
|
||||
height: height + buffer_header_height,
|
||||
first_excerpt,
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if new_buffer_id.is_some() {
|
||||
height += buffer_header_height;
|
||||
} else {
|
||||
height += excerpt_header_height;
|
||||
}
|
||||
if new_buffer_id.is_some() {
|
||||
height += self.buffer_header_height;
|
||||
} else {
|
||||
height += self.excerpt_header_height;
|
||||
}
|
||||
|
||||
Some((
|
||||
BlockPlacement::Above(WrapRow(wrap_row)),
|
||||
Block::ExcerptBoundary {
|
||||
excerpt: excerpt_boundary.next,
|
||||
height,
|
||||
starts_new_buffer: new_buffer_id.is_some(),
|
||||
},
|
||||
))
|
||||
return Some((
|
||||
BlockPlacement::Above(WrapRow(wrap_row)),
|
||||
Block::ExcerptBoundary {
|
||||
excerpt: excerpt_boundary.next,
|
||||
height,
|
||||
starts_new_buffer: new_buffer_id.is_some(),
|
||||
},
|
||||
));
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -1168,6 +1170,10 @@ impl BlockMapWriter<'_> {
|
|||
self.remove(blocks_to_remove);
|
||||
}
|
||||
|
||||
pub fn disable_header_for_buffer(&mut self, buffer_id: BufferId) {
|
||||
self.0.buffers_with_disabled_headers.insert(buffer_id);
|
||||
}
|
||||
|
||||
pub fn fold_buffers(
|
||||
&mut self,
|
||||
buffer_ids: impl IntoIterator<Item = BufferId>,
|
||||
|
@ -3159,11 +3165,8 @@ mod tests {
|
|||
}));
|
||||
|
||||
// Note that this needs to be synced with the related section in BlockMap::sync
|
||||
expected_blocks.extend(BlockMap::header_and_footer_blocks(
|
||||
buffer_start_header_height,
|
||||
excerpt_header_height,
|
||||
expected_blocks.extend(block_map.header_and_footer_blocks(
|
||||
&buffer_snapshot,
|
||||
&block_map.folded_buffers,
|
||||
0..,
|
||||
&wraps_snapshot,
|
||||
));
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -3,13 +3,12 @@ use crate::{
|
|||
ContextMenuPlacement, CursorShape, CustomBlockId, DisplayDiffHunk, DisplayPoint, DisplayRow,
|
||||
DocumentHighlightRead, DocumentHighlightWrite, EditDisplayMode, Editor, EditorMode,
|
||||
EditorSettings, EditorSnapshot, EditorStyle, FILE_HEADER_HEIGHT, FocusedBlock,
|
||||
GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput,
|
||||
HoveredCursor, InlayHintRefreshReason, InlineCompletion, JumpData, LineDown, LineHighlight,
|
||||
LineUp, MAX_LINE_LEN, MIN_LINE_NUMBER_DIGITS, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, OpenExcerpts,
|
||||
PageDown, PageUp, Point, RowExt, RowRangeExt, SelectPhase, SelectedTextHighlight, Selection,
|
||||
SoftWrap, StickyHeaderExcerpt, ToPoint, ToggleFold,
|
||||
GutterDimensions, HalfPageDown, HalfPageUp, HandleInput, HoveredCursor, InlayHintRefreshReason,
|
||||
InlineCompletion, JumpData, LineDown, LineHighlight, LineUp, MAX_LINE_LEN,
|
||||
MIN_LINE_NUMBER_DIGITS, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, OpenExcerpts, PageDown, PageUp,
|
||||
Point, RowExt, RowRangeExt, SelectPhase, SelectedTextHighlight, Selection, SoftWrap,
|
||||
StickyHeaderExcerpt, ToPoint, ToggleFold,
|
||||
code_context_menus::{CodeActionsMenu, MENU_ASIDE_MAX_WIDTH, MENU_ASIDE_MIN_WIDTH, MENU_GAP},
|
||||
commit_tooltip::{CommitTooltip, ParsedCommitMessage, blame_entry_relative_timestamp},
|
||||
display_map::{
|
||||
Block, BlockContext, BlockStyle, DisplaySnapshot, HighlightedChunk, ToDisplayPoint,
|
||||
},
|
||||
|
@ -17,13 +16,13 @@ use crate::{
|
|||
CurrentLineHighlight, DoubleClickInMultibuffer, MultiCursorModifier, ScrollBeyondLastLine,
|
||||
ScrollbarAxes, ScrollbarDiagnostics, ShowScrollbar,
|
||||
},
|
||||
git::blame::GitBlame,
|
||||
git::blame::{BlameRenderer, GitBlame, GlobalBlameRenderer},
|
||||
hover_popover::{
|
||||
self, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT, hover_at,
|
||||
},
|
||||
inlay_hint_settings,
|
||||
items::BufferSearchHighlights,
|
||||
mouse_context_menu::{self, MenuPosition, MouseContextMenu},
|
||||
mouse_context_menu::{self, MenuPosition},
|
||||
scroll::scroll_amount::ScrollAmount,
|
||||
};
|
||||
use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind};
|
||||
|
@ -34,12 +33,12 @@ use file_icons::FileIcons;
|
|||
use git::{Oid, blame::BlameEntry, status::FileStatus};
|
||||
use gpui::{
|
||||
Action, Along, AnyElement, App, AvailableSpace, Axis as ScrollbarAxis, BorderStyle, Bounds,
|
||||
ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners, CursorStyle, DispatchPhase,
|
||||
Edges, Element, ElementInputHandler, Entity, Focusable as _, FontId, GlobalElementId, Hitbox,
|
||||
Hsla, InteractiveElement, IntoElement, Keystroke, Length, ModifiersChangedEvent, MouseButton,
|
||||
ClickEvent, ContentMask, Context, Corner, Corners, CursorStyle, DispatchPhase, Edges, Element,
|
||||
ElementInputHandler, Entity, Focusable as _, FontId, GlobalElementId, Hitbox, Hsla,
|
||||
InteractiveElement, IntoElement, Keystroke, Length, ModifiersChangedEvent, MouseButton,
|
||||
MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta,
|
||||
ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled,
|
||||
Subscription, TextRun, TextStyleRefinement, Window, anchored, deferred, div, fill,
|
||||
TextRun, TextStyleRefinement, WeakEntity, Window, anchored, deferred, div, fill,
|
||||
linear_color_stop, linear_gradient, outline, point, px, quad, relative, size, solid_background,
|
||||
transparent_black,
|
||||
};
|
||||
|
@ -76,10 +75,10 @@ use std::{
|
|||
use sum_tree::Bias;
|
||||
use text::BufferId;
|
||||
use theme::{ActiveTheme, Appearance, BufferLineHeight, PlayerColor};
|
||||
use ui::{ButtonLike, ContextMenu, KeyBinding, POPOVER_Y_PADDING, Tooltip, h_flex, prelude::*};
|
||||
use ui::{ButtonLike, KeyBinding, POPOVER_Y_PADDING, Tooltip, h_flex, prelude::*};
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use util::{RangeExt, ResultExt, debug_panic};
|
||||
use workspace::{item::Item, notifications::NotifyTaskExt};
|
||||
use workspace::{Workspace, item::Item, notifications::NotifyTaskExt};
|
||||
|
||||
const INLINE_BLAME_PADDING_EM_WIDTHS: f32 = 7.;
|
||||
|
||||
|
@ -426,6 +425,7 @@ impl EditorElement {
|
|||
register_action(editor, window, Editor::copy_file_location);
|
||||
register_action(editor, window, Editor::toggle_git_blame);
|
||||
register_action(editor, window, Editor::toggle_git_blame_inline);
|
||||
register_action(editor, window, Editor::open_git_blame_commit);
|
||||
register_action(editor, window, Editor::toggle_selected_diff_hunks);
|
||||
register_action(editor, window, Editor::toggle_staged_selected_diff_hunks);
|
||||
register_action(editor, window, Editor::stage_and_next);
|
||||
|
@ -1759,14 +1759,21 @@ impl EditorElement {
|
|||
padding * em_width
|
||||
};
|
||||
|
||||
let workspace = editor.workspace()?.downgrade();
|
||||
let blame_entry = blame
|
||||
.update(cx, |blame, cx| {
|
||||
blame.blame_for_rows(&[*row_info], cx).next()
|
||||
})
|
||||
.flatten()?;
|
||||
|
||||
let mut element =
|
||||
render_inline_blame_entry(self.editor.clone(), &blame, blame_entry, &self.style, cx);
|
||||
let mut element = render_inline_blame_entry(
|
||||
self.editor.clone(),
|
||||
workspace,
|
||||
&blame,
|
||||
blame_entry,
|
||||
&self.style,
|
||||
cx,
|
||||
)?;
|
||||
|
||||
let start_y = content_origin.y
|
||||
+ line_height * (display_row.as_f32() - scroll_pixel_position.y / line_height);
|
||||
|
@ -1816,6 +1823,7 @@ impl EditorElement {
|
|||
}
|
||||
|
||||
let blame = self.editor.read(cx).blame.clone()?;
|
||||
let workspace = self.editor.read(cx).workspace()?;
|
||||
let blamed_rows: Vec<_> = blame.update(cx, |blame, cx| {
|
||||
blame.blame_for_rows(buffer_rows, cx).collect()
|
||||
});
|
||||
|
@ -1829,36 +1837,35 @@ impl EditorElement {
|
|||
let start_x = em_width;
|
||||
|
||||
let mut last_used_color: Option<(PlayerColor, Oid)> = None;
|
||||
let blame_renderer = cx.global::<GlobalBlameRenderer>().0.clone();
|
||||
|
||||
let shaped_lines = blamed_rows
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.flat_map(|(ix, blame_entry)| {
|
||||
if let Some(blame_entry) = blame_entry {
|
||||
let mut element = render_blame_entry(
|
||||
ix,
|
||||
&blame,
|
||||
blame_entry,
|
||||
&self.style,
|
||||
&mut last_used_color,
|
||||
self.editor.clone(),
|
||||
cx,
|
||||
);
|
||||
let mut element = render_blame_entry(
|
||||
ix,
|
||||
&blame,
|
||||
blame_entry?,
|
||||
&self.style,
|
||||
&mut last_used_color,
|
||||
self.editor.clone(),
|
||||
workspace.clone(),
|
||||
blame_renderer.clone(),
|
||||
cx,
|
||||
)?;
|
||||
|
||||
let start_y = ix as f32 * line_height - (scroll_top % line_height);
|
||||
let absolute_offset = gutter_hitbox.origin + point(start_x, start_y);
|
||||
let start_y = ix as f32 * line_height - (scroll_top % line_height);
|
||||
let absolute_offset = gutter_hitbox.origin + point(start_x, start_y);
|
||||
|
||||
element.prepaint_as_root(
|
||||
absolute_offset,
|
||||
size(width, AvailableSpace::MinContent),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
element.prepaint_as_root(
|
||||
absolute_offset,
|
||||
size(width, AvailableSpace::MinContent),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
Some(element)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
Some(element)
|
||||
})
|
||||
.collect();
|
||||
|
||||
|
@ -5725,61 +5732,43 @@ fn prepaint_gutter_button(
|
|||
|
||||
fn render_inline_blame_entry(
|
||||
editor: Entity<Editor>,
|
||||
blame: &gpui::Entity<GitBlame>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
blame: &Entity<GitBlame>,
|
||||
blame_entry: BlameEntry,
|
||||
style: &EditorStyle,
|
||||
cx: &mut App,
|
||||
) -> 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
|
||||
.show_inline_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 blame = blame.clone();
|
||||
let blame_entry = blame_entry.clone();
|
||||
|
||||
h_flex()
|
||||
.id("inline-blame")
|
||||
.w_full()
|
||||
.font_family(style.text.font().family)
|
||||
.text_color(cx.theme().status().hint)
|
||||
.line_height(style.text.line_height)
|
||||
.child(Icon::new(IconName::FileGit).color(Color::Hint))
|
||||
.child(text)
|
||||
.gap_2()
|
||||
.hoverable_tooltip(move |window, cx| {
|
||||
let details = blame.read(cx).details_for_entry(&blame_entry);
|
||||
let tooltip =
|
||||
cx.new(|cx| CommitTooltip::blame_entry(&blame_entry, details, window, cx));
|
||||
editor.update(cx, |editor, _| {
|
||||
editor.git_blame_inline_tooltip = Some(tooltip.downgrade())
|
||||
});
|
||||
tooltip.into()
|
||||
})
|
||||
.into_any()
|
||||
) -> Option<AnyElement> {
|
||||
let renderer = cx.global::<GlobalBlameRenderer>().0.clone();
|
||||
let blame = blame.read(cx);
|
||||
let details = blame.details_for_entry(&blame_entry);
|
||||
let repository = blame.repository(cx)?.clone();
|
||||
renderer.render_inline_blame_entry(
|
||||
&style.text,
|
||||
blame_entry,
|
||||
details,
|
||||
repository,
|
||||
workspace,
|
||||
editor,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
fn render_blame_entry(
|
||||
ix: usize,
|
||||
blame: &gpui::Entity<GitBlame>,
|
||||
blame: &Entity<GitBlame>,
|
||||
blame_entry: BlameEntry,
|
||||
style: &EditorStyle,
|
||||
last_used_color: &mut Option<(PlayerColor, Oid)>,
|
||||
editor: Entity<Editor>,
|
||||
workspace: Entity<Workspace>,
|
||||
renderer: Arc<dyn BlameRenderer>,
|
||||
cx: &mut App,
|
||||
) -> AnyElement {
|
||||
) -> Option<AnyElement> {
|
||||
let mut sha_color = cx
|
||||
.theme()
|
||||
.players()
|
||||
.color_for_participant(blame_entry.sha.into());
|
||||
|
||||
// If the last color we used is the same as the one we get for this line, but
|
||||
// the commit SHAs are different, then we try again to get a different color.
|
||||
match *last_used_color {
|
||||
|
@ -5791,97 +5780,20 @@ fn render_blame_entry(
|
|||
};
|
||||
last_used_color.replace((sha_color, blame_entry.sha));
|
||||
|
||||
let relative_timestamp = blame_entry_relative_timestamp(&blame_entry);
|
||||
|
||||
let short_commit_id = blame_entry.sha.display_short();
|
||||
|
||||
let author_name = blame_entry.author.as_deref().unwrap_or("<no name>");
|
||||
let name = util::truncate_and_trailoff(author_name, GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED);
|
||||
let details = blame.read(cx).details_for_entry(&blame_entry);
|
||||
|
||||
h_flex()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.font_family(style.text.font().family)
|
||||
.line_height(style.text.line_height)
|
||||
.id(("blame", ix))
|
||||
.text_color(cx.theme().status().hint)
|
||||
.pr_2()
|
||||
.gap_2()
|
||||
.child(
|
||||
h_flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.child(div().text_color(sha_color.cursor).child(short_commit_id))
|
||||
.child(name),
|
||||
)
|
||||
.child(relative_timestamp)
|
||||
.on_mouse_down(MouseButton::Right, {
|
||||
let blame_entry = blame_entry.clone();
|
||||
let details = details.clone();
|
||||
move |event, window, cx| {
|
||||
deploy_blame_entry_context_menu(
|
||||
&blame_entry,
|
||||
details.as_ref(),
|
||||
editor.clone(),
|
||||
event.position,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
})
|
||||
.hover(|style| style.bg(cx.theme().colors().element_hover))
|
||||
.when_some(
|
||||
details
|
||||
.as_ref()
|
||||
.and_then(|details| details.permalink.clone()),
|
||||
|this, url| {
|
||||
this.cursor_pointer().on_click(move |_, _, cx| {
|
||||
cx.stop_propagation();
|
||||
cx.open_url(url.as_str())
|
||||
})
|
||||
},
|
||||
)
|
||||
.hoverable_tooltip(move |window, cx| {
|
||||
cx.new(|cx| CommitTooltip::blame_entry(&blame_entry, details.clone(), window, cx))
|
||||
.into()
|
||||
})
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn deploy_blame_entry_context_menu(
|
||||
blame_entry: &BlameEntry,
|
||||
details: Option<&ParsedCommitMessage>,
|
||||
editor: Entity<Editor>,
|
||||
position: gpui::Point<Pixels>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let context_menu = ContextMenu::build(window, cx, move |menu, _, _| {
|
||||
let sha = format!("{}", blame_entry.sha);
|
||||
menu.on_blur_subscription(Subscription::new(|| {}))
|
||||
.entry("Copy commit SHA", None, move |_, cx| {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(sha.clone()));
|
||||
})
|
||||
.when_some(
|
||||
details.and_then(|details| details.permalink.clone()),
|
||||
|this, url| {
|
||||
this.entry("Open permalink", None, move |_, cx| {
|
||||
cx.open_url(url.as_str())
|
||||
})
|
||||
},
|
||||
)
|
||||
});
|
||||
|
||||
editor.update(cx, move |editor, cx| {
|
||||
editor.mouse_context_menu = Some(MouseContextMenu::new(
|
||||
MenuPosition::PinnedToScreen(position),
|
||||
context_menu,
|
||||
window,
|
||||
cx,
|
||||
));
|
||||
cx.notify();
|
||||
});
|
||||
let blame = blame.read(cx);
|
||||
let details = blame.details_for_entry(&blame_entry);
|
||||
let repository = blame.repository(cx)?;
|
||||
renderer.render_blame_entry(
|
||||
&style.text,
|
||||
blame_entry,
|
||||
details,
|
||||
repository,
|
||||
workspace.downgrade(),
|
||||
editor,
|
||||
ix,
|
||||
sha_color.cursor,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -6588,9 +6500,9 @@ impl Element for EditorElement {
|
|||
window.with_rem_size(rem_size, |window| {
|
||||
window.with_text_style(Some(text_style), |window| {
|
||||
window.with_content_mask(Some(ContentMask { bounds }), |window| {
|
||||
let mut snapshot = self
|
||||
.editor
|
||||
.update(cx, |editor, cx| editor.snapshot(window, cx));
|
||||
let (mut snapshot, is_read_only) = self.editor.update(cx, |editor, cx| {
|
||||
(editor.snapshot(window, cx), editor.read_only(cx))
|
||||
});
|
||||
let style = self.style.clone();
|
||||
|
||||
let font_id = window.text_system().resolve_font(&style.text.font());
|
||||
|
@ -6970,11 +6882,12 @@ impl Element for EditorElement {
|
|||
.flatten()?;
|
||||
let mut element = render_inline_blame_entry(
|
||||
self.editor.clone(),
|
||||
editor.workspace()?.downgrade(),
|
||||
blame,
|
||||
blame_entry,
|
||||
&style,
|
||||
cx,
|
||||
);
|
||||
)?;
|
||||
let inline_blame_padding = INLINE_BLAME_PADDING_EM_WIDTHS * em_advance;
|
||||
Some(
|
||||
element
|
||||
|
@ -7507,19 +7420,23 @@ impl Element for EditorElement {
|
|||
editor.last_position_map = Some(position_map.clone())
|
||||
});
|
||||
|
||||
let diff_hunk_controls = self.layout_diff_hunk_controls(
|
||||
start_row..end_row,
|
||||
&row_infos,
|
||||
&text_hitbox,
|
||||
&position_map,
|
||||
newest_selection_head,
|
||||
line_height,
|
||||
scroll_pixel_position,
|
||||
&display_hunks,
|
||||
self.editor.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let diff_hunk_controls = if is_read_only {
|
||||
vec![]
|
||||
} else {
|
||||
self.layout_diff_hunk_controls(
|
||||
start_row..end_row,
|
||||
&row_infos,
|
||||
&text_hitbox,
|
||||
&position_map,
|
||||
newest_selection_head,
|
||||
line_height,
|
||||
scroll_pixel_position,
|
||||
&display_hunks,
|
||||
self.editor.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
};
|
||||
|
||||
EditorLayout {
|
||||
mode,
|
||||
|
|
|
@ -1,22 +1,22 @@
|
|||
use crate::Editor;
|
||||
use anyhow::Result;
|
||||
use collections::HashMap;
|
||||
use git::{
|
||||
GitHostingProvider, GitHostingProviderRegistry, Oid,
|
||||
blame::{Blame, BlameEntry},
|
||||
GitHostingProviderRegistry, GitRemote, Oid,
|
||||
blame::{Blame, BlameEntry, ParsedCommitMessage},
|
||||
parse_git_remote_url,
|
||||
};
|
||||
use gpui::{App, AppContext as _, Context, Entity, Subscription, Task};
|
||||
use http_client::HttpClient;
|
||||
use gpui::{
|
||||
AnyElement, App, AppContext as _, Context, Entity, Hsla, Subscription, Task, TextStyle,
|
||||
WeakEntity, Window,
|
||||
};
|
||||
use language::{Bias, Buffer, BufferSnapshot, Edit};
|
||||
use multi_buffer::RowInfo;
|
||||
use project::{Project, ProjectItem};
|
||||
use project::{Project, ProjectItem, git_store::Repository};
|
||||
use smallvec::SmallVec;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use sum_tree::SumTree;
|
||||
use ui::SharedString;
|
||||
use url::Url;
|
||||
|
||||
use crate::commit_tooltip::ParsedCommitMessage;
|
||||
use workspace::Workspace;
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct GitBlameEntry {
|
||||
|
@ -59,45 +59,11 @@ impl<'a> sum_tree::Dimension<'a, GitBlameEntrySummary> for u32 {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct GitRemote {
|
||||
pub host: Arc<dyn GitHostingProvider + Send + Sync + 'static>,
|
||||
pub owner: String,
|
||||
pub repo: String,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for GitRemote {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("GitRemote")
|
||||
.field("host", &self.host.name())
|
||||
.field("owner", &self.owner)
|
||||
.field("repo", &self.repo)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl GitRemote {
|
||||
pub fn host_supports_avatars(&self) -> bool {
|
||||
self.host.supports_avatars()
|
||||
}
|
||||
|
||||
pub async fn avatar_url(
|
||||
&self,
|
||||
commit: SharedString,
|
||||
client: Arc<dyn HttpClient>,
|
||||
) -> Option<Url> {
|
||||
self.host
|
||||
.commit_author_avatar_url(&self.owner, &self.repo, commit, client)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
}
|
||||
}
|
||||
pub struct GitBlame {
|
||||
project: Entity<Project>,
|
||||
buffer: Entity<Buffer>,
|
||||
entries: SumTree<GitBlameEntry>,
|
||||
commit_details: HashMap<Oid, crate::commit_tooltip::ParsedCommitMessage>,
|
||||
commit_details: HashMap<Oid, ParsedCommitMessage>,
|
||||
buffer_snapshot: BufferSnapshot,
|
||||
buffer_edits: text::Subscription,
|
||||
task: Task<Result<()>>,
|
||||
|
@ -109,6 +75,91 @@ pub struct GitBlame {
|
|||
_regenerate_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
pub trait BlameRenderer {
|
||||
fn max_author_length(&self) -> usize;
|
||||
|
||||
fn render_blame_entry(
|
||||
&self,
|
||||
_: &TextStyle,
|
||||
_: BlameEntry,
|
||||
_: Option<ParsedCommitMessage>,
|
||||
_: Entity<Repository>,
|
||||
_: WeakEntity<Workspace>,
|
||||
_: Entity<Editor>,
|
||||
_: usize,
|
||||
_: Hsla,
|
||||
_: &mut App,
|
||||
) -> Option<AnyElement>;
|
||||
|
||||
fn render_inline_blame_entry(
|
||||
&self,
|
||||
_: &TextStyle,
|
||||
_: BlameEntry,
|
||||
_: Option<ParsedCommitMessage>,
|
||||
_: Entity<Repository>,
|
||||
_: WeakEntity<Workspace>,
|
||||
_: Entity<Editor>,
|
||||
_: &mut App,
|
||||
) -> Option<AnyElement>;
|
||||
|
||||
fn open_blame_commit(
|
||||
&self,
|
||||
_: BlameEntry,
|
||||
_: Entity<Repository>,
|
||||
_: WeakEntity<Workspace>,
|
||||
_: &mut Window,
|
||||
_: &mut App,
|
||||
);
|
||||
}
|
||||
|
||||
impl BlameRenderer for () {
|
||||
fn max_author_length(&self) -> usize {
|
||||
0
|
||||
}
|
||||
|
||||
fn render_blame_entry(
|
||||
&self,
|
||||
_: &TextStyle,
|
||||
_: BlameEntry,
|
||||
_: Option<ParsedCommitMessage>,
|
||||
_: Entity<Repository>,
|
||||
_: WeakEntity<Workspace>,
|
||||
_: Entity<Editor>,
|
||||
_: usize,
|
||||
_: Hsla,
|
||||
_: &mut App,
|
||||
) -> Option<AnyElement> {
|
||||
None
|
||||
}
|
||||
|
||||
fn render_inline_blame_entry(
|
||||
&self,
|
||||
_: &TextStyle,
|
||||
_: BlameEntry,
|
||||
_: Option<ParsedCommitMessage>,
|
||||
_: Entity<Repository>,
|
||||
_: WeakEntity<Workspace>,
|
||||
_: Entity<Editor>,
|
||||
_: &mut App,
|
||||
) -> Option<AnyElement> {
|
||||
None
|
||||
}
|
||||
|
||||
fn open_blame_commit(
|
||||
&self,
|
||||
_: BlameEntry,
|
||||
_: Entity<Repository>,
|
||||
_: WeakEntity<Workspace>,
|
||||
_: &mut Window,
|
||||
_: &mut App,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct GlobalBlameRenderer(pub Arc<dyn BlameRenderer>);
|
||||
|
||||
impl gpui::Global for GlobalBlameRenderer {}
|
||||
|
||||
impl GitBlame {
|
||||
pub fn new(
|
||||
buffer: Entity<Buffer>,
|
||||
|
@ -181,6 +232,15 @@ impl GitBlame {
|
|||
this
|
||||
}
|
||||
|
||||
pub fn repository(&self, cx: &App) -> Option<Entity<Repository>> {
|
||||
self.project
|
||||
.read(cx)
|
||||
.git_store()
|
||||
.read(cx)
|
||||
.repository_and_path_for_buffer_id(self.buffer.read(cx).remote_id(), cx)
|
||||
.map(|(repo, _)| repo)
|
||||
}
|
||||
|
||||
pub fn has_generated_entries(&self) -> bool {
|
||||
self.generated
|
||||
}
|
||||
|
|
|
@ -109,7 +109,7 @@ impl ProposedChangesEditor {
|
|||
let diff =
|
||||
this.multibuffer.read(cx).diff_for(buffer.remote_id())?;
|
||||
Some(diff.update(cx, |diff, cx| {
|
||||
diff.set_base_text(base_buffer.clone(), buffer, cx)
|
||||
diff.set_base_text_buffer(base_buffer.clone(), buffer, cx)
|
||||
}))
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
|
@ -185,7 +185,7 @@ impl ProposedChangesEditor {
|
|||
branch_buffer = location.buffer.update(cx, |buffer, cx| buffer.branch(cx));
|
||||
new_diffs.push(cx.new(|cx| {
|
||||
let mut diff = BufferDiff::new(&branch_buffer.read(cx).snapshot(), cx);
|
||||
let _ = diff.set_base_text(
|
||||
let _ = diff.set_base_text_buffer(
|
||||
location.buffer.clone(),
|
||||
branch_buffer.read(cx).text_snapshot(),
|
||||
cx,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue