diff --git a/assets/settings/default.json b/assets/settings/default.json index 6c0b4e1eca294c96bca445fd4fbed258c8cc969a..d3defb428c68120e89c6bc6cc82488f03a06b320 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -299,6 +299,13 @@ // // Default: split "diff_view_style": "split", + // The minimum width (in em-widths) at which the split diff view is used. + // When the editor is narrower than this, the diff view automatically + // switches to unified mode and switches back when the editor is wide + // enough. Set to 0 to disable automatic switching. + // + // Default: 100 + "minimum_split_diff_width": 100, // Show method signatures in the editor, when inside parentheses. "auto_signature_help": false, // Whether to show the signature help after completion or a bracket pair inserted. diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index fbb2ac703bf8f0617c7fd5e0918821bdbe951d41..e4a20476419578ff646952c84b399e2333f0a411 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -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(), } } } diff --git a/crates/editor/src/split.rs b/crates/editor/src/split.rs index 61f4526f3d4902f7972b65266de725818accef1e..cdb016ea4b612aaae288acd008f745ef2ecf0f1d 100644 --- a/crates/editor/src/split.rs +++ b/crates/editor/src/split.rs @@ -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, split_state: Entity, searched_side: Option, + /// 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, _subscriptions: Vec, } @@ -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, ) { - 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.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, cx: &App) -> Option> { -// Some(Box::new(self.last_selected_editor().clone())) -// } -// } - impl Render for SplittableEditor { fn render( &mut self, _window: &mut ui::Window, cx: &mut ui::Context, ) -> 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(), + ) } } diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 8a8537337db66cd5ab1d53404639a5103cccb7f2..cab8e20cd22e1f4155232f36416be77d4f2ca24d 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -20,9 +20,9 @@ use editor::{ }; use futures::channel::oneshot; use gpui::{ - App, ClickEvent, Context, Entity, EventEmitter, Focusable, InteractiveElement as _, - IntoElement, KeyContext, ParentElement as _, Render, ScrollHandle, Styled, Subscription, Task, - WeakEntity, Window, div, + Action as _, App, ClickEvent, Context, Entity, EventEmitter, Focusable, + InteractiveElement as _, IntoElement, KeyContext, ParentElement as _, Render, ScrollHandle, + Styled, Subscription, Task, WeakEntity, Window, div, }; use language::{Language, LanguageRegistry}; use project::{ @@ -33,7 +33,9 @@ use project::{ use fs::Fs; use settings::{DiffViewStyle, Settings, update_settings_file}; use std::{any::TypeId, sync::Arc}; -use zed_actions::{outline::ToggleOutline, workspace::CopyPath, workspace::CopyRelativePath}; +use zed_actions::{ + OpenSettingsAt, outline::ToggleOutline, workspace::CopyPath, workspace::CopyRelativePath, +}; use ui::{ BASE_REM_SIZE_IN_PX, IconButtonShape, PlatformStyle, TextSize, Tooltip, prelude::*, @@ -110,96 +112,97 @@ impl Render for BufferSearchBar { .as_ref() .and_then(|weak| weak.upgrade()) .map(|splittable_editor| { - let is_split = splittable_editor.read(cx).is_split(); + let editor_ref = splittable_editor.read(cx); + let diff_view_style = editor_ref.diff_view_style(); + let is_split = editor_ref.is_split(); + let min_columns = + EditorSettings::get_global(cx).minimum_split_diff_width as u32; + + let mut split_button = IconButton::new("diff-split", IconName::DiffSplit) + .shape(IconButtonShape::Square) + .tooltip(Tooltip::element(move |_, cx| { + let message = if min_columns == 0 { + SharedString::from("Split") + } else { + format!("Split when wider than {} columns", min_columns).into() + }; + + v_flex() + .child(message) + .child( + h_flex() + .gap_0p5() + .text_ui_sm(cx) + .text_color(Color::Muted.color(cx)) + .children(render_modifiers( + &gpui::Modifiers::secondary_key(), + PlatformStyle::platform(), + None, + Some(TextSize::Small.rems(cx).into()), + false, + )) + .child("click to change min width"), + ) + .into_any() + })) + .on_click({ + let splittable_editor = splittable_editor.downgrade(); + move |_, window, cx| { + if window.modifiers().secondary() { + window.dispatch_action( + OpenSettingsAt { + path: "minimum_split_diff_width".to_string(), + } + .boxed_clone(), + cx, + ); + } else { + update_settings_file( + ::global(cx), + cx, + |settings, _| { + settings.editor.diff_view_style = + Some(DiffViewStyle::Split); + }, + ); + if diff_view_style == DiffViewStyle::Unified { + splittable_editor + .update(cx, |editor, cx| { + editor.toggle_split(&ToggleSplitDiff, window, cx); + }) + .ok(); + } + } + } + }); + + if diff_view_style == DiffViewStyle::Split { + if !is_split { + split_button = split_button.icon_color(Color::Disabled) + } else { + split_button = split_button.toggle_state(true) + } + } + h_flex() .gap_1() .child( IconButton::new("diff-unified", IconName::DiffUnified) .shape(IconButtonShape::Square) - .toggle_state(!is_split) - .tooltip(Tooltip::element(move |_, cx| { - v_flex() - .child("Unified") - .child( - h_flex() - .gap_0p5() - .text_ui_sm(cx) - .text_color(Color::Muted.color(cx)) - .children(render_modifiers( - &gpui::Modifiers::secondary_key(), - PlatformStyle::platform(), - None, - Some(TextSize::Small.rems(cx).into()), - false, - )) - .child("click to set as default"), - ) - .into_any() - })) - .on_click({ - let splittable_editor = splittable_editor.downgrade(); - move |_, window, cx| { - if window.modifiers().secondary() { - update_settings_file( - ::global(cx), - cx, - |settings, _| { - settings.editor.diff_view_style = - Some(DiffViewStyle::Unified); - }, - ); - } - if is_split { - splittable_editor - .update(cx, |editor, cx| { - editor.toggle_split( - &ToggleSplitDiff, - window, - cx, - ); - }) - .ok(); - } - } - }), - ) - .child( - IconButton::new("diff-split", IconName::DiffSplit) - .shape(IconButtonShape::Square) - .toggle_state(is_split) - .tooltip(Tooltip::element(move |_, cx| { - v_flex() - .child("Split") - .child( - h_flex() - .gap_0p5() - .text_ui_sm(cx) - .text_color(Color::Muted.color(cx)) - .children(render_modifiers( - &gpui::Modifiers::secondary_key(), - PlatformStyle::platform(), - None, - Some(TextSize::Small.rems(cx).into()), - false, - )) - .child("click to set as default"), - ) - .into_any() - })) + .toggle_state(diff_view_style == DiffViewStyle::Unified) + .tooltip(Tooltip::text("Unified")) .on_click({ let splittable_editor = splittable_editor.downgrade(); move |_, window, cx| { - if window.modifiers().secondary() { - update_settings_file( - ::global(cx), - cx, - |settings, _| { - settings.editor.diff_view_style = - Some(DiffViewStyle::Split); - }, - ); - } - if !is_split { + update_settings_file( + ::global(cx), + cx, + |settings, _| { + settings.editor.diff_view_style = + Some(DiffViewStyle::Unified); + }, + ); + if diff_view_style == DiffViewStyle::Split { splittable_editor .update(cx, |editor, cx| { editor.toggle_split( @@ -213,6 +216,7 @@ impl Render for BufferSearchBar { } }), ) + .child(split_button) }) } else { None diff --git a/crates/settings/src/vscode_import.rs b/crates/settings/src/vscode_import.rs index fa6a9afbce1ef67097b11435f9481a133a0d563a..8cb596d46e0bd21358043553252c187c6bbd1202 100644 --- a/crates/settings/src/vscode_import.rs +++ b/crates/settings/src/vscode_import.rs @@ -308,6 +308,7 @@ impl VsCodeSettings { completion_menu_scrollbar: None, completion_detail_alignment: None, diff_view_style: None, + minimum_split_diff_width: None, } } diff --git a/crates/settings_content/src/editor.rs b/crates/settings_content/src/editor.rs index 4d824e85e0e2ee020f48cdddb530bf494b2ce800..b37192882694f999a5e7f3180e5a7899a8732393 100644 --- a/crates/settings_content/src/editor.rs +++ b/crates/settings_content/src/editor.rs @@ -226,6 +226,14 @@ pub struct EditorSettingsContent { /// /// Default: split pub diff_view_style: Option, + + /// The minimum width (in em-widths) at which the split diff view is used. + /// When the editor is narrower than this, the diff view automatically + /// switches to unified mode and switches back when the editor is wide + /// enough. Set to 0 to disable automatic switching. + /// + /// Default: 100 + pub minimum_split_diff_width: Option, } #[derive( diff --git a/crates/settings_ui/src/page_data.rs b/crates/settings_ui/src/page_data.rs index 83895cf6f3493fb6bddf6963e72b5467f382dd0a..e4eac81b067b2ead2e89153c9a444b4ebd016f64 100644 --- a/crates/settings_ui/src/page_data.rs +++ b/crates/settings_ui/src/page_data.rs @@ -1474,7 +1474,7 @@ fn editor_page() -> SettingsPage { ] } - fn multibuffer_section() -> [SettingsPageItem; 6] { + fn multibuffer_section() -> [SettingsPageItem; 7] { [ SettingsPageItem::SectionHeader("Multibuffer"), SettingsPageItem::SettingItem(SettingItem { @@ -1554,6 +1554,21 @@ fn editor_page() -> SettingsPage { metadata: None, files: USER, }), + SettingsPageItem::SettingItem(SettingItem { + title: "Minimum Split Diff Width", + 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.", + field: Box::new(SettingField { + json_path: Some("minimum_split_diff_width"), + pick: |settings_content| { + settings_content.editor.minimum_split_diff_width.as_ref() + }, + write: |settings_content, value| { + settings_content.editor.minimum_split_diff_width = value; + }, + }), + metadata: None, + files: USER, + }), ] }