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::{proto, AnyProtoClient, TypedEnvelope};
  6use schemars::JsonSchema;
  7use serde::{Deserialize, Serialize};
  8use settings::{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
179pub struct SettingsObserver {
180    mode: SettingsObserverMode,
181    downstream_client: Option<AnyProtoClient>,
182    worktree_store: Model<WorktreeStore>,
183    project_id: u64,
184}
185
186/// SettingsObserver observers changes to .zed/settings.json files in local worktrees
187/// (or the equivalent protobuf messages from upstream) and updates local settings
188/// and sends notifications downstream.
189/// In ssh mode it also monitors ~/.config/zed/settings.json and sends the content
190/// upstream.
191impl SettingsObserver {
192    pub fn init(client: &AnyProtoClient) {
193        client.add_model_message_handler(Self::handle_update_worktree_settings);
194        client.add_model_message_handler(Self::handle_update_user_settings)
195    }
196
197    pub fn new_local(
198        fs: Arc<dyn Fs>,
199        worktree_store: Model<WorktreeStore>,
200        cx: &mut ModelContext<Self>,
201    ) -> Self {
202        cx.subscribe(&worktree_store, Self::on_worktree_store_event)
203            .detach();
204
205        Self {
206            worktree_store,
207            mode: SettingsObserverMode::Local(fs),
208            downstream_client: None,
209            project_id: 0,
210        }
211    }
212
213    pub fn new_ssh(
214        client: AnyProtoClient,
215        worktree_store: Model<WorktreeStore>,
216        cx: &mut ModelContext<Self>,
217    ) -> Self {
218        let this = Self {
219            worktree_store,
220            mode: SettingsObserverMode::Ssh(client.clone()),
221            downstream_client: None,
222            project_id: 0,
223        };
224        this.maintain_ssh_settings(client, cx);
225        this
226    }
227
228    pub fn new_remote(worktree_store: Model<WorktreeStore>, _: &mut ModelContext<Self>) -> Self {
229        Self {
230            worktree_store,
231            mode: SettingsObserverMode::Remote,
232            downstream_client: None,
233            project_id: 0,
234        }
235    }
236
237    pub fn shared(
238        &mut self,
239        project_id: u64,
240        downstream_client: AnyProtoClient,
241        cx: &mut ModelContext<Self>,
242    ) {
243        self.project_id = project_id;
244        self.downstream_client = Some(downstream_client.clone());
245
246        let store = cx.global::<SettingsStore>();
247        for worktree in self.worktree_store.read(cx).worktrees() {
248            let worktree_id = worktree.read(cx).id().to_proto();
249            for (path, content) in store.local_settings(worktree.read(cx).id()) {
250                downstream_client
251                    .send(proto::UpdateWorktreeSettings {
252                        project_id,
253                        worktree_id,
254                        path: path.to_string_lossy().into(),
255                        content: Some(content),
256                    })
257                    .log_err();
258            }
259        }
260    }
261
262    pub fn unshared(&mut self, _: &mut ModelContext<Self>) {
263        self.downstream_client = None;
264    }
265
266    async fn handle_update_worktree_settings(
267        this: Model<Self>,
268        envelope: TypedEnvelope<proto::UpdateWorktreeSettings>,
269        mut cx: AsyncAppContext,
270    ) -> anyhow::Result<()> {
271        this.update(&mut cx, |this, cx| {
272            let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
273            let Some(worktree) = this
274                .worktree_store
275                .read(cx)
276                .worktree_for_id(worktree_id, cx)
277            else {
278                return;
279            };
280            this.update_settings(
281                worktree,
282                [(
283                    PathBuf::from(&envelope.payload.path).into(),
284                    envelope.payload.content,
285                )],
286                cx,
287            );
288        })?;
289        Ok(())
290    }
291
292    pub async fn handle_update_user_settings(
293        _: Model<Self>,
294        envelope: TypedEnvelope<proto::UpdateUserSettings>,
295        mut cx: AsyncAppContext,
296    ) -> anyhow::Result<()> {
297        cx.update_global(move |settings_store: &mut SettingsStore, cx| {
298            settings_store.set_user_settings(&envelope.payload.content, cx)
299        })??;
300
301        Ok(())
302    }
303
304    pub fn maintain_ssh_settings(&self, ssh: AnyProtoClient, cx: &mut ModelContext<Self>) {
305        let mut settings = cx.global::<SettingsStore>().raw_user_settings().clone();
306        if let Some(content) = serde_json::to_string(&settings).log_err() {
307            ssh.send(proto::UpdateUserSettings {
308                project_id: 0,
309                content,
310            })
311            .log_err();
312        }
313
314        cx.observe_global::<SettingsStore>(move |_, cx| {
315            let new_settings = cx.global::<SettingsStore>().raw_user_settings();
316            if &settings != new_settings {
317                settings = new_settings.clone()
318            }
319            if let Some(content) = serde_json::to_string(&settings).log_err() {
320                ssh.send(proto::UpdateUserSettings {
321                    project_id: 0,
322                    content,
323                })
324                .log_err();
325            }
326        })
327        .detach();
328    }
329
330    fn on_worktree_store_event(
331        &mut self,
332        _: Model<WorktreeStore>,
333        event: &WorktreeStoreEvent,
334        cx: &mut ModelContext<Self>,
335    ) {
336        if let WorktreeStoreEvent::WorktreeAdded(worktree) = event {
337            cx.subscribe(worktree, |this, worktree, event, cx| {
338                if let worktree::Event::UpdatedEntries(changes) = event {
339                    this.update_local_worktree_settings(&worktree, changes, cx)
340                }
341            })
342            .detach()
343        }
344    }
345
346    fn update_local_worktree_settings(
347        &mut self,
348        worktree: &Model<Worktree>,
349        changes: &UpdatedEntriesSet,
350        cx: &mut ModelContext<Self>,
351    ) {
352        let SettingsObserverMode::Local(fs) = &self.mode else {
353            return;
354        };
355
356        let mut settings_contents = Vec::new();
357        for (path, _, change) in changes.iter() {
358            let removed = change == &PathChange::Removed;
359            let abs_path = match worktree.read(cx).absolutize(path) {
360                Ok(abs_path) => abs_path,
361                Err(e) => {
362                    log::warn!("Cannot absolutize {path:?} received as {change:?} FS change: {e}");
363                    continue;
364                }
365            };
366
367            if path.ends_with(local_settings_file_relative_path()) {
368                let settings_dir = Arc::from(
369                    path.ancestors()
370                        .nth(local_settings_file_relative_path().components().count())
371                        .unwrap(),
372                );
373                let fs = fs.clone();
374                settings_contents.push(async move {
375                    (
376                        settings_dir,
377                        if removed {
378                            None
379                        } else {
380                            Some(async move { fs.load(&abs_path).await }.await)
381                        },
382                    )
383                });
384            }
385        }
386
387        if settings_contents.is_empty() {
388            return;
389        }
390
391        let worktree = worktree.clone();
392        cx.spawn(move |this, cx| async move {
393            let settings_contents: Vec<(Arc<Path>, _)> =
394                futures::future::join_all(settings_contents).await;
395            cx.update(|cx| {
396                this.update(cx, |this, cx| {
397                    this.update_settings(
398                        worktree,
399                        settings_contents
400                            .into_iter()
401                            .map(|(path, content)| (path, content.and_then(|c| c.log_err()))),
402                        cx,
403                    )
404                })
405            })
406        })
407        .detach();
408    }
409
410    fn update_settings(
411        &mut self,
412        worktree: Model<Worktree>,
413        settings_contents: impl IntoIterator<Item = (Arc<Path>, Option<String>)>,
414        cx: &mut ModelContext<Self>,
415    ) {
416        let worktree_id = worktree.read(cx).id();
417        let remote_worktree_id = worktree.read(cx).id();
418        cx.update_global::<SettingsStore, _>(|store, cx| {
419            for (directory, file_content) in settings_contents {
420                store
421                    .set_local_settings(worktree_id, directory.clone(), file_content.as_deref(), cx)
422                    .log_err();
423                if let Some(downstream_client) = &self.downstream_client {
424                    downstream_client
425                        .send(proto::UpdateWorktreeSettings {
426                            project_id: self.project_id,
427                            worktree_id: remote_worktree_id.to_proto(),
428                            path: directory.to_string_lossy().into_owned(),
429                            content: file_content,
430                        })
431                        .log_err();
432                }
433            }
434        })
435    }
436}