History

commit view: Display message within editor (#44024)

#42441 moved the commit message out of the multi-buffer editor into its own header element which looks nicer, but unfortunately can make the view become unusable when the commit message is too long since it doesn't scroll with the diff. This PR maintains the metadata in its own element, but moves the commit message back to the editor so the user can scroll past it. This does mean that we lose markdown rendering for now, but we think this is a good solution for the moment. https://github.com/user-attachments/assets/d67cf22e-1a79-451a-932a-cdc8a65e43de Release Notes: - N/A ---------

Agus Zubiaga committed 11 months ago

Commit 8a12ecf1 parent 22bf4492 files · +94 −213Browse files

Unified · Split

2 files
  1. crates/editor/src/editor.rs +4 −0
  2. crates/git_ui/src/commit_view.rs +90 −213
crates/editor/src/editor.rs+4−0
@@ -22598,6 +22598,10 @@ impl Editor { · 22597 unmodified lines
2259822598 }
2259922599 }
2260022600
22601+ pub fn last_gutter_dimensions(&self) -> &GutterDimensions {
22602+ &self.gutter_dimensions
22603+ }
22604+
2260122605 pub fn wait_for_diff_to_load(&self) -> Option<Shared<Task<()>>> {
2260222606 self.load_diff_task.clone()
2260322607 }
crates/git_ui/src/commit_view.rs+90−213
@@ -1,19 +1,18 @@
11 use anyhow::{Context as _, Result};
22 use buffer_diff::{BufferDiff, BufferDiffSnapshot};
3−use editor::{Addon, Editor, EditorEvent, MultiBuffer};
3+use editor::display_map::{BlockPlacement, BlockProperties, BlockStyle};
4+use editor::{Addon, Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer};
45 use git::repository::{CommitDetails, CommitDiff, RepoPath};
56 use git::{GitHostingProviderRegistry, GitRemote, parse_git_remote_url};
67 use gpui::{
78 AnyElement, App, AppContext as _, Asset, AsyncApp, AsyncWindowContext, Context, Element,
89 Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement,
9− PromptLevel, Render, Styled, Task, TextStyleRefinement, UnderlineStyle, WeakEntity, Window,
10− actions, px,
10+ PromptLevel, Render, Styled, Task, WeakEntity, Window, actions,
1111 };
1212 use language::{
13− Buffer, Capability, DiskState, File, LanguageRegistry, LineEnding, ReplicaId, Rope, TextBuffer,
14− ToPoint,
13+ Anchor, Buffer, Capability, DiskState, File, LanguageRegistry, LineEnding, ReplicaId, Rope,
14+ TextBuffer, ToPoint,
1515 };
16−use markdown::{Markdown, MarkdownElement, MarkdownStyle};
1716 use multi_buffer::ExcerptInfo;
1817 use multi_buffer::PathKey;
1918 use project::{Project, WorktreeId, git_store::Repository};
@@ -63,7 +62,6 @@ pub struct CommitView { · 43 unmodified lines
6362 multibuffer: Entity<MultiBuffer>,
6463 repository: Entity<Repository>,
6564 remote: Option<GitRemote>,
66− markdown: Entity<Markdown>,
6765 }
6866
6967 struct GitBlob {
@@ -167,6 +165,8 @@ impl CommitView { · 97 unmodified lines
167165 .map(|worktree| worktree.read(cx).id());
168166
169167 let repository_clone = repository.clone();
168+ let commit_message = commit.message.clone();
169+
170170 cx.spawn(async move |this, cx| {
171171 for file in commit_diff.files {
172172 let is_deleted = file.new_text.is_none();
@@ -227,6 +227,58 @@ impl CommitView { · 54 unmodified lines
227227 });
228228 })?;
229229 }
230+
231+ let message_buffer = cx.new(|cx| {
232+ let mut buffer = Buffer::local(commit_message, cx);
233+ buffer.set_capability(Capability::ReadOnly, cx);
234+ buffer
235+ })?;
236+
237+ this.update(cx, |this, cx| {
238+ this.multibuffer.update(cx, |multibuffer, cx| {
239+ let range = ExcerptRange {
240+ context: Anchor::MIN..Anchor::MAX,
241+ primary: Anchor::MIN..Anchor::MAX,
242+ };
243+ multibuffer.insert_excerpts_after(
244+ ExcerptId::min(),
245+ message_buffer.clone(),
246+ [range],
247+ cx,
248+ )
249+ });
250+
251+ this.editor.update(cx, |editor, cx| {
252+ editor.disable_header_for_buffer(message_buffer.read(cx).remote_id(), cx);
253+
254+ editor.insert_blocks(
255+ [BlockProperties {
256+ placement: BlockPlacement::Above(editor::Anchor::min()),
257+ height: Some(1),
258+ style: BlockStyle::Sticky,
259+ render: Arc::new(|_| gpui::Empty.into_any_element()),
260+ priority: 0,
261+ }]
262+ .into_iter()
263+ .chain(
264+ editor
265+ .buffer()
266+ .read(cx)
267+ .buffer_anchor_to_anchor(&message_buffer, Anchor::MAX, cx)
268+ .map(|anchor| BlockProperties {
269+ placement: BlockPlacement::Below(anchor),
270+ height: Some(1),
271+ style: BlockStyle::Sticky,
272+ render: Arc::new(|_| gpui::Empty.into_any_element()),
273+ priority: 0,
274+ }),
275+ ),
276+ None,
277+ cx,
278+ )
279+ });
280+ })?;
281+
230282 anyhow::Ok(())
231283 })
232284 .detach();
@@ -246,14 +298,6 @@ impl CommitView { · 13 unmodified lines
246298 })
247299 });
248300
249− let processed_message = if let Some(ref remote) = remote {
250− Self::process_github_issues(&commit.message, remote)
251− } else {
252− commit.message.to_string()
253− };
254−
255− let markdown = cx.new(|cx| Markdown::new(processed_message.into(), None, None, cx));
256−
257301 Self {
258302 commit,
259303 editor,
@@ -261,18 +305,9 @@ impl CommitView { · 1 unmodified line
261305 stash,
262306 repository,
263307 remote,
264− markdown,
265308 }
266309 }
267310
268− fn fallback_commit_avatar() -> AnyElement {
269− Icon::new(IconName::Person)
270− .color(Color::Muted)
271− .size(IconSize::Medium)
272− .into_element()
273− .into_any()
274− }
275−
276311 fn render_commit_avatar(
277312 &self,
278313 sha: &SharedString,
@@ -280,21 +315,34 @@ impl CommitView { · 1 unmodified line
280315 window: &mut Window,
281316 cx: &mut App,
282317 ) -> AnyElement {
318+ let size = size.into();
283319 let remote = self.remote.as_ref().filter(|r| r.host_supports_avatars());
284320
285321 if let Some(remote) = remote {
286322 let avatar_asset = CommitAvatarAsset::new(remote.clone(), sha.clone());
287323 if let Some(Some(url)) = window.use_asset::<CommitAvatarAsset>(&avatar_asset, cx) {
288− Avatar::new(url.to_string())
324+ return Avatar::new(url.to_string())
289325 .size(size)
290326 .into_element()
291− .into_any()
292− } else {
293− Self::fallback_commit_avatar()
327+ .into_any();
294328 }
295− } else {
296− Self::fallback_commit_avatar()
297329 }
330+
331+ v_flex()
332+ .w(size)
333+ .h(size)
334+ .border_1()
335+ .border_color(cx.theme().colors().border)
336+ .rounded_full()
337+ .justify_center()
338+ .items_center()
339+ .child(
340+ Icon::new(IconName::Person)
341+ .color(Color::Muted)
342+ .size(IconSize::Medium)
343+ .into_element(),
344+ )
345+ .into_any()
298346 }
299347
300348 fn render_header(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
@@ -322,14 +370,24 @@ impl CommitView { · 21 unmodified lines
322370
323371 v_flex()
324372 .p_4()
373+ .pl_0()
325374 .gap_4()
326375 .border_b_1()
327376 .border_color(cx.theme().colors().border)
328377 .child(
329378 h_flex()
330379 .items_start()
331− .gap_3()
332− .child(self.render_commit_avatar(&commit.sha, gpui::rems(3.0), window, cx))
380+ .child(
381+ h_flex()
382+ .w(self.editor.read(cx).last_gutter_dimensions().full_width())
383+ .justify_center()
384+ .child(self.render_commit_avatar(
385+ &commit.sha,
386+ gpui::rems(3.0),
387+ window,
388+ cx,
389+ )),
390+ )
333391 .child(
334392 v_flex()
335393 .gap_1()
@@ -353,66 +411,6 @@ impl CommitView { · 17 unmodified lines
353411 .on_click(move |_, _, cx| cx.open_url(&url))
354412 })),
355413 )
356− .child(self.render_commit_message(window, cx))
357− }
358−
359− fn process_github_issues(message: &str, remote: &GitRemote) -> String {
360− let mut result = String::new();
361− let chars: Vec<char> = message.chars().collect();
362− let mut i = 0;
363−
364− while i < chars.len() {
365− if chars[i] == '#' && i + 1 < chars.len() && chars[i + 1].is_ascii_digit() {
366− let mut j = i + 1;
367− while j < chars.len() && chars[j].is_ascii_digit() {
368− j += 1;
369− }
370− let issue_number = &message[i + 1..i + (j - i)];
371− let url = format!(
372− "{}/{}/{}/issues/{}",
373− remote.host.base_url().as_str().trim_end_matches('/'),
374− remote.owner,
375− remote.repo,
376− issue_number
377− );
378− result.push_str(&format!("[#{}]({})", issue_number, url));
379− i = j;
380− } else if i + 3 < chars.len()
381− && chars[i] == 'G'
382− && chars[i + 1] == 'H'
383− && chars[i + 2] == '-'
384− && chars[i + 3].is_ascii_digit()
385− {
386− let mut j = i + 3;
387− while j < chars.len() && chars[j].is_ascii_digit() {
388− j += 1;
389− }
390− let issue_number = &message[i + 3..i + (j - i)];
391− let url = format!(
392− "{}/{}/{}/issues/{}",
393− remote.host.base_url().as_str().trim_end_matches('/'),
394− remote.owner,
395− remote.repo,
396− issue_number
397− );
398− result.push_str(&format!("[GH-{}]({})", issue_number, url));
399− i = j;
400− } else {
401− result.push(chars[i]);
402− i += 1;
403− }
404− }
405−
406− result
407− }
408−
409− fn render_commit_message(
410− &self,
411− window: &mut Window,
412− cx: &mut Context<Self>,
413− ) -> impl IntoElement {
414− let style = hover_markdown_style(window, cx);
415− MarkdownElement::new(self.markdown.clone(), style)
416414 }
417415
418416 fn apply_stash(workspace: &mut Workspace, window: &mut Window, cx: &mut App) {
@@ -963,12 +961,6 @@ impl Item for CommitView { · 544 unmodified lines
963961 .update(cx, |editor, cx| editor.clone(window, cx))
964962 });
965963 let multibuffer = editor.read(cx).buffer().clone();
966− let processed_message = if let Some(ref remote) = self.remote {
967− Self::process_github_issues(&self.commit.message, remote)
968− } else {
969− self.commit.message.to_string()
970− };
971− let markdown = cx.new(|cx| Markdown::new(processed_message.into(), None, None, cx));
972964 Self {
973965 editor,
974966 multibuffer,
@@ -976,7 +968,6 @@ impl Item for CommitView { · 1 unmodified line
976968 stash: self.stash,
977969 repository: self.repository.clone(),
978970 remote: self.remote.clone(),
979− markdown,
980971 }
981972 })))
982973 }
@@ -1046,117 +1037,3 @@ fn stash_matches_index(sha: &str, stash_index: usize, repo: &Repository) -> bool · 63 unmodified lines
10461037 .map(|entry| entry.oid.to_string() == sha)
10471038 .unwrap_or(false)
10481039 }
1049−
1050−fn hover_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
1051− let colors = cx.theme().colors();
1052− let mut style = MarkdownStyle::default();
1053− style.base_text_style = window.text_style();
1054− style.syntax = cx.theme().syntax().clone();
1055− style.selection_background_color = colors.element_selection_background;
1056− style.link = TextStyleRefinement {
1057− color: Some(colors.text_accent),
1058− underline: Some(UnderlineStyle {
1059− thickness: px(1.0),
1060− color: Some(colors.text_accent),
1061− wavy: false,
1062− }),
1063− ..Default::default()
1064− };
1065− style
1066−}
1067−
1068−#[cfg(test)]
1069−mod tests {
1070− use super::*;
1071− use git_hosting_providers::Github;
1072−
1073− fn create_test_remote() -> GitRemote {
1074− GitRemote {
1075− host: Arc::new(Github::public_instance()),
1076− owner: "zed-industries".into(),
1077− repo: "zed".into(),
1078− }
1079− }
1080−
1081− #[test]
1082− fn test_process_github_issues_simple_issue_number() {
1083− let remote = create_test_remote();
1084− let message = "Fix bug #123";
1085− let result = CommitView::process_github_issues(message, &remote);
1086− assert_eq!(
1087− result,
1088− "Fix bug [#123](https://github.com/zed-industries/zed/issues/123)"
1089− );
1090− }
1091−
1092− #[test]
1093− fn test_process_github_issues_multiple_issue_numbers() {
1094− let remote = create_test_remote();
1095− let message = "Fix #123 and #456";
1096− let result = CommitView::process_github_issues(message, &remote);
1097− assert_eq!(
1098− result,
1099− "Fix [#123](https://github.com/zed-industries/zed/issues/123) and [#456](https://github.com/zed-industries/zed/issues/456)"
1100− );
1101− }
1102−
1103− #[test]
1104− fn test_process_github_issues_gh_format() {
1105− let remote = create_test_remote();
1106− let message = "Fix GH-789";
1107− let result = CommitView::process_github_issues(message, &remote);
1108− assert_eq!(
1109− result,
1110− "Fix [GH-789](https://github.com/zed-industries/zed/issues/789)"
1111− );
1112− }
1113−
1114− #[test]
1115− fn test_process_github_issues_mixed_formats() {
1116− let remote = create_test_remote();
1117− let message = "Fix #123 and GH-456";
1118− let result = CommitView::process_github_issues(message, &remote);
1119− assert_eq!(
1120− result,
1121− "Fix [#123](https://github.com/zed-industries/zed/issues/123) and [GH-456](https://github.com/zed-industries/zed/issues/456)"
1122− );
1123− }
1124−
1125− #[test]
1126− fn test_process_github_issues_no_issues() {
1127− let remote = create_test_remote();
1128− let message = "This is a commit message without any issues";
1129− let result = CommitView::process_github_issues(message, &remote);
1130− assert_eq!(result, message);
1131− }
1132−
1133− #[test]
1134− fn test_process_github_issues_hash_without_number() {
1135− let remote = create_test_remote();
1136− let message = "Use # for comments";
1137− let result = CommitView::process_github_issues(message, &remote);
1138− assert_eq!(result, message);
1139− }
1140−
1141− #[test]
1142− fn test_process_github_issues_consecutive_issues() {
1143− let remote = create_test_remote();
1144− let message = "#123#456";
1145− let result = CommitView::process_github_issues(message, &remote);
1146− assert_eq!(
1147− result,
1148− "[#123](https://github.com/zed-industries/zed/issues/123)[#456](https://github.com/zed-industries/zed/issues/456)"
1149− );
1150− }
1151−
1152− #[test]
1153− fn test_process_github_issues_multiline() {
1154− let remote = create_test_remote();
1155− let message = "Fix #123\n\nThis also fixes #456";
1156− let result = CommitView::process_github_issues(message, &remote);
1157− assert_eq!(
1158− result,
1159− "Fix [#123](https://github.com/zed-industries/zed/issues/123)\n\nThis also fixes [#456](https://github.com/zed-industries/zed/issues/456)"
1160− );
1161− }
1162−}