project_settings.rs

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