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