settings_ui.rs

  1mod appearance_settings_controls;
  2
  3use std::any::TypeId;
  4use std::sync::Arc;
  5
  6use command_palette_hooks::CommandPaletteFilter;
  7use editor::EditorSettingsControls;
  8use feature_flags::{FeatureFlag, FeatureFlagViewExt};
  9use fs::Fs;
 10use gpui::{
 11    App, AsyncWindowContext, Entity, EventEmitter, FocusHandle, Focusable, Task, actions,
 12    impl_actions,
 13};
 14use schemars::JsonSchema;
 15use serde::Deserialize;
 16use settings::{SettingsStore, VsCodeSettingsSource};
 17use ui::prelude::*;
 18use workspace::Workspace;
 19use workspace::item::{Item, ItemEvent};
 20
 21use crate::appearance_settings_controls::AppearanceSettingsControls;
 22
 23pub struct SettingsUiFeatureFlag;
 24
 25impl FeatureFlag for SettingsUiFeatureFlag {
 26    const NAME: &'static str = "settings-ui";
 27}
 28
 29#[derive(Copy, Clone, Debug, Default, PartialEq, Deserialize, JsonSchema)]
 30pub struct ImportVsCodeSettings {
 31    #[serde(default)]
 32    pub skip_prompt: bool,
 33}
 34
 35#[derive(Copy, Clone, Debug, Default, PartialEq, Deserialize, JsonSchema)]
 36pub struct ImportCursorSettings {
 37    #[serde(default)]
 38    pub skip_prompt: bool,
 39}
 40
 41impl_actions!(zed, [ImportVsCodeSettings, ImportCursorSettings]);
 42actions!(zed, [OpenSettingsEditor]);
 43
 44pub fn init(cx: &mut App) {
 45    cx.observe_new(|workspace: &mut Workspace, window, cx| {
 46        let Some(window) = window else {
 47            return;
 48        };
 49
 50        workspace.register_action(|workspace, _: &OpenSettingsEditor, window, cx| {
 51            let existing = workspace
 52                .active_pane()
 53                .read(cx)
 54                .items()
 55                .find_map(|item| item.downcast::<SettingsPage>());
 56
 57            if let Some(existing) = existing {
 58                workspace.activate_item(&existing, true, true, window, cx);
 59            } else {
 60                let settings_page = SettingsPage::new(workspace, cx);
 61                workspace.add_item_to_active_pane(Box::new(settings_page), None, true, window, cx)
 62            }
 63        });
 64
 65        workspace.register_action(|_workspace, action: &ImportVsCodeSettings, window, cx| {
 66            let fs = <dyn Fs>::global(cx);
 67            let action = *action;
 68
 69            window
 70                .spawn(cx, async move |cx: &mut AsyncWindowContext| {
 71                    handle_import_vscode_settings(
 72                        VsCodeSettingsSource::VsCode,
 73                        action.skip_prompt,
 74                        fs,
 75                        cx,
 76                    )
 77                    .await
 78                })
 79                .detach();
 80        });
 81
 82        workspace.register_action(|_workspace, action: &ImportCursorSettings, window, cx| {
 83            let fs = <dyn Fs>::global(cx);
 84            let action = *action;
 85
 86            window
 87                .spawn(cx, async move |cx: &mut AsyncWindowContext| {
 88                    handle_import_vscode_settings(
 89                        VsCodeSettingsSource::Cursor,
 90                        action.skip_prompt,
 91                        fs,
 92                        cx,
 93                    )
 94                    .await
 95                })
 96                .detach();
 97        });
 98
 99        let settings_ui_actions = [TypeId::of::<OpenSettingsEditor>()];
100
101        CommandPaletteFilter::update_global(cx, |filter, _cx| {
102            filter.hide_action_types(&settings_ui_actions);
103        });
104
105        cx.observe_flag::<SettingsUiFeatureFlag, _>(
106            window,
107            move |is_enabled, _workspace, _, cx| {
108                if is_enabled {
109                    CommandPaletteFilter::update_global(cx, |filter, _cx| {
110                        filter.show_action_types(settings_ui_actions.iter());
111                    });
112                } else {
113                    CommandPaletteFilter::update_global(cx, |filter, _cx| {
114                        filter.hide_action_types(&settings_ui_actions);
115                    });
116                }
117            },
118        )
119        .detach();
120    })
121    .detach();
122}
123
124async fn handle_import_vscode_settings(
125    source: VsCodeSettingsSource,
126    skip_prompt: bool,
127    fs: Arc<dyn Fs>,
128    cx: &mut AsyncWindowContext,
129) {
130    let vscode = match settings::VsCodeSettings::load_user_settings(source, fs.clone()).await {
131        Ok(vscode) => vscode,
132        Err(err) => {
133            println!(
134                "Failed to load {source} settings: {}",
135                err.context(format!(
136                    "Loading {source} settings from path: {:?}",
137                    paths::vscode_settings_file()
138                ))
139            );
140
141            let _ = cx.prompt(
142                gpui::PromptLevel::Info,
143                &format!("Could not find or load a {source} settings file"),
144                None,
145                &["Ok"],
146            );
147            return;
148        }
149    };
150
151    let prompt = if skip_prompt {
152        Task::ready(Some(0))
153    } else {
154        let prompt = cx.prompt(
155            gpui::PromptLevel::Warning,
156            "Importing settings may overwrite your existing settings",
157            None,
158            &["Ok", "Cancel"],
159        );
160        cx.spawn(async move |_| prompt.await.ok())
161    };
162    if prompt.await != Some(0) {
163        return;
164    }
165
166    cx.update(|_, cx| {
167        cx.global::<SettingsStore>()
168            .import_vscode_settings(fs, vscode);
169        log::info!("Imported settings from {source}");
170    })
171    .ok();
172}
173
174pub struct SettingsPage {
175    focus_handle: FocusHandle,
176}
177
178impl SettingsPage {
179    pub fn new(_workspace: &Workspace, cx: &mut Context<Workspace>) -> Entity<Self> {
180        cx.new(|cx| Self {
181            focus_handle: cx.focus_handle(),
182        })
183    }
184}
185
186impl EventEmitter<ItemEvent> for SettingsPage {}
187
188impl Focusable for SettingsPage {
189    fn focus_handle(&self, _cx: &App) -> FocusHandle {
190        self.focus_handle.clone()
191    }
192}
193
194impl Item for SettingsPage {
195    type Event = ItemEvent;
196
197    fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
198        Some(Icon::new(IconName::Settings))
199    }
200
201    fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
202        "Settings".into()
203    }
204
205    fn show_toolbar(&self) -> bool {
206        false
207    }
208
209    fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) {
210        f(*event)
211    }
212}
213
214impl Render for SettingsPage {
215    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
216        v_flex()
217            .p_4()
218            .size_full()
219            .gap_4()
220            .child(Label::new("Settings").size(LabelSize::Large))
221            .child(
222                v_flex().gap_1().child(Label::new("Appearance")).child(
223                    v_flex()
224                        .elevation_2(cx)
225                        .child(AppearanceSettingsControls::new()),
226                ),
227            )
228            .child(
229                v_flex().gap_1().child(Label::new("Editor")).child(
230                    v_flex()
231                        .elevation_2(cx)
232                        .child(EditorSettingsControls::new()),
233                ),
234            )
235    }
236}