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