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