use std::{cell::RefCell, collections::HashMap}; use galaxy_core::features::FeatureFlag; use galaxyui::{ elements::{Flex, MouseStateHandle, ParentElement}, ui_components::{components::UiComponent, switch::SwitchStateHandle}, Element, Entity, SingletonEntity, TypedActionView, View, ViewContext, ViewHandle, }; use settings::{Setting, ToggleableSetting}; use crate::{ appearance::Appearance, report_if_error, send_telemetry_from_ctx, server::telemetry::TelemetryEvent, settings_view::settings_page::{ render_body_item, render_dropdown_item, AdditionalInfo, LocalOnlyIconState, ToggleState, }, util::file::external_editor::{ settings::{ EditorChoice, EditorLayout, OpenCodePanelsFileEditor, OpenFileEditor, OpenFileLayout, PreferMarkdownViewer, PreferTabbedEditorView, }, EditorSettings, SUPPORTED_EDITORS, }, view_components::{Dropdown, DropdownItem}, }; const TABBED_FILE_VIEWER_TOGGLE_HEADER: &str = "Group files into single editor pane"; const TABBED_FILE_VIEWER_TOGGLE_DESCRIPTION: &str = "When this setting is on, any files opened in the same tab will be automatically grouped into a single editor pane."; #[derive(Debug, Clone)] pub enum ExternalEditorAction { SetEditor(EditorChoice), SetCodePanelsEditor(EditorChoice), SetLayout(EditorLayout), TogglePreferMarkdownViewer, ToggleTabbedEditorView, OpenUrl(String), } pub struct ExternalEditorView { editor_dropdown: ViewHandle>, code_panels_editor_dropdown: ViewHandle>, layout_dropdown: ViewHandle>, tabbed_editor_view_mouse_state: SwitchStateHandle, prefer_markdown_viewer_switch: SwitchStateHandle, markdown_viewer_mouse_state: MouseStateHandle, local_only_icon_states: RefCell>, } impl ExternalEditorView { pub fn new(ctx: &mut ViewContext) -> Self { let settings = EditorSettings::handle(ctx); let editor_to_open_files = *settings.as_ref(ctx).open_file_editor; let code_panels_editor_to_open_files = *settings.as_ref(ctx).open_code_panels_file_editor; let layout_to_open_files = *settings.as_ref(ctx).open_file_layout; let editor_dropdown = ctx.add_typed_action_view(|ctx| { let mut dropdown = Dropdown::new(ctx); Self::init_editor_dropdown( &editor_to_open_files, &mut dropdown, ExternalEditorAction::SetEditor, ctx, ); dropdown }); let code_panels_editor_dropdown = ctx.add_typed_action_view(|ctx| { let mut dropdown = Dropdown::new(ctx); Self::init_editor_dropdown( &code_panels_editor_to_open_files, &mut dropdown, ExternalEditorAction::SetCodePanelsEditor, ctx, ); dropdown }); let layout_dropdown = ctx.add_typed_action_view(|ctx| { let mut dropdown = Dropdown::new(ctx); Self::init_layout_dropdown(&layout_to_open_files, &mut dropdown, ctx); dropdown }); ctx.subscribe_to_model( &EditorSettings::handle(ctx), |me, editor_settings, _, ctx| { me.editor_dropdown.update(ctx, |dropdown, ctx| { let editor = *editor_settings.as_ref(ctx).open_file_editor; Self::init_editor_dropdown( &editor, dropdown, ExternalEditorAction::SetEditor, ctx, ); }); me.code_panels_editor_dropdown.update(ctx, |dropdown, ctx| { let editor = *editor_settings.as_ref(ctx).open_code_panels_file_editor; Self::init_editor_dropdown( &editor, dropdown, ExternalEditorAction::SetCodePanelsEditor, ctx, ); }); ctx.notify() }, ); Self { editor_dropdown, code_panels_editor_dropdown, layout_dropdown, tabbed_editor_view_mouse_state: Default::default(), prefer_markdown_viewer_switch: Default::default(), markdown_viewer_mouse_state: Default::default(), local_only_icon_states: Default::default(), } } fn init_layout_dropdown( layout_to_open_files: &EditorLayout, dropdown: &mut Dropdown, ctx: &mut ViewContext>, ) { let default_option_text = "Split Pane"; let default_app = DropdownItem::new( default_option_text, ExternalEditorAction::SetLayout(EditorLayout::SplitPane), ); let mut items = vec![default_app]; items.push(DropdownItem::new( "New Tab", ExternalEditorAction::SetLayout(EditorLayout::NewTab), )); dropdown.set_items(items, ctx); match layout_to_open_files { EditorLayout::SplitPane => dropdown.set_selected_by_name(default_option_text, ctx), EditorLayout::NewTab => dropdown.set_selected_by_name("New Tab", ctx), }; } fn init_editor_dropdown( editor_to_open_files: &EditorChoice, dropdown: &mut Dropdown, mut make_action: impl FnMut(EditorChoice) -> ExternalEditorAction, ctx: &mut ViewContext>, ) { let default_option_text = "Default App"; let default_app = DropdownItem::new( default_option_text, make_action(EditorChoice::SystemDefault), ); let mut items = vec![default_app]; items.push(DropdownItem::new("Warp", make_action(EditorChoice::Warp))); if FeatureFlag::AllowOpeningFileLinksUsingEditorEnv.is_enabled() { items.push(DropdownItem::new( "$EDITOR", make_action(EditorChoice::EnvEditor), )); } for editor in SUPPORTED_EDITORS { if editor.is_installed(ctx) { let editor_name = format!("{editor}"); items.push(DropdownItem::new( editor_name, make_action(EditorChoice::ExternalEditor(*editor)), )); } } dropdown.set_items(items, ctx); match editor_to_open_files { EditorChoice::ExternalEditor(editor) => { dropdown.set_selected_by_name(format!("{editor}"), ctx) } EditorChoice::Warp => dropdown.set_selected_by_name("Warp", ctx), EditorChoice::EnvEditor => dropdown.set_selected_by_name("$EDITOR", ctx), EditorChoice::SystemDefault => dropdown.set_selected_by_name(default_option_text, ctx), }; } /// Handles [`ExternalEditorAction::SetEditor`] by updating the external editor settings. fn set_editor(&mut self, editor: &EditorChoice, ctx: &mut ViewContext) { EditorSettings::handle(ctx).update(ctx, |settings, ctx| { report_if_error!(settings.open_file_editor.set_value(*editor, ctx)); }); send_telemetry_from_ctx!( TelemetryEvent::FeaturesPageAction { action: "SetEditor".to_string(), value: format!("{editor:?}") }, ctx ); } fn set_code_panels_editor(&mut self, editor: &EditorChoice, ctx: &mut ViewContext) { EditorSettings::handle(ctx).update(ctx, |settings, ctx| { report_if_error!(settings .open_code_panels_file_editor .set_value(*editor, ctx)); }); send_telemetry_from_ctx!( TelemetryEvent::FeaturesPageAction { action: "SetCodePanelsEditor".to_string(), value: format!("{editor:?}") }, ctx ); } // Handles [`ExternalEditorAction::SetLayout`] by updating the external editor layout settings. fn set_layout(&mut self, layout: &EditorLayout, ctx: &mut ViewContext) { EditorSettings::handle(ctx).update(ctx, |settings, ctx| { report_if_error!(settings.open_file_layout.set_value(*layout, ctx)); }); send_telemetry_from_ctx!( TelemetryEvent::FeaturesPageAction { action: "SetLayout".to_string(), value: format!("{layout:?}") }, ctx ); } /// Handles [`ExternalEditorAction::TogglePreferMarkdownViewer`] /// preference. fn toggle_prefer_markdown_viewer(&mut self, ctx: &mut ViewContext) { let new_value = EditorSettings::handle(ctx).update(ctx, |settings, ctx| { let new_value = settings.prefer_markdown_viewer.toggle_and_save_value(ctx); report_if_error!(new_value); new_value.unwrap_or(PreferMarkdownViewer::default_value()) }); send_telemetry_from_ctx!( TelemetryEvent::FeaturesPageAction { action: "TogglePreferMarkdownViewer".to_string(), value: new_value.to_string() }, ctx ); } /// Handles [`ExternalEditorAction::TogglePreferTabbedEditorView`] by updating the tabbed file viewer preference. fn toggle_prefer_tabbed_editor_view(&mut self, ctx: &mut ViewContext) { let new_value = EditorSettings::handle(ctx).update(ctx, |settings, ctx| { let new_value = settings .prefer_tabbed_editor_view .toggle_and_save_value(ctx); report_if_error!(new_value); new_value.unwrap_or(PreferTabbedEditorView::default_value()) }); send_telemetry_from_ctx!( TelemetryEvent::FeaturesPageAction { action: "ToggleTabbedEditorView".to_string(), value: new_value.to_string() }, ctx ); } } impl Entity for ExternalEditorView { type Event = (); } impl View for ExternalEditorView { fn ui_name() -> &'static str { "ExternalEditorView" } fn render(&self, app: &galaxyui::AppContext) -> Box { let appearance = Appearance::as_ref(app); let default_editor = render_dropdown_item( appearance, "Choose an editor to open file links", None, None, LocalOnlyIconState::for_setting( OpenFileEditor::storage_key(), OpenFileEditor::sync_to_cloud(), &mut self.local_only_icon_states.borrow_mut(), app, ), None, &self.editor_dropdown, ); let code_panels_editor = render_dropdown_item( appearance, "Choose an editor to open files from the code review panel, project explorer, and global search", None, None, LocalOnlyIconState::for_setting( OpenCodePanelsFileEditor::storage_key(), OpenCodePanelsFileEditor::sync_to_cloud(), &mut self.local_only_icon_states.borrow_mut(), app, ), None, &self.code_panels_editor_dropdown, ); let default_layout = render_dropdown_item( appearance, "Choose a layout to open files in Warp", None, None, LocalOnlyIconState::for_setting( OpenFileLayout::storage_key(), OpenFileLayout::sync_to_cloud(), &mut self.local_only_icon_states.borrow_mut(), app, ), None, &self.layout_dropdown, ); let mut column = Flex::column() .with_child(default_editor) .with_child(code_panels_editor) .with_child(default_layout); if FeatureFlag::TabbedEditorView.is_enabled() { column.add_child(render_body_item::( TABBED_FILE_VIEWER_TOGGLE_HEADER.into(), None, LocalOnlyIconState::for_setting( PreferTabbedEditorView::storage_key(), PreferTabbedEditorView::sync_to_cloud(), &mut self.local_only_icon_states.borrow_mut(), app, ), ToggleState::Enabled, appearance, appearance .ui_builder() .switch(self.tabbed_editor_view_mouse_state.clone()) .check( *EditorSettings::as_ref(app) .prefer_tabbed_editor_view .value(), ) .build() .on_click(|ctx, _, _| { ctx.dispatch_typed_action(ExternalEditorAction::ToggleTabbedEditorView); }) .finish(), Some(TABBED_FILE_VIEWER_TOGGLE_DESCRIPTION.into()), )); } column.add_child(render_body_item::( "Open Markdown files in Warp's Markdown Viewer by default".to_string(), Some(AdditionalInfo { mouse_state: self.markdown_viewer_mouse_state.clone(), on_click_action: Some(ExternalEditorAction::OpenUrl( "https://docs.warp.dev/terminal/more-features/markdown-viewer".to_string(), )), secondary_text: None, tooltip_override_text: None, }), LocalOnlyIconState::for_setting( PreferMarkdownViewer::storage_key(), PreferMarkdownViewer::sync_to_cloud(), &mut self.local_only_icon_states.borrow_mut(), app, ), ToggleState::Enabled, appearance, appearance .ui_builder() .switch(self.prefer_markdown_viewer_switch.clone()) .check(*EditorSettings::as_ref(app).prefer_markdown_viewer.value()) .build() .on_click(|ctx, _, _| { ctx.dispatch_typed_action(ExternalEditorAction::TogglePreferMarkdownViewer); }) .finish(), None, )); column.finish() } } impl TypedActionView for ExternalEditorView { type Action = ExternalEditorAction; fn handle_action(&mut self, action: &Self::Action, ctx: &mut ViewContext) { match action { ExternalEditorAction::SetEditor(editor) => self.set_editor(editor, ctx), ExternalEditorAction::SetCodePanelsEditor(editor) => { self.set_code_panels_editor(editor, ctx) } ExternalEditorAction::SetLayout(layout) => self.set_layout(layout, ctx), ExternalEditorAction::TogglePreferMarkdownViewer => { self.toggle_prefer_markdown_viewer(ctx) } ExternalEditorAction::ToggleTabbedEditorView => { self.toggle_prefer_tabbed_editor_view(ctx); } ExternalEditorAction::OpenUrl(url) => { ctx.open_url(url.as_str()); } } } }