project_settings.rs

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