git_ui: Show full files by default in solo diffs (#60989)
# Objective Make solo diff views show the full file by default while preserving an easy way to focus on changed hunks. This addresses a recurring request across issues and pull requests for full-file context in single-file Git diffs. ## Solution - Initialize the solo diff with a singleton multibuffer so the entire file is visible on open. - Keep the existing toolbar toggle available for switching to changes-only excerpts. - Use the singleton path key when replacing excerpts so toggling remains reliable. - Preserve Git change indicators in the scrollbar; the editor generates those markers for singleton buffers. ## Testing - Ran `cargo fmt -p git_ui`. - Ran `git diff --check`. - Ran `cargo check -p git_ui --no-default-features` successfully. - Manually reviewed the full-file and changes-only state transitions in `SoloDiffView`. ## Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content adheres to Zed's UI standards ([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) and [icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md) guidelines) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable ## Showcase Before, solo diffs opened with only changed hunks and surrounding context. They now open with the complete file, retain Git change indicators in the scrollbar, and can still be toggled to changes-only from the toolbar. --- Release Notes: - Improved solo diffs to show the full file by default while retaining Git change indicators in the scrollbar.
This commit is contained in:
1 parent
1b67fdc7f4
commit
fcd85e1bfb
8 files changed
+178
-33
No files matched your search
@@ -1690,6 +1690,10 @@
|
||||
"branch_picker": {
|
||||
"show_author_name": true,
|
||||
},
|
||||
"file_diff": {
|
||||
// Whether newly opened file diffs show the full file instead of changes only.
|
||||
"show_full_file": true,
|
||||
},
|
||||
// How git hunks are displayed visually in the editor.
|
||||
// This setting can take two values:
|
||||
//
|
||||
|
||||
@@ -198,6 +198,16 @@ impl Editor {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn set_allow_git_diff_scrollbar_markers(&mut self, allow: bool, cx: &mut Context<Self>) {
|
||||
if self.allow_git_diff_scrollbar_markers != allow {
|
||||
self.allow_git_diff_scrollbar_markers = allow;
|
||||
self.scrollbar_marker_state.dirty = true;
|
||||
self.scrollbar_marker_state.markers = Default::default();
|
||||
self.scrollbar_marker_state.pending_refresh = None;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_show_code_actions(&mut self, show_code_actions: bool, cx: &mut Context<Self>) {
|
||||
self.show_code_actions = Some(show_code_actions);
|
||||
cx.notify();
|
||||
|
||||
@@ -999,6 +999,7 @@ pub struct Editor {
|
||||
background_highlights: HashMap<HighlightKey, BackgroundHighlight>,
|
||||
navigation_overlays: HashMap<NavigationOverlayKey, Arc<[NavigationTargetOverlay]>>,
|
||||
gutter_highlights: TypeIdHashMap<GutterHighlight>,
|
||||
allow_git_diff_scrollbar_markers: bool,
|
||||
scrollbar_marker_state: ScrollbarMarkerState,
|
||||
active_indent_guides_state: ActiveIndentGuidesState,
|
||||
nav_history: Option<ItemNavHistory>,
|
||||
@@ -2302,6 +2303,7 @@ impl Editor {
|
||||
background_highlights: HashMap::default(),
|
||||
navigation_overlays: HashMap::default(),
|
||||
gutter_highlights: Default::default(),
|
||||
allow_git_diff_scrollbar_markers: false,
|
||||
scrollbar_marker_state: ScrollbarMarkerState::default(),
|
||||
active_indent_guides_state: ActiveIndentGuidesState::default(),
|
||||
nav_history: None,
|
||||
|
||||
@@ -69,6 +69,15 @@ pub struct EditorSettings {
|
||||
pub completion_menu_item_kind: CompletionMenuItemKind,
|
||||
pub diff_view_style: DiffViewStyle,
|
||||
pub minimum_split_diff_width: f32,
|
||||
pub file_diff: FileDiffSettings,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct FileDiffSettings {
|
||||
/// Whether newly opened file diffs show the full file instead of changes only.
|
||||
///
|
||||
/// Default: true
|
||||
pub show_full_file: bool,
|
||||
}
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Jupyter {
|
||||
@@ -204,6 +213,7 @@ impl Settings for EditorSettings {
|
||||
let search = editor.search.unwrap();
|
||||
let drag_and_drop_selection = editor.drag_and_drop_selection.unwrap();
|
||||
let sticky_scroll = editor.sticky_scroll.unwrap();
|
||||
let file_diff = content.git.as_ref().unwrap().file_diff.unwrap();
|
||||
Self {
|
||||
cursor_blink: editor.cursor_blink.unwrap(),
|
||||
cursor_shape: editor.cursor_shape.map(Into::into),
|
||||
@@ -313,6 +323,9 @@ impl Settings for EditorSettings {
|
||||
completion_menu_item_kind: editor.completion_menu_item_kind.unwrap(),
|
||||
diff_view_style: editor.diff_view_style.unwrap(),
|
||||
minimum_split_diff_width: editor.minimum_split_diff_width.unwrap(),
|
||||
file_diff: FileDiffSettings {
|
||||
show_full_file: file_diff.show_full_file.unwrap(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1315,8 +1315,10 @@ impl EditorElement {
|
||||
ShowScrollbar::Auto => {
|
||||
let editor = self.editor.read(cx);
|
||||
let is_singleton = editor.buffer_kind(cx) == ItemBufferKind::Singleton;
|
||||
let supports_git_diff_markers =
|
||||
is_singleton || editor.allow_git_diff_scrollbar_markers;
|
||||
// Git
|
||||
(is_singleton && scrollbar_settings.git_diff && snapshot.buffer_snapshot().has_diff_hunks())
|
||||
(supports_git_diff_markers && scrollbar_settings.git_diff && snapshot.buffer_snapshot().has_diff_hunks())
|
||||
||
|
||||
// Buffer Search Results
|
||||
(is_singleton && scrollbar_settings.search_results && editor.has_background_highlights(HighlightKey::BufferSearchHighlights))
|
||||
@@ -6001,10 +6003,19 @@ impl EditorElement {
|
||||
cx: &mut App,
|
||||
) {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
if editor.buffer_kind(cx) != ItemBufferKind::Singleton
|
||||
|| !editor
|
||||
.scrollbar_marker_state
|
||||
.should_refresh(scrollbar_layout.hitbox.size)
|
||||
let is_singleton = editor.buffer_kind(cx) == ItemBufferKind::Singleton;
|
||||
let scrollbar_settings = EditorSettings::get_global(cx).scrollbar;
|
||||
let show_git_diff_markers = scrollbar_settings.git_diff
|
||||
&& (is_singleton || editor.allow_git_diff_scrollbar_markers);
|
||||
if !is_singleton && !show_git_diff_markers {
|
||||
editor.scrollbar_marker_state.dirty = true;
|
||||
editor.scrollbar_marker_state.markers = Default::default();
|
||||
editor.scrollbar_marker_state.pending_refresh = None;
|
||||
return;
|
||||
}
|
||||
if !editor
|
||||
.scrollbar_marker_state
|
||||
.should_refresh(scrollbar_layout.hitbox.size)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -6013,7 +6024,6 @@ impl EditorElement {
|
||||
let background_highlights = editor.background_highlights.clone();
|
||||
let snapshot = layout.position_map.snapshot.clone();
|
||||
let theme = cx.theme().clone();
|
||||
let scrollbar_settings = EditorSettings::get_global(cx).scrollbar;
|
||||
|
||||
editor.scrollbar_marker_state.dirty = false;
|
||||
editor.scrollbar_marker_state.pending_refresh =
|
||||
@@ -6023,7 +6033,7 @@ impl EditorElement {
|
||||
.background_spawn(async move {
|
||||
let max_point = snapshot.display_snapshot.buffer_snapshot().max_point();
|
||||
let mut marker_quads = Vec::new();
|
||||
if scrollbar_settings.git_diff {
|
||||
if show_git_diff_markers {
|
||||
let marker_row_ranges =
|
||||
snapshot.buffer_snapshot().diff_hunks().map(|hunk| {
|
||||
let start_display_row =
|
||||
@@ -6062,7 +6072,7 @@ impl EditorElement {
|
||||
}
|
||||
|
||||
for (background_highlight_id, (_, background_ranges)) in
|
||||
background_highlights.iter()
|
||||
background_highlights.iter().filter(|_| is_singleton)
|
||||
{
|
||||
let is_search_highlights = *background_highlight_id
|
||||
== HighlightKey::BufferSearchHighlights;
|
||||
@@ -6099,7 +6109,9 @@ impl EditorElement {
|
||||
}
|
||||
}
|
||||
|
||||
if scrollbar_settings.diagnostics != ScrollbarDiagnostics::None {
|
||||
if is_singleton
|
||||
&& scrollbar_settings.diagnostics != ScrollbarDiagnostics::None
|
||||
{
|
||||
let diagnostics = snapshot
|
||||
.buffer_snapshot()
|
||||
.diagnostics_in_range::<Point>(Point::zero()..max_point)
|
||||
|
||||
@@ -129,18 +129,9 @@ impl SoloDiffView {
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let repository_id = repository.read(cx).id;
|
||||
let multibuffer = cx.new(|cx| {
|
||||
let mut multibuffer = MultiBuffer::without_headers(buffer.read(cx).capability());
|
||||
multibuffer.set_excerpts_for_path(
|
||||
PathKey::for_buffer(&buffer, cx),
|
||||
buffer.clone(),
|
||||
Self::hunk_ranges(&buffer, &diff, cx),
|
||||
excerpt_context_lines(cx),
|
||||
cx,
|
||||
);
|
||||
multibuffer.add_diff(diff.clone(), cx);
|
||||
multibuffer
|
||||
});
|
||||
let showing_full_file = EditorSettings::get_global(cx).file_diff.show_full_file;
|
||||
let multibuffer = cx
|
||||
.new(|cx| Self::build_multibuffer(buffer.clone(), diff.clone(), showing_full_file, cx));
|
||||
let editor = cx.new(|cx| {
|
||||
let editor = SplittableEditor::new(
|
||||
EditorSettings::get_global(cx).diff_view_style,
|
||||
@@ -152,6 +143,7 @@ impl SoloDiffView {
|
||||
);
|
||||
editor.rhs_editor().update(cx, |editor, cx| {
|
||||
editor.set_should_serialize(false, cx);
|
||||
editor.set_allow_git_diff_scrollbar_markers(showing_full_file, cx);
|
||||
let snapshot = editor.snapshot(window, cx);
|
||||
editor.go_to_hunk_before_or_after_position(
|
||||
&snapshot,
|
||||
@@ -188,11 +180,42 @@ impl SoloDiffView {
|
||||
diff,
|
||||
editor,
|
||||
workspace: workspace.downgrade(),
|
||||
showing_full_file: false,
|
||||
showing_full_file,
|
||||
_settings_subscription: settings_subscription,
|
||||
}
|
||||
}
|
||||
|
||||
fn build_multibuffer(
|
||||
buffer: Entity<Buffer>,
|
||||
diff: Entity<buffer_diff::BufferDiff>,
|
||||
showing_full_file: bool,
|
||||
cx: &mut Context<MultiBuffer>,
|
||||
) -> MultiBuffer {
|
||||
let (ranges, context_line_count) =
|
||||
Self::excerpt_ranges(&buffer, &diff, showing_full_file, cx);
|
||||
|
||||
let mut multibuffer = MultiBuffer::without_headers(buffer.read(cx).capability());
|
||||
multibuffer.set_excerpts_for_buffer(buffer, ranges, context_line_count, cx);
|
||||
multibuffer.add_diff(diff, cx);
|
||||
multibuffer
|
||||
}
|
||||
|
||||
fn excerpt_ranges(
|
||||
buffer: &Entity<Buffer>,
|
||||
diff: &Entity<buffer_diff::BufferDiff>,
|
||||
showing_full_file: bool,
|
||||
cx: &App,
|
||||
) -> (Vec<Range<Point>>, u32) {
|
||||
if showing_full_file {
|
||||
(vec![Point::zero()..buffer.read(cx).max_point()], 0)
|
||||
} else {
|
||||
(
|
||||
Self::hunk_ranges(buffer, diff, cx),
|
||||
excerpt_context_lines(cx),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn hunk_ranges(
|
||||
buffer: &Entity<Buffer>,
|
||||
diff: &Entity<buffer_diff::BufferDiff>,
|
||||
@@ -215,17 +238,8 @@ impl SoloDiffView {
|
||||
return;
|
||||
}
|
||||
|
||||
let ranges = if showing_full_file {
|
||||
let buffer = self.buffer.read(cx);
|
||||
vec![Point::zero()..buffer.max_point()]
|
||||
} else {
|
||||
Self::hunk_ranges(&self.buffer, &self.diff, cx)
|
||||
};
|
||||
let context_line_count = if showing_full_file {
|
||||
0
|
||||
} else {
|
||||
excerpt_context_lines(cx)
|
||||
};
|
||||
let (ranges, context_line_count) =
|
||||
Self::excerpt_ranges(&self.buffer, &self.diff, showing_full_file, cx);
|
||||
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
let path = PathKey::for_buffer(&self.buffer, cx);
|
||||
@@ -238,6 +252,9 @@ impl SoloDiffView {
|
||||
self.diff.clone(),
|
||||
cx,
|
||||
);
|
||||
editor.rhs_editor().update(cx, |editor, cx| {
|
||||
editor.set_allow_git_diff_scrollbar_markers(showing_full_file, cx);
|
||||
});
|
||||
});
|
||||
|
||||
self.showing_full_file = showing_full_file;
|
||||
@@ -703,6 +720,47 @@ struct SoloDiffButtonStates {
|
||||
unstage_file: bool,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use gpui::TestAppContext;
|
||||
use multi_buffer::MultiBufferRow;
|
||||
|
||||
#[gpui::test]
|
||||
fn test_changes_only_multibuffer_has_one_buffer_and_expand_controls(cx: &mut TestAppContext) {
|
||||
let base_text = (0..20)
|
||||
.map(|line| format!("line {line}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
let current_text = base_text.replace("line 10", "changed line 10");
|
||||
let buffer = cx.new(|cx| Buffer::local(current_text, cx));
|
||||
let diff = cx.new(|cx| {
|
||||
buffer_diff::BufferDiff::new_with_base_text(
|
||||
&base_text,
|
||||
&buffer.read(cx).text_snapshot(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let multibuffer = cx.new(|cx| SoloDiffView::build_multibuffer(buffer, diff, false, cx));
|
||||
|
||||
let (is_singleton, buffer_count, has_expand_controls) =
|
||||
multibuffer.update(cx, |multibuffer, cx| {
|
||||
(
|
||||
multibuffer.is_singleton(),
|
||||
multibuffer.snapshot(cx).all_buffer_ids().count(),
|
||||
multibuffer
|
||||
.snapshot(cx)
|
||||
.row_infos(MultiBufferRow(0))
|
||||
.any(|row| row.expand_info.is_some()),
|
||||
)
|
||||
});
|
||||
|
||||
assert!(!is_singleton);
|
||||
assert_eq!(buffer_count, 1);
|
||||
assert!(has_expand_controls);
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for SoloDiffGitToolbar {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let Some(solo_diff) = self.solo_diff() else {
|
||||
|
||||
@@ -540,6 +540,8 @@ pub struct GitSettings {
|
||||
///
|
||||
/// Default: on
|
||||
pub branch_picker: Option<BranchPickerSettingsContent>,
|
||||
/// File diff settings.
|
||||
pub file_diff: Option<FileDiffSettingsContent>,
|
||||
/// How hunks are displayed visually in the editor.
|
||||
///
|
||||
/// Default: staged_hollow
|
||||
@@ -691,6 +693,16 @@ pub struct BranchPickerSettingsContent {
|
||||
pub show_author_name: Option<bool>,
|
||||
}
|
||||
|
||||
#[with_fallible_options]
|
||||
#[derive(Clone, Copy, PartialEq, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct FileDiffSettingsContent {
|
||||
/// Whether newly opened file diffs show the full file instead of changes only.
|
||||
///
|
||||
/// Default: true
|
||||
pub show_full_file: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone,
|
||||
Copy,
|
||||
|
||||
@@ -7875,6 +7875,39 @@ fn version_control_page() -> SettingsPage {
|
||||
]
|
||||
}
|
||||
|
||||
fn file_diff_section() -> [SettingsPageItem; 2] {
|
||||
[
|
||||
SettingsPageItem::SectionHeader("File Diff"),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "Show Full File by Default",
|
||||
description: "Whether newly opened file diffs show the full file instead of changes only.",
|
||||
field: Box::new(SettingField {
|
||||
organization_override: None,
|
||||
json_path: Some("git.file_diff.show_full_file"),
|
||||
pick: |settings_content| {
|
||||
settings_content
|
||||
.git
|
||||
.as_ref()?
|
||||
.file_diff
|
||||
.as_ref()?
|
||||
.show_full_file
|
||||
.as_ref()
|
||||
},
|
||||
write: |settings_content, value, _| {
|
||||
settings_content
|
||||
.git
|
||||
.get_or_insert_default()
|
||||
.file_diff
|
||||
.get_or_insert_default()
|
||||
.show_full_file = value;
|
||||
},
|
||||
}),
|
||||
metadata: None,
|
||||
files: USER,
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
SettingsPage {
|
||||
title: "Version Control",
|
||||
items: concat_sections![
|
||||
@@ -7883,6 +7916,7 @@ fn version_control_page() -> SettingsPage {
|
||||
inline_git_blame_section(),
|
||||
git_blame_view_section(),
|
||||
branch_picker_section(),
|
||||
file_diff_section(),
|
||||
git_hunks_section(),
|
||||
],
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user