diff --git a/assets/settings/default.json b/assets/settings/default.json index 563aeaf1a2..b7cf7019ef 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -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: // diff --git a/crates/editor/src/config.rs b/crates/editor/src/config.rs index bfda1aae44..f1a717db7c 100644 --- a/crates/editor/src/config.rs +++ b/crates/editor/src/config.rs @@ -198,6 +198,16 @@ impl Editor { cx.notify(); } + pub fn set_allow_git_diff_scrollbar_markers(&mut self, allow: bool, cx: &mut Context) { + 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.show_code_actions = Some(show_code_actions); cx.notify(); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 635521b460..f456d62a22 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -999,6 +999,7 @@ pub struct Editor { background_highlights: HashMap, navigation_overlays: HashMap>, gutter_highlights: TypeIdHashMap, + allow_git_diff_scrollbar_markers: bool, scrollbar_marker_state: ScrollbarMarkerState, active_indent_guides_state: ActiveIndentGuidesState, nav_history: Option, @@ -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, diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index ffda80c358..9e6cdcd44b 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -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(), + }, } } } diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index f7be01d96f..6fa72f155f 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -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::zero()..max_point) diff --git a/crates/git_ui/src/solo_diff_view.rs b/crates/git_ui/src/solo_diff_view.rs index 038bde7d07..9063056b4c 100644 --- a/crates/git_ui/src/solo_diff_view.rs +++ b/crates/git_ui/src/solo_diff_view.rs @@ -129,18 +129,9 @@ impl SoloDiffView { cx: &mut Context, ) -> 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, + diff: Entity, + showing_full_file: bool, + cx: &mut Context, + ) -> 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, + diff: &Entity, + showing_full_file: bool, + cx: &App, + ) -> (Vec>, 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, diff: &Entity, @@ -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::>() + .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) -> impl IntoElement { let Some(solo_diff) = self.solo_diff() else { diff --git a/crates/settings_content/src/project.rs b/crates/settings_content/src/project.rs index 7f4d8d5391..f178e59efc 100644 --- a/crates/settings_content/src/project.rs +++ b/crates/settings_content/src/project.rs @@ -540,6 +540,8 @@ pub struct GitSettings { /// /// Default: on pub branch_picker: Option, + /// File diff settings. + pub file_diff: Option, /// How hunks are displayed visually in the editor. /// /// Default: staged_hollow @@ -691,6 +693,16 @@ pub struct BranchPickerSettingsContent { pub show_author_name: Option, } +#[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, +} + #[derive( Clone, Copy, diff --git a/crates/settings_ui/src/page_data.rs b/crates/settings_ui/src/page_data.rs index 3c1aa9708e..13939e246b 100644 --- a/crates/settings_ui/src/page_data.rs +++ b/crates/settings_ui/src/page_data.rs @@ -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(), ], }