History

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.

Max Brunsfeld committed 7 months ago

Commit fb877861 parent e39d5c97 files · +208 −107Browse files

Unified · Split

7 files
  1. assets/settings/default.json +7 −0
  2. crates/editor/src/editor_settings.rs +2 −0
  3. crates/editor/src/split.rs +84 −20
  4. crates/search/src/buffer_search.rs +90 −86
  5. crates/settings/src/vscode_import.rs +1 −0
  6. crates/settings_content/src/editor.rs +8 −0
  7. crates/settings_ui/src/page_data.rs +16 −1
assets/settings/default.json+7−0
@@ -299,6 +299,13 @@ · 298 unmodified lines
299299 //
300300 // Default: split
301301 "diff_view_style": "split",
302+ // The minimum width (in em-widths) at which the split diff view is used.
303+ // When the editor is narrower than this, the diff view automatically
304+ // switches to unified mode and switches back when the editor is wide
305+ // enough. Set to 0 to disable automatic switching.
306+ //
307+ // Default: 100
308+ "minimum_split_diff_width": 100,
302309 // Show method signatures in the editor, when inside parentheses.
303310 "auto_signature_help": false,
304311 // Whether to show the signature help after completion or a bracket pair inserted.
crates/editor/src/editor_settings.rs+2−0
@@ -60,6 +60,7 @@ pub struct EditorSettings { · 59 unmodified lines
6060 pub completion_menu_scrollbar: ShowScrollbar,
6161 pub completion_detail_alignment: CompletionDetailAlignment,
6262 pub diff_view_style: DiffViewStyle,
63+ pub minimum_split_diff_width: f32,
6364 }
6465 #[derive(Debug, Clone)]
6566 pub struct Jupyter {
@@ -294,6 +295,7 @@ impl Settings for EditorSettings { · 228 unmodified lines
294295 .unwrap(),
295296 completion_detail_alignment: editor.completion_detail_alignment.unwrap(),
296297 diff_view_style: editor.diff_view_style.unwrap(),
298+ minimum_split_diff_width: editor.minimum_split_diff_width.unwrap(),
297299 }
298300 }
299301 }
crates/editor/src/split.rs+84−20
@@ -7,7 +7,8 @@ use buffer_diff::{BufferDiff, BufferDiffSnapshot}; · 6 unmodified lines
77 use collections::HashMap;
88
99 use gpui::{
10− Action, AppContext as _, Entity, EventEmitter, Focusable, Font, Subscription, WeakEntity,
10+ Action, AppContext as _, Entity, EventEmitter, Focusable, Font, Pixels, Subscription,
11+ WeakEntity, canvas,
1112 };
1213 use itertools::Itertools;
1314 use language::{Buffer, Capability, HighlightedText};
@@ -17,7 +18,7 @@ use multi_buffer::{ · 3 unmodified lines
1718 };
1819 use project::Project;
1920 use rope::Point;
20−use settings::DiffViewStyle;
21+use settings::{DiffViewStyle, Settings};
2122 use text::{Bias, BufferId, OffsetRangeExt as _, Patch, ToPoint as _};
2223 use ui::{
2324 App, Context, InteractiveElement as _, IntoElement as _, ParentElement as _, Render,
@@ -36,7 +37,7 @@ use workspace::{ · 12 unmodified lines
3637 };
3738
3839 use crate::{
39− Autoscroll, Editor, EditorEvent, RenderDiffHunkControlsFn, ToggleSoftWrap,
40+ Autoscroll, Editor, EditorEvent, EditorSettings, RenderDiffHunkControlsFn, ToggleSoftWrap,
4041 actions::{DisableBreakpoint, EditLogBreakpoint, EnableBreakpoint, ToggleBreakpoint},
4142 display_map::Companion,
4243 };
@@ -377,6 +378,12 @@ pub struct SplittableEditor { · 334 unmodified lines
377378 workspace: WeakEntity<Workspace>,
378379 split_state: Entity<SplitEditorState>,
379380 searched_side: Option<SplitSide>,
381+ /// The preferred diff style.
382+ diff_view_style: DiffViewStyle,
383+ /// True when the current width is below the minimum threshold for split
384+ /// mode, regardless of the current diff view style setting.
385+ too_narrow_for_split: bool,
386+ last_width: Option<Pixels>,
380387 _subscriptions: Vec<Subscription>,
381388 }
382389
@@ -396,6 +403,10 @@ impl SplittableEditor { · 13 unmodified lines
396403 self.lhs.as_ref().map(|s| &s.editor)
397404 }
398405
406+ pub fn diff_view_style(&self) -> DiffViewStyle {
407+ self.diff_view_style
408+ }
409+
399410 pub fn is_split(&self) -> bool {
400411 self.lhs.is_some()
401412 }
@@ -499,12 +510,15 @@ impl SplittableEditor { · 97 unmodified lines
499510 });
500511 let split_state = cx.new(|cx| SplitEditorState::new(cx));
501512 Self {
513+ diff_view_style: style,
502514 rhs_editor,
503515 rhs_multibuffer,
504516 lhs: None,
505517 workspace: workspace.downgrade(),
506518 split_state,
507519 searched_side: None,
520+ too_narrow_for_split: false,
521+ last_width: None,
508522 _subscriptions: subscriptions,
509523 }
510524 }
@@ -826,10 +840,19 @@ impl SplittableEditor { · 315 unmodified lines
826840 window: &mut Window,
827841 cx: &mut Context<Self>,
828842 ) {
829− if self.lhs.is_some() {
830− self.unsplit(window, cx);
831− } else {
832− self.split(window, cx);
843+ match self.diff_view_style {
844+ DiffViewStyle::Unified => {
845+ self.diff_view_style = DiffViewStyle::Split;
846+ if !self.too_narrow_for_split {
847+ self.split(window, cx);
848+ }
849+ }
850+ DiffViewStyle::Split => {
851+ self.diff_view_style = DiffViewStyle::Unified;
852+ if self.is_split() {
853+ self.unsplit(window, cx);
854+ }
855+ }
833856 }
834857 }
835858
@@ -1249,6 +1272,35 @@ impl SplittableEditor { · 413 unmodified lines
12491272 }
12501273 });
12511274 }
1275+
1276+ fn width_changed(&mut self, width: Pixels, window: &mut Window, cx: &mut Context<Self>) {
1277+ self.last_width = Some(width);
1278+
1279+ let min_ems = EditorSettings::get_global(cx).minimum_split_diff_width;
1280+
1281+ let style = self.rhs_editor.read(cx).create_style(cx);
1282+ let font_id = window.text_system().resolve_font(&style.text.font());
1283+ let font_size = style.text.font_size.to_pixels(window.rem_size());
1284+ let em_advance = window
1285+ .text_system()
1286+ .em_advance(font_id, font_size)
1287+ .unwrap_or(font_size);
1288+ let min_width = em_advance * min_ems;
1289+ let is_split = self.lhs.is_some();
1290+
1291+ self.too_narrow_for_split = min_ems > 0.0 && width < min_width;
1292+
1293+ match self.diff_view_style {
1294+ DiffViewStyle::Unified => {}
1295+ DiffViewStyle::Split => {
1296+ if self.too_narrow_for_split && is_split {
1297+ self.unsplit(window, cx);
1298+ } else if !self.too_narrow_for_split && !is_split {
1299+ self.split(window, cx);
1300+ }
1301+ }
1302+ }
1303+ }
12521304 }
12531305
12541306 #[cfg(test)]
@@ -2042,30 +2094,23 @@ impl Focusable for SplittableEditor { · 787 unmodified lines
20422094 }
20432095 }
20442096
2045−// impl Item for SplittableEditor {
2046−// type Event = EditorEvent;
2047−
2048−// fn tab_content_text(&self, detail: usize, cx: &App) -> ui::SharedString {
2049−// self.rhs_editor().tab_content_text(detail, cx)
2050−// }
2051−
2052−// fn as_searchable(&self, _this: &Entity<Self>, cx: &App) -> Option<Box<dyn workspace::searchable::SearchableItemHandle>> {
2053−// Some(Box::new(self.last_selected_editor().clone()))
2054−// }
2055−// }
2056−
20572097 impl Render for SplittableEditor {
20582098 fn render(
20592099 &mut self,
20602100 _window: &mut ui::Window,
20612101 cx: &mut ui::Context<Self>,
20622102 ) -> impl ui::IntoElement {
2063− let inner = if self.lhs.is_some() {
2103+ let is_split = self.lhs.is_some();
2104+ let inner = if is_split {
20642105 let style = self.rhs_editor.read(cx).create_style(cx);
20652106 SplitEditorView::new(cx.entity(), style, self.split_state.clone()).into_any_element()
20662107 } else {
20672108 self.rhs_editor.clone().into_any_element()
20682109 };
2110+
2111+ let this = cx.entity().downgrade();
2112+ let last_width = self.last_width;
2113+
20692114 div()
20702115 .id("splittable-editor")
20712116 .on_action(cx.listener(Self::toggle_split))
@@ -2079,6 +2124,25 @@ impl Render for SplittableEditor { · 7 unmodified lines
20792124 .capture_action(cx.listener(Self::toggle_soft_wrap))
20802125 .size_full()
20812126 .child(inner)
2127+ .child(
2128+ canvas(
2129+ move |bounds, window, cx| {
2130+ let width = bounds.size.width;
2131+ if last_width == Some(width) {
2132+ return;
2133+ }
2134+ window.defer(cx, move |window, cx| {
2135+ this.update(cx, |this, cx| {
2136+ this.width_changed(width, window, cx);
2137+ })
2138+ .ok();
2139+ });
2140+ },
2141+ |_, _, _, _| {},
2142+ )
2143+ .absolute()
2144+ .size_full(),
2145+ )
20822146 }
20832147 }
20842148
crates/search/src/buffer_search.rs+90−86
@@ -20,9 +20,9 @@ use editor::{ · 19 unmodified lines
2020 };
2121 use futures::channel::oneshot;
2222 use gpui::{
23− App, ClickEvent, Context, Entity, EventEmitter, Focusable, InteractiveElement as _,
24− IntoElement, KeyContext, ParentElement as _, Render, ScrollHandle, Styled, Subscription, Task,
25− WeakEntity, Window, div,
23+ Action as _, App, ClickEvent, Context, Entity, EventEmitter, Focusable,
24+ InteractiveElement as _, IntoElement, KeyContext, ParentElement as _, Render, ScrollHandle,
25+ Styled, Subscription, Task, WeakEntity, Window, div,
2626 };
2727 use language::{Language, LanguageRegistry};
2828 use project::{
@@ -33,7 +33,9 @@ use project::{ · 4 unmodified lines
3333 use fs::Fs;
3434 use settings::{DiffViewStyle, Settings, update_settings_file};
3535 use std::{any::TypeId, sync::Arc};
36−use zed_actions::{outline::ToggleOutline, workspace::CopyPath, workspace::CopyRelativePath};
36+use zed_actions::{
37+ OpenSettingsAt, outline::ToggleOutline, workspace::CopyPath, workspace::CopyRelativePath,
38+};
3739
3840 use ui::{
3941 BASE_REM_SIZE_IN_PX, IconButtonShape, PlatformStyle, TextSize, Tooltip, prelude::*,
@@ -110,96 +112,97 @@ impl Render for BufferSearchBar { · 70 unmodified lines
110112 .as_ref()
111113 .and_then(|weak| weak.upgrade())
112114 .map(|splittable_editor| {
113− let is_split = splittable_editor.read(cx).is_split();
115+ let editor_ref = splittable_editor.read(cx);
116+ let diff_view_style = editor_ref.diff_view_style();
117+ let is_split = editor_ref.is_split();
118+ let min_columns =
119+ EditorSettings::get_global(cx).minimum_split_diff_width as u32;
120+
121+ let mut split_button = IconButton::new("diff-split", IconName::DiffSplit)
122+ .shape(IconButtonShape::Square)
123+ .tooltip(Tooltip::element(move |_, cx| {
124+ let message = if min_columns == 0 {
125+ SharedString::from("Split")
126+ } else {
127+ format!("Split when wider than {} columns", min_columns).into()
128+ };
129+
130+ v_flex()
131+ .child(message)
132+ .child(
133+ h_flex()
134+ .gap_0p5()
135+ .text_ui_sm(cx)
136+ .text_color(Color::Muted.color(cx))
137+ .children(render_modifiers(
138+ &gpui::Modifiers::secondary_key(),
139+ PlatformStyle::platform(),
140+ None,
141+ Some(TextSize::Small.rems(cx).into()),
142+ false,
143+ ))
144+ .child("click to change min width"),
145+ )
146+ .into_any()
147+ }))
148+ .on_click({
149+ let splittable_editor = splittable_editor.downgrade();
150+ move |_, window, cx| {
151+ if window.modifiers().secondary() {
152+ window.dispatch_action(
153+ OpenSettingsAt {
154+ path: "minimum_split_diff_width".to_string(),
155+ }
156+ .boxed_clone(),
157+ cx,
158+ );
159+ } else {
160+ update_settings_file(
161+ <dyn Fs>::global(cx),
162+ cx,
163+ |settings, _| {
164+ settings.editor.diff_view_style =
165+ Some(DiffViewStyle::Split);
166+ },
167+ );
168+ if diff_view_style == DiffViewStyle::Unified {
169+ splittable_editor
170+ .update(cx, |editor, cx| {
171+ editor.toggle_split(&ToggleSplitDiff, window, cx);
172+ })
173+ .ok();
174+ }
175+ }
176+ }
177+ });
178+
179+ if diff_view_style == DiffViewStyle::Split {
180+ if !is_split {
181+ split_button = split_button.icon_color(Color::Disabled)
182+ } else {
183+ split_button = split_button.toggle_state(true)
184+ }
185+ }
186+
114187 h_flex()
115188 .gap_1()
116189 .child(
117190 IconButton::new("diff-unified", IconName::DiffUnified)
118191 .shape(IconButtonShape::Square)
119− .toggle_state(!is_split)
120− .tooltip(Tooltip::element(move |_, cx| {
121− v_flex()
122− .child("Unified")
123− .child(
124− h_flex()
125− .gap_0p5()
126− .text_ui_sm(cx)
127− .text_color(Color::Muted.color(cx))
128− .children(render_modifiers(
129− &gpui::Modifiers::secondary_key(),
130− PlatformStyle::platform(),
131− None,
132− Some(TextSize::Small.rems(cx).into()),
133− false,
134− ))
135− .child("click to set as default"),
136− )
137− .into_any()
138− }))
139− .on_click({
140− let splittable_editor = splittable_editor.downgrade();
141− move |_, window, cx| {
142− if window.modifiers().secondary() {
143− update_settings_file(
144− <dyn Fs>::global(cx),
145− cx,
146− |settings, _| {
147− settings.editor.diff_view_style =
148− Some(DiffViewStyle::Unified);
149− },
150− );
151− }
152− if is_split {
153− splittable_editor
154− .update(cx, |editor, cx| {
155− editor.toggle_split(
156− &ToggleSplitDiff,
157− window,
158− cx,
159− );
160− })
161− .ok();
162− }
163− }
164− }),
165− )
166− .child(
167− IconButton::new("diff-split", IconName::DiffSplit)
168− .shape(IconButtonShape::Square)
169− .toggle_state(is_split)
170− .tooltip(Tooltip::element(move |_, cx| {
171− v_flex()
172− .child("Split")
173− .child(
174− h_flex()
175− .gap_0p5()
176− .text_ui_sm(cx)
177− .text_color(Color::Muted.color(cx))
178− .children(render_modifiers(
179− &gpui::Modifiers::secondary_key(),
180− PlatformStyle::platform(),
181− None,
182− Some(TextSize::Small.rems(cx).into()),
183− false,
184− ))
185− .child("click to set as default"),
186− )
187− .into_any()
188− }))
192+ .toggle_state(diff_view_style == DiffViewStyle::Unified)
193+ .tooltip(Tooltip::text("Unified"))
189194 .on_click({
190195 let splittable_editor = splittable_editor.downgrade();
191196 move |_, window, cx| {
192− if window.modifiers().secondary() {
193− update_settings_file(
194− <dyn Fs>::global(cx),
195− cx,
196− |settings, _| {
197− settings.editor.diff_view_style =
198− Some(DiffViewStyle::Split);
199− },
200− );
201− }
202− if !is_split {
197+ update_settings_file(
198+ <dyn Fs>::global(cx),
199+ cx,
200+ |settings, _| {
201+ settings.editor.diff_view_style =
202+ Some(DiffViewStyle::Unified);
203+ },
204+ );
205+ if diff_view_style == DiffViewStyle::Split {
203206 splittable_editor
204207 .update(cx, |editor, cx| {
205208 editor.toggle_split(
@@ -213,6 +216,7 @@ impl Render for BufferSearchBar { · 7 unmodified lines
213216 }
214217 }),
215218 )
219+ .child(split_button)
216220 })
217221 } else {
218222 None
crates/settings/src/vscode_import.rs+1−0
@@ -308,6 +308,7 @@ impl VsCodeSettings { · 307 unmodified lines
308308 completion_menu_scrollbar: None,
309309 completion_detail_alignment: None,
310310 diff_view_style: None,
311+ minimum_split_diff_width: None,
311312 }
312313 }
313314
crates/settings_content/src/editor.rs+8−0
@@ -226,6 +226,14 @@ pub struct EditorSettingsContent { · 225 unmodified lines
226226 ///
227227 /// Default: split
228228 pub diff_view_style: Option<DiffViewStyle>,
229+
230+ /// The minimum width (in em-widths) at which the split diff view is used.
231+ /// When the editor is narrower than this, the diff view automatically
232+ /// switches to unified mode and switches back when the editor is wide
233+ /// enough. Set to 0 to disable automatic switching.
234+ ///
235+ /// Default: 100
236+ pub minimum_split_diff_width: Option<f32>,
229237 }
230238
231239 #[derive(
crates/settings_ui/src/page_data.rs+16−1
@@ -1474,7 +1474,7 @@ fn editor_page() -> SettingsPage { · 1473 unmodified lines
14741474 ]
14751475 }
14761476
1477− fn multibuffer_section() -> [SettingsPageItem; 6] {
1477+ fn multibuffer_section() -> [SettingsPageItem; 7] {
14781478 [
14791479 SettingsPageItem::SectionHeader("Multibuffer"),
14801480 SettingsPageItem::SettingItem(SettingItem {
@@ -1554,6 +1554,21 @@ fn editor_page() -> SettingsPage { · 73 unmodified lines
15541554 metadata: None,
15551555 files: USER,
15561556 }),
1557+ SettingsPageItem::SettingItem(SettingItem {
1558+ title: "Minimum Split Diff Width",
1559+ description: "The minimum width (in columns) at which the split diff view is used. When the editor is narrower, the diff view automatically switches to unified mode. Set to 0 to disable.",
1560+ field: Box::new(SettingField {
1561+ json_path: Some("minimum_split_diff_width"),
1562+ pick: |settings_content| {
1563+ settings_content.editor.minimum_split_diff_width.as_ref()
1564+ },
1565+ write: |settings_content, value| {
1566+ settings_content.editor.minimum_split_diff_width = value;
1567+ },
1568+ }),
1569+ metadata: None,
1570+ files: USER,
1571+ }),
15571572 ]
15581573 }
15591574