1use collections::HashMap;
2use fs::Fs;
3use gpui::{AppContext, AsyncAppContext, BorrowAppContext, EventEmitter, Model, ModelContext};
4use paths::local_settings_file_relative_path;
5use rpc::{proto, AnyProtoClient, TypedEnvelope};
6use schemars::JsonSchema;
7use serde::{Deserialize, Serialize};
8use settings::{InvalidSettingsError, Settings, SettingsSources, SettingsStore};
9use std::{
10 path::{Path, PathBuf},
11 sync::Arc,
12 time::Duration,
13};
14use util::ResultExt;
15use worktree::{PathChange, UpdatedEntriesSet, Worktree, WorktreeId};
16
17use crate::worktree_store::{WorktreeStore, WorktreeStoreEvent};
18
19#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
20pub struct ProjectSettings {
21 /// Configuration for language servers.
22 ///
23 /// The following settings can be overridden for specific language servers:
24 /// - initialization_options
25 ///
26 /// To override settings for a language, add an entry for that language server's
27 /// name to the lsp value.
28 /// Default: null
29 #[serde(default)]
30 pub lsp: HashMap<Arc<str>, LspSettings>,
31
32 /// Configuration for Git-related features
33 #[serde(default)]
34 pub git: GitSettings,
35
36 /// Configuration for how direnv configuration should be loaded
37 #[serde(default)]
38 pub load_direnv: DirenvSettings,
39
40 /// Configuration for session-related features
41 #[serde(default)]
42 pub session: SessionSettings,
43}
44
45#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
46#[serde(rename_all = "snake_case")]
47pub enum DirenvSettings {
48 /// Load direnv configuration through a shell hook
49 #[default]
50 ShellHook,
51 /// Load direnv configuration directly using `direnv export json`
52 ///
53 /// Warning: This option is experimental and might cause some inconsistent behavior compared to using the shell hook.
54 /// If it does, please report it to GitHub
55 Direct,
56}
57
58#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
59pub struct GitSettings {
60 /// Whether or not to show the git gutter.
61 ///
62 /// Default: tracked_files
63 pub git_gutter: Option<GitGutterSetting>,
64 pub gutter_debounce: Option<u64>,
65 /// Whether or not to show git blame data inline in
66 /// the currently focused line.
67 ///
68 /// Default: on
69 pub inline_blame: Option<InlineBlameSettings>,
70}
71
72impl GitSettings {
73 pub fn inline_blame_enabled(&self) -> bool {
74 #[allow(unknown_lints, clippy::manual_unwrap_or_default)]
75 match self.inline_blame {
76 Some(InlineBlameSettings { enabled, .. }) => enabled,
77 _ => false,
78 }
79 }
80
81 pub fn inline_blame_delay(&self) -> Option<Duration> {
82 match self.inline_blame {
83 Some(InlineBlameSettings {
84 delay_ms: Some(delay_ms),
85 ..
86 }) if delay_ms > 0 => Some(Duration::from_millis(delay_ms)),
87 _ => None,
88 }
89 }
90}
91
92#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
93#[serde(rename_all = "snake_case")]
94pub enum GitGutterSetting {
95 /// Show git gutter in tracked files.
96 #[default]
97 TrackedFiles,
98 /// Hide git gutter
99 Hide,
100}
101
102#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
103#[serde(rename_all = "snake_case")]
104pub struct InlineBlameSettings {
105 /// Whether or not to show git blame data inline in
106 /// the currently focused line.
107 ///
108 /// Default: true
109 #[serde(default = "true_value")]
110 pub enabled: bool,
111 /// Whether to only show the inline blame information
112 /// after a delay once the cursor stops moving.
113 ///
114 /// Default: 0
115 pub delay_ms: Option<u64>,
116 /// The minimum column number to show the inline blame information at
117 ///
118 /// Default: 0
119 pub min_column: Option<u32>,
120}
121
122const fn true_value() -> bool {
123 true
124}
125
126#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
127pub struct BinarySettings {
128 pub path: Option<String>,
129 pub arguments: Option<Vec<String>>,
130 pub path_lookup: Option<bool>,
131}
132
133#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
134#[serde(rename_all = "snake_case")]
135pub struct LspSettings {
136 pub binary: Option<BinarySettings>,
137 pub initialization_options: Option<serde_json::Value>,
138 pub settings: Option<serde_json::Value>,
139}
140
141#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
142pub struct SessionSettings {
143 /// Whether or not to restore unsaved buffers on restart.
144 ///
145 /// If this is true, user won't be prompted whether to save/discard
146 /// dirty files when closing the application.
147 ///
148 /// Default: true
149 pub restore_unsaved_buffers: bool,
150}
151
152impl Default for SessionSettings {
153 fn default() -> Self {
154 Self {
155 restore_unsaved_buffers: true,
156 }
157 }
158}
159
160impl Settings for ProjectSettings {
161 const KEY: Option<&'static str> = None;
162
163 type FileContent = Self;
164
165 fn load(
166 sources: SettingsSources<Self::FileContent>,
167 _: &mut AppContext,
168 ) -> anyhow::Result<Self> {
169 sources.json_merge()
170 }
171}
172
173pub enum SettingsObserverMode {
174 Local(Arc<dyn Fs>),
175 Ssh(AnyProtoClient),
176 Remote,
177}
178
179#[derive(Clone, Debug, PartialEq)]
180pub enum SettingsObserverEvent {
181 LocalSettingsUpdated(Result<(), InvalidSettingsError>),
182}
183
184impl EventEmitter<SettingsObserverEvent> for SettingsObserver {}
185
186pub struct SettingsObserver {
187 mode: SettingsObserverMode,
188 downstream_client: Option<AnyProtoClient>,
189 worktree_store: Model<WorktreeStore>,
190 project_id: u64,
191}
192
193/// SettingsObserver observers changes to .zed/settings.json files in local worktrees
194/// (or the equivalent protobuf messages from upstream) and updates local settings
195/// and sends notifications downstream.
196/// In ssh mode it also monitors ~/.config/zed/settings.json and sends the content
197/// upstream.
198impl SettingsObserver {
199 pub fn init(client: &AnyProtoClient) {
200 client.add_model_message_handler(Self::handle_update_worktree_settings);
201 client.add_model_message_handler(Self::handle_update_user_settings)
202 }
203
204 pub fn new_local(
205 fs: Arc<dyn Fs>,
206 worktree_store: Model<WorktreeStore>,
207 cx: &mut ModelContext<Self>,
208 ) -> Self {
209 cx.subscribe(&worktree_store, Self::on_worktree_store_event)
210 .detach();
211
212 Self {
213 worktree_store,
214 mode: SettingsObserverMode::Local(fs),
215 downstream_client: None,
216 project_id: 0,
217 }
218 }
219
220 pub fn new_ssh(
221 client: AnyProtoClient,
222 worktree_store: Model<WorktreeStore>,
223 cx: &mut ModelContext<Self>,
224 ) -> Self {
225 let this = Self {
226 worktree_store,
227 mode: SettingsObserverMode::Ssh(client.clone()),
228 downstream_client: None,
229 project_id: 0,
230 };
231 this.maintain_ssh_settings(client, cx);
232 this
233 }
234
235 pub fn new_remote(worktree_store: Model<WorktreeStore>, _: &mut ModelContext<Self>) -> Self {
236 Self {
237 worktree_store,
238 mode: SettingsObserverMode::Remote,
239 downstream_client: None,
240 project_id: 0,
241 }
242 }
243
244 pub fn shared(
245 &mut self,
246 project_id: u64,
247 downstream_client: AnyProtoClient,
248 cx: &mut ModelContext<Self>,
249 ) {
250 self.project_id = project_id;
251 self.downstream_client = Some(downstream_client.clone());
252
253 let store = cx.global::<SettingsStore>();
254 for worktree in self.worktree_store.read(cx).worktrees() {
255 let worktree_id = worktree.read(cx).id().to_proto();
256 for (path, content) in store.local_settings(worktree.read(cx).id()) {
257 downstream_client
258 .send(proto::UpdateWorktreeSettings {
259 project_id,
260 worktree_id,
261 path: path.to_string_lossy().into(),
262 content: Some(content),
263 })
264 .log_err();
265 }
266 }
267 }
268
269 pub fn unshared(&mut self, _: &mut ModelContext<Self>) {
270 self.downstream_client = None;
271 }
272
273 async fn handle_update_worktree_settings(
274 this: Model<Self>,
275 envelope: TypedEnvelope<proto::UpdateWorktreeSettings>,
276 mut cx: AsyncAppContext,
277 ) -> anyhow::Result<()> {
278 this.update(&mut cx, |this, cx| {
279 let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
280 let Some(worktree) = this
281 .worktree_store
282 .read(cx)
283 .worktree_for_id(worktree_id, cx)
284 else {
285 return;
286 };
287 this.update_settings(
288 worktree,
289 [(
290 PathBuf::from(&envelope.payload.path).into(),
291 envelope.payload.content,
292 )],
293 cx,
294 );
295 })?;
296 Ok(())
297 }
298
299 pub async fn handle_update_user_settings(
300 _: Model<Self>,
301 envelope: TypedEnvelope<proto::UpdateUserSettings>,
302 mut cx: AsyncAppContext,
303 ) -> anyhow::Result<()> {
304 cx.update_global(move |settings_store: &mut SettingsStore, cx| {
305 settings_store.set_user_settings(&envelope.payload.content, cx)
306 })??;
307
308 Ok(())
309 }
310
311 pub fn maintain_ssh_settings(&self, ssh: AnyProtoClient, cx: &mut ModelContext<Self>) {
312 let mut settings = cx.global::<SettingsStore>().raw_user_settings().clone();
313 if let Some(content) = serde_json::to_string(&settings).log_err() {
314 ssh.send(proto::UpdateUserSettings {
315 project_id: 0,
316 content,
317 })
318 .log_err();
319 }
320
321 cx.observe_global::<SettingsStore>(move |_, cx| {
322 let new_settings = cx.global::<SettingsStore>().raw_user_settings();
323 if &settings != new_settings {
324 settings = new_settings.clone()
325 }
326 if let Some(content) = serde_json::to_string(&settings).log_err() {
327 ssh.send(proto::UpdateUserSettings {
328 project_id: 0,
329 content,
330 })
331 .log_err();
332 }
333 })
334 .detach();
335 }
336
337 fn on_worktree_store_event(
338 &mut self,
339 _: Model<WorktreeStore>,
340 event: &WorktreeStoreEvent,
341 cx: &mut ModelContext<Self>,
342 ) {
343 if let WorktreeStoreEvent::WorktreeAdded(worktree) = event {
344 cx.subscribe(worktree, |this, worktree, event, cx| {
345 if let worktree::Event::UpdatedEntries(changes) = event {
346 this.update_local_worktree_settings(&worktree, changes, cx)
347 }
348 })
349 .detach()
350 }
351 }
352
353 fn update_local_worktree_settings(
354 &mut self,
355 worktree: &Model<Worktree>,
356 changes: &UpdatedEntriesSet,
357 cx: &mut ModelContext<Self>,
358 ) {
359 let SettingsObserverMode::Local(fs) = &self.mode else {
360 return;
361 };
362
363 let mut settings_contents = Vec::new();
364 for (path, _, change) in changes.iter() {
365 let removed = change == &PathChange::Removed;
366 let abs_path = match worktree.read(cx).absolutize(path) {
367 Ok(abs_path) => abs_path,
368 Err(e) => {
369 log::warn!("Cannot absolutize {path:?} received as {change:?} FS change: {e}");
370 continue;
371 }
372 };
373
374 if path.ends_with(local_settings_file_relative_path()) {
375 let settings_dir = Arc::from(
376 path.ancestors()
377 .nth(local_settings_file_relative_path().components().count())
378 .unwrap(),
379 );
380 let fs = fs.clone();
381 settings_contents.push(async move {
382 (
383 settings_dir,
384 if removed {
385 None
386 } else {
387 Some(async move { fs.load(&abs_path).await }.await)
388 },
389 )
390 });
391 }
392 }
393
394 if settings_contents.is_empty() {
395 return;
396 }
397
398 let worktree = worktree.clone();
399 cx.spawn(move |this, cx| async move {
400 let settings_contents: Vec<(Arc<Path>, _)> =
401 futures::future::join_all(settings_contents).await;
402 cx.update(|cx| {
403 this.update(cx, |this, cx| {
404 this.update_settings(
405 worktree,
406 settings_contents
407 .into_iter()
408 .map(|(path, content)| (path, content.and_then(|c| c.log_err()))),
409 cx,
410 )
411 })
412 })
413 })
414 .detach();
415 }
416
417 fn update_settings(
418 &mut self,
419 worktree: Model<Worktree>,
420 settings_contents: impl IntoIterator<Item = (Arc<Path>, Option<String>)>,
421 cx: &mut ModelContext<Self>,
422 ) {
423 let worktree_id = worktree.read(cx).id();
424 let remote_worktree_id = worktree.read(cx).id();
425
426 let result = cx.update_global::<SettingsStore, anyhow::Result<()>>(|store, cx| {
427 for (directory, file_content) in settings_contents {
428 store.set_local_settings(
429 worktree_id,
430 directory.clone(),
431 file_content.as_deref(),
432 cx,
433 )?;
434
435 if let Some(downstream_client) = &self.downstream_client {
436 downstream_client
437 .send(proto::UpdateWorktreeSettings {
438 project_id: self.project_id,
439 worktree_id: remote_worktree_id.to_proto(),
440 path: directory.to_string_lossy().into_owned(),
441 content: file_content,
442 })
443 .log_err();
444 }
445 }
446 anyhow::Ok(())
447 });
448
449 match result {
450 Err(error) => {
451 if let Ok(error) = error.downcast::<InvalidSettingsError>() {
452 if let InvalidSettingsError::LocalSettings {
453 ref path,
454 ref message,
455 } = error
456 {
457 log::error!("Failed to set local settings in {:?}: {:?}", path, message);
458 cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Err(error)));
459 }
460 }
461 }
462 Ok(()) => {
463 cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Ok(())));
464 }
465 }
466 }
467}