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
299 //299 //
300 // Default: split300 // Default: split
301 "diff_view_style": "split",301 "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,
302 // Show method signatures in the editor, when inside parentheses.309 // Show method signatures in the editor, when inside parentheses.
303 "auto_signature_help": false,310 "auto_signature_help": false,
304 // Whether to show the signature help after completion or a bracket pair inserted.311 // 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
60 pub completion_menu_scrollbar: ShowScrollbar,60 pub completion_menu_scrollbar: ShowScrollbar,
61 pub completion_detail_alignment: CompletionDetailAlignment,61 pub completion_detail_alignment: CompletionDetailAlignment,
62 pub diff_view_style: DiffViewStyle,62 pub diff_view_style: DiffViewStyle,
63 pub minimum_split_diff_width: f32,
63}64}
64#[derive(Debug, Clone)]65#[derive(Debug, Clone)]
65pub struct Jupyter {66pub struct Jupyter {
@@ -294,6 +295,7 @@ impl Settings for EditorSettings { · 228 unmodified lines
294 .unwrap(),295 .unwrap(),
295 completion_detail_alignment: editor.completion_detail_alignment.unwrap(),296 completion_detail_alignment: editor.completion_detail_alignment.unwrap(),
296 diff_view_style: editor.diff_view_style.unwrap(),297 diff_view_style: editor.diff_view_style.unwrap(),
298 minimum_split_diff_width: editor.minimum_split_diff_width.unwrap(),
297 }299 }
298 }300 }
299}301}
crates/editor/src/split.rs+84−20
@@ -7,7 +7,8 @@ use buffer_diff::{BufferDiff, BufferDiffSnapshot}; · 6 unmodified lines
7use collections::HashMap;7use collections::HashMap;
8 8
9use gpui::{9use gpui::{
10 Action, AppContext as _, Entity, EventEmitter, Focusable, Font, Subscription, WeakEntity,10 Action, AppContext as _, Entity, EventEmitter, Focusable, Font, Pixels, Subscription,
11 WeakEntity, canvas,
11};12};
12use itertools::Itertools;13use itertools::Itertools;
13use language::{Buffer, Capability, HighlightedText};14use language::{Buffer, Capability, HighlightedText};
@@ -17,7 +18,7 @@ use multi_buffer::{ · 3 unmodified lines
17};18};
18use project::Project;19use project::Project;
19use rope::Point;20use rope::Point;
20use settings::DiffViewStyle;21use settings::{DiffViewStyle, Settings};
21use text::{Bias, BufferId, OffsetRangeExt as _, Patch, ToPoint as _};22use text::{Bias, BufferId, OffsetRangeExt as _, Patch, ToPoint as _};
22use ui::{23use ui::{
23 App, Context, InteractiveElement as _, IntoElement as _, ParentElement as _, Render,24 App, Context, InteractiveElement as _, IntoElement as _, ParentElement as _, Render,
@@ -36,7 +37,7 @@ use workspace::{ · 12 unmodified lines
36};37};
37 38
38use crate::{39use crate::{
39 Autoscroll, Editor, EditorEvent, RenderDiffHunkControlsFn, ToggleSoftWrap,40 Autoscroll, Editor, EditorEvent, EditorSettings, RenderDiffHunkControlsFn, ToggleSoftWrap,
40 actions::{DisableBreakpoint, EditLogBreakpoint, EnableBreakpoint, ToggleBreakpoint},41 actions::{DisableBreakpoint, EditLogBreakpoint, EnableBreakpoint, ToggleBreakpoint},
41 display_map::Companion,42 display_map::Companion,
42};43};
@@ -377,6 +378,12 @@ pub struct SplittableEditor { · 334 unmodified lines
377 workspace: WeakEntity<Workspace>,378 workspace: WeakEntity<Workspace>,
378 split_state: Entity<SplitEditorState>,379 split_state: Entity<SplitEditorState>,
379 searched_side: Option<SplitSide>,380 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>,
380 _subscriptions: Vec<Subscription>,387 _subscriptions: Vec<Subscription>,
381}388}
382 389
@@ -396,6 +403,10 @@ impl SplittableEditor { · 13 unmodified lines
396 self.lhs.as_ref().map(|s| &s.editor)403 self.lhs.as_ref().map(|s| &s.editor)
397 }404 }
398 405
406 pub fn diff_view_style(&self) -> DiffViewStyle {
407 self.diff_view_style
408 }
409
399 pub fn is_split(&self) -> bool {410 pub fn is_split(&self) -> bool {
400 self.lhs.is_some()411 self.lhs.is_some()
401 }412 }
@@ -499,12 +510,15 @@ impl SplittableEditor { · 97 unmodified lines
499 });510 });
500 let split_state = cx.new(|cx| SplitEditorState::new(cx));511 let split_state = cx.new(|cx| SplitEditorState::new(cx));
501 Self {512 Self {
513 diff_view_style: style,
502 rhs_editor,514 rhs_editor,
503 rhs_multibuffer,515 rhs_multibuffer,
504 lhs: None,516 lhs: None,
505 workspace: workspace.downgrade(),517 workspace: workspace.downgrade(),
506 split_state,518 split_state,
507 searched_side: None,519 searched_side: None,
520 too_narrow_for_split: false,
521 last_width: None,
508 _subscriptions: subscriptions,522 _subscriptions: subscriptions,
509 }523 }
510 }524 }
@@ -826,10 +840,19 @@ impl SplittableEditor { · 315 unmodified lines
826 window: &mut Window,840 window: &mut Window,
827 cx: &mut Context<Self>,841 cx: &mut Context<Self>,
828 ) {842 ) {
829 if self.lhs.is_some() {843 match self.diff_view_style {
830 self.unsplit(window, cx);844 DiffViewStyle::Unified => {
831 } else {845 self.diff_view_style = DiffViewStyle::Split;
832 self.split(window, cx);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 }
833 }856 }
834 }857 }
835 858
@@ -1249,6 +1272,35 @@ impl SplittableEditor { · 413 unmodified lines
1249 }1272 }
1250 });1273 });
1251 }1274 }
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 }
1252}1304}
1253 1305
1254#[cfg(test)]1306#[cfg(test)]
@@ -2042,30 +2094,23 @@ impl Focusable for SplittableEditor { · 787 unmodified lines
2042 }2094 }
2043}2095}
2044 2096
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
2057impl Render for SplittableEditor {2097impl Render for SplittableEditor {
2058 fn render(2098 fn render(
2059 &mut self,2099 &mut self,
2060 _window: &mut ui::Window,2100 _window: &mut ui::Window,
2061 cx: &mut ui::Context<Self>,2101 cx: &mut ui::Context<Self>,
2062 ) -> impl ui::IntoElement {2102 ) -> 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 {
2064 let style = self.rhs_editor.read(cx).create_style(cx);2105 let style = self.rhs_editor.read(cx).create_style(cx);
2065 SplitEditorView::new(cx.entity(), style, self.split_state.clone()).into_any_element()2106 SplitEditorView::new(cx.entity(), style, self.split_state.clone()).into_any_element()
2066 } else {2107 } else {
2067 self.rhs_editor.clone().into_any_element()2108 self.rhs_editor.clone().into_any_element()
2068 };2109 };
2110
2111 let this = cx.entity().downgrade();
2112 let last_width = self.last_width;
2113
2069 div()2114 div()
2070 .id("splittable-editor")2115 .id("splittable-editor")
2071 .on_action(cx.listener(Self::toggle_split))2116 .on_action(cx.listener(Self::toggle_split))
@@ -2079,6 +2124,25 @@ impl Render for SplittableEditor { · 7 unmodified lines
2079 .capture_action(cx.listener(Self::toggle_soft_wrap))2124 .capture_action(cx.listener(Self::toggle_soft_wrap))
2080 .size_full()2125 .size_full()
2081 .child(inner)2126 .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 )
2082 }2146 }
2083}2147}
2084 2148
crates/search/src/buffer_search.rs+90−86
@@ -20,9 +20,9 @@ use editor::{ · 19 unmodified lines
20};20};
21use futures::channel::oneshot;21use futures::channel::oneshot;
22use gpui::{22use gpui::{
23 App, ClickEvent, Context, Entity, EventEmitter, Focusable, InteractiveElement as _,23 Action as _, App, ClickEvent, Context, Entity, EventEmitter, Focusable,
24 IntoElement, KeyContext, ParentElement as _, Render, ScrollHandle, Styled, Subscription, Task,24 InteractiveElement as _, IntoElement, KeyContext, ParentElement as _, Render, ScrollHandle,
25 WeakEntity, Window, div,25 Styled, Subscription, Task, WeakEntity, Window, div,
26};26};
27use language::{Language, LanguageRegistry};27use language::{Language, LanguageRegistry};
28use project::{28use project::{
@@ -33,7 +33,9 @@ use project::{ · 4 unmodified lines
33use fs::Fs;33use fs::Fs;
34use settings::{DiffViewStyle, Settings, update_settings_file};34use settings::{DiffViewStyle, Settings, update_settings_file};
35use std::{any::TypeId, sync::Arc};35use std::{any::TypeId, sync::Arc};
36use zed_actions::{outline::ToggleOutline, workspace::CopyPath, workspace::CopyRelativePath};36use zed_actions::{
37 OpenSettingsAt, outline::ToggleOutline, workspace::CopyPath, workspace::CopyRelativePath,
38};
37 39
38use ui::{40use ui::{
39 BASE_REM_SIZE_IN_PX, IconButtonShape, PlatformStyle, TextSize, Tooltip, prelude::*,41 BASE_REM_SIZE_IN_PX, IconButtonShape, PlatformStyle, TextSize, Tooltip, prelude::*,
@@ -110,96 +112,97 @@ impl Render for BufferSearchBar { · 70 unmodified lines
110 .as_ref()112 .as_ref()
111 .and_then(|weak| weak.upgrade())113 .and_then(|weak| weak.upgrade())
112 .map(|splittable_editor| {114 .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
114 h_flex()187 h_flex()
115 .gap_1()188 .gap_1()
116 .child(189 .child(
117 IconButton::new("diff-unified", IconName::DiffUnified)190 IconButton::new("diff-unified", IconName::DiffUnified)
118 .shape(IconButtonShape::Square)191 .shape(IconButtonShape::Square)
119 .toggle_state(!is_split)192 .toggle_state(diff_view_style == DiffViewStyle::Unified)
120 .tooltip(Tooltip::element(move |_, cx| {193 .tooltip(Tooltip::text("Unified"))
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 }))
189 .on_click({194 .on_click({
190 let splittable_editor = splittable_editor.downgrade();195 let splittable_editor = splittable_editor.downgrade();
191 move |_, window, cx| {196 move |_, window, cx| {
192 if window.modifiers().secondary() {197 update_settings_file(
193 update_settings_file(198 <dyn Fs>::global(cx),
194 <dyn Fs>::global(cx),199 cx,
195 cx,200 |settings, _| {
196 |settings, _| {201 settings.editor.diff_view_style =
197 settings.editor.diff_view_style =202 Some(DiffViewStyle::Unified);
198 Some(DiffViewStyle::Split);203 },
199 },204 );
200 );205 if diff_view_style == DiffViewStyle::Split {
201 }
202 if !is_split {
203 splittable_editor206 splittable_editor
204 .update(cx, |editor, cx| {207 .update(cx, |editor, cx| {
205 editor.toggle_split(208 editor.toggle_split(
@@ -213,6 +216,7 @@ impl Render for BufferSearchBar { · 7 unmodified lines
213 }216 }
214 }),217 }),
215 )218 )
219 .child(split_button)
216 })220 })
217 } else {221 } else {
218 None222 None
crates/settings/src/vscode_import.rs+1−0
@@ -308,6 +308,7 @@ impl VsCodeSettings { · 307 unmodified lines
308 completion_menu_scrollbar: None,308 completion_menu_scrollbar: None,
309 completion_detail_alignment: None,309 completion_detail_alignment: None,
310 diff_view_style: None,310 diff_view_style: None,
311 minimum_split_diff_width: None,
311 }312 }
312 }313 }
313 314
crates/settings_content/src/editor.rs+8−0
@@ -226,6 +226,14 @@ pub struct EditorSettingsContent { · 225 unmodified lines
226 ///226 ///
227 /// Default: split227 /// Default: split
228 pub diff_view_style: Option<DiffViewStyle>,228 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>,
229}237}
230 238
231#[derive(239#[derive(
crates/settings_ui/src/page_data.rs+16−1
@@ -1474,7 +1474,7 @@ fn editor_page() -> SettingsPage { · 1473 unmodified lines
1474 ]1474 ]
1475 }1475 }
1476 1476
1477 fn multibuffer_section() -> [SettingsPageItem; 6] {1477 fn multibuffer_section() -> [SettingsPageItem; 7] {
1478 [1478 [
1479 SettingsPageItem::SectionHeader("Multibuffer"),1479 SettingsPageItem::SectionHeader("Multibuffer"),
1480 SettingsPageItem::SettingItem(SettingItem {1480 SettingsPageItem::SettingItem(SettingItem {
@@ -1554,6 +1554,21 @@ fn editor_page() -> SettingsPage { · 73 unmodified lines
1554 metadata: None,1554 metadata: None,
1555 files: USER,1555 files: USER,
1556 }),1556 }),
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 }),
1557 ]1572 ]
1558 }1573 }
1559 1574