Automatically switch to unified diffs when diff view is narrower than a configurable "minimum split diff width" (#52781)

Release Notes:

- The git diff diff view now automatically switches from split mode to
unified mode when the pane is narrower than a configurable minimum
column count. You can configure this via the new
`minimum_split_diff_width` setting.
This commit is contained in:
Max Brunsfeld authored and GitHub committed 2026-03-30 18:47:50 -07:00
1 parent e39d5c9908
commit fb87786375
7 files changed
+208 -107

No files matched your search

+2
View File
@@ -60,6 +60,7 @@ pub struct EditorSettings {
pub completion_menu_scrollbar: ShowScrollbar,
pub completion_detail_alignment: CompletionDetailAlignment,
pub diff_view_style: DiffViewStyle,
pub minimum_split_diff_width: f32,
}
#[derive(Debug, Clone)]
pub struct Jupyter {
@@ -294,6 +295,7 @@ impl Settings for EditorSettings {
.unwrap(),
completion_detail_alignment: editor.completion_detail_alignment.unwrap(),
diff_view_style: editor.diff_view_style.unwrap(),
minimum_split_diff_width: editor.minimum_split_diff_width.unwrap(),
}
}
}
+84 -20
View File
@@ -7,7 +7,8 @@ use buffer_diff::{BufferDiff, BufferDiffSnapshot};
use collections::HashMap;
use gpui::{
Action, AppContext as _, Entity, EventEmitter, Focusable, Font, Subscription, WeakEntity,
Action, AppContext as _, Entity, EventEmitter, Focusable, Font, Pixels, Subscription,
WeakEntity, canvas,
};
use itertools::Itertools;
use language::{Buffer, Capability, HighlightedText};
@@ -17,7 +18,7 @@ use multi_buffer::{
};
use project::Project;
use rope::Point;
use settings::DiffViewStyle;
use settings::{DiffViewStyle, Settings};
use text::{Bias, BufferId, OffsetRangeExt as _, Patch, ToPoint as _};
use ui::{
App, Context, InteractiveElement as _, IntoElement as _, ParentElement as _, Render,
@@ -36,7 +37,7 @@ use workspace::{
};
use crate::{
Autoscroll, Editor, EditorEvent, RenderDiffHunkControlsFn, ToggleSoftWrap,
Autoscroll, Editor, EditorEvent, EditorSettings, RenderDiffHunkControlsFn, ToggleSoftWrap,
actions::{DisableBreakpoint, EditLogBreakpoint, EnableBreakpoint, ToggleBreakpoint},
display_map::Companion,
};
@@ -377,6 +378,12 @@ pub struct SplittableEditor {
workspace: WeakEntity<Workspace>,
split_state: Entity<SplitEditorState>,
searched_side: Option<SplitSide>,
/// The preferred diff style.
diff_view_style: DiffViewStyle,
/// True when the current width is below the minimum threshold for split
/// mode, regardless of the current diff view style setting.
too_narrow_for_split: bool,
last_width: Option<Pixels>,
_subscriptions: Vec<Subscription>,
}
@@ -396,6 +403,10 @@ impl SplittableEditor {
self.lhs.as_ref().map(|s| &s.editor)
}
pub fn diff_view_style(&self) -> DiffViewStyle {
self.diff_view_style
}
pub fn is_split(&self) -> bool {
self.lhs.is_some()
}
@@ -499,12 +510,15 @@ impl SplittableEditor {
});
let split_state = cx.new(|cx| SplitEditorState::new(cx));
Self {
diff_view_style: style,
rhs_editor,
rhs_multibuffer,
lhs: None,
workspace: workspace.downgrade(),
split_state,
searched_side: None,
too_narrow_for_split: false,
last_width: None,
_subscriptions: subscriptions,
}
}
@@ -826,10 +840,19 @@ impl SplittableEditor {
window: &mut Window,
cx: &mut Context<Self>,
) {
if self.lhs.is_some() {
self.unsplit(window, cx);
} else {
self.split(window, cx);
match self.diff_view_style {
DiffViewStyle::Unified => {
self.diff_view_style = DiffViewStyle::Split;
if !self.too_narrow_for_split {
self.split(window, cx);
}
}
DiffViewStyle::Split => {
self.diff_view_style = DiffViewStyle::Unified;
if self.is_split() {
self.unsplit(window, cx);
}
}
}
}
@@ -1249,6 +1272,35 @@ impl SplittableEditor {
}
});
}
fn width_changed(&mut self, width: Pixels, window: &mut Window, cx: &mut Context<Self>) {
self.last_width = Some(width);
let min_ems = EditorSettings::get_global(cx).minimum_split_diff_width;
let style = self.rhs_editor.read(cx).create_style(cx);
let font_id = window.text_system().resolve_font(&style.text.font());
let font_size = style.text.font_size.to_pixels(window.rem_size());
let em_advance = window
.text_system()
.em_advance(font_id, font_size)
.unwrap_or(font_size);
let min_width = em_advance * min_ems;
let is_split = self.lhs.is_some();
self.too_narrow_for_split = min_ems > 0.0 && width < min_width;
match self.diff_view_style {
DiffViewStyle::Unified => {}
DiffViewStyle::Split => {
if self.too_narrow_for_split && is_split {
self.unsplit(window, cx);
} else if !self.too_narrow_for_split && !is_split {
self.split(window, cx);
}
}
}
}
}
#[cfg(test)]
@@ -2042,30 +2094,23 @@ impl Focusable for SplittableEditor {
}
}
// impl Item for SplittableEditor {
// type Event = EditorEvent;
// fn tab_content_text(&self, detail: usize, cx: &App) -> ui::SharedString {
// self.rhs_editor().tab_content_text(detail, cx)
// }
// fn as_searchable(&self, _this: &Entity<Self>, cx: &App) -> Option<Box<dyn workspace::searchable::SearchableItemHandle>> {
// Some(Box::new(self.last_selected_editor().clone()))
// }
// }
impl Render for SplittableEditor {
fn render(
&mut self,
_window: &mut ui::Window,
cx: &mut ui::Context<Self>,
) -> impl ui::IntoElement {
let inner = if self.lhs.is_some() {
let is_split = self.lhs.is_some();
let inner = if is_split {
let style = self.rhs_editor.read(cx).create_style(cx);
SplitEditorView::new(cx.entity(), style, self.split_state.clone()).into_any_element()
} else {
self.rhs_editor.clone().into_any_element()
};
let this = cx.entity().downgrade();
let last_width = self.last_width;
div()
.id("splittable-editor")
.on_action(cx.listener(Self::toggle_split))
@@ -2079,6 +2124,25 @@ impl Render for SplittableEditor {
.capture_action(cx.listener(Self::toggle_soft_wrap))
.size_full()
.child(inner)
.child(
canvas(
move |bounds, window, cx| {
let width = bounds.size.width;
if last_width == Some(width) {
return;
}
window.defer(cx, move |window, cx| {
this.update(cx, |this, cx| {
this.width_changed(width, window, cx);
})
.ok();
});
},
|_, _, _, _| {},
)
.absolute()
.size_full(),
)
}
}