project_settings.rs

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