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