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,
  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, kind, 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(local_settings_kind_to_proto(kind).into()),
298                    })
299                    .log_err();
300            }
301        }
302    }
303
304    pub fn unshared(&mut self, _: &mut ModelContext<Self>) {
305        self.downstream_client = None;
306    }
307
308    async fn handle_update_worktree_settings(
309        this: Model<Self>,
310        envelope: TypedEnvelope<proto::UpdateWorktreeSettings>,
311        mut cx: AsyncAppContext,
312    ) -> anyhow::Result<()> {
313        let kind = match envelope.payload.kind {
314            Some(kind) => proto::LocalSettingsKind::from_i32(kind)
315                .with_context(|| format!("unknown kind {kind}"))?,
316            None => proto::LocalSettingsKind::Settings,
317        };
318        this.update(&mut cx, |this, cx| {
319            let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
320            let Some(worktree) = this
321                .worktree_store
322                .read(cx)
323                .worktree_for_id(worktree_id, cx)
324            else {
325                return;
326            };
327
328            this.update_settings(
329                worktree,
330                [(
331                    PathBuf::from(&envelope.payload.path).into(),
332                    local_settings_kind_from_proto(kind),
333                    envelope.payload.content,
334                )],
335                cx,
336            );
337        })?;
338        Ok(())
339    }
340
341    pub async fn handle_update_user_settings(
342        settings_observer: Model<Self>,
343        envelope: TypedEnvelope<proto::UpdateUserSettings>,
344        mut cx: AsyncAppContext,
345    ) -> anyhow::Result<()> {
346        match envelope.payload.kind() {
347            proto::update_user_settings::Kind::Settings => {
348                cx.update_global(move |settings_store: &mut SettingsStore, cx| {
349                    settings_store.set_user_settings(&envelope.payload.content, cx)
350                })
351            }
352            proto::update_user_settings::Kind::Tasks => {
353                settings_observer.update(&mut cx, |settings_observer, cx| {
354                    settings_observer.task_store.update(cx, |task_store, cx| {
355                        task_store.update_user_tasks(None, Some(&envelope.payload.content), cx)
356                    })
357                })
358            }
359        }??;
360
361        Ok(())
362    }
363
364    pub fn maintain_ssh_settings(&self, ssh: AnyProtoClient, cx: &mut ModelContext<Self>) {
365        let settings_store = cx.global::<SettingsStore>();
366
367        let mut settings = settings_store.raw_user_settings().clone();
368        if let Some(content) = serde_json::to_string(&settings).log_err() {
369            ssh.send(proto::UpdateUserSettings {
370                project_id: 0,
371                content,
372                kind: Some(proto::LocalSettingsKind::Settings.into()),
373            })
374            .log_err();
375        }
376
377        let weak_client = ssh.downgrade();
378        cx.observe_global::<SettingsStore>(move |_, cx| {
379            let new_settings = cx.global::<SettingsStore>().raw_user_settings();
380            if &settings != new_settings {
381                settings = new_settings.clone()
382            }
383            if let Some(content) = serde_json::to_string(&settings).log_err() {
384                if let Some(ssh) = weak_client.upgrade() {
385                    ssh.send(proto::UpdateUserSettings {
386                        project_id: 0,
387                        content,
388                        kind: Some(proto::LocalSettingsKind::Settings.into()),
389                    })
390                    .log_err();
391                }
392            }
393        })
394        .detach();
395    }
396
397    fn on_worktree_store_event(
398        &mut self,
399        _: Model<WorktreeStore>,
400        event: &WorktreeStoreEvent,
401        cx: &mut ModelContext<Self>,
402    ) {
403        if let WorktreeStoreEvent::WorktreeAdded(worktree) = event {
404            cx.subscribe(worktree, |this, worktree, event, cx| {
405                if let worktree::Event::UpdatedEntries(changes) = event {
406                    this.update_local_worktree_settings(&worktree, changes, cx)
407                }
408            })
409            .detach()
410        }
411    }
412
413    fn update_local_worktree_settings(
414        &mut self,
415        worktree: &Model<Worktree>,
416        changes: &UpdatedEntriesSet,
417        cx: &mut ModelContext<Self>,
418    ) {
419        let SettingsObserverMode::Local(fs) = &self.mode else {
420            return;
421        };
422
423        let mut settings_contents = Vec::new();
424        for (path, _, change) in changes.iter() {
425            let (settings_dir, kind) = if path.ends_with(local_settings_file_relative_path()) {
426                let settings_dir = Arc::<Path>::from(
427                    path.ancestors()
428                        .nth(local_settings_file_relative_path().components().count())
429                        .unwrap(),
430                );
431                (settings_dir, LocalSettingsKind::Settings)
432            } else if path.ends_with(local_tasks_file_relative_path()) {
433                let settings_dir = Arc::<Path>::from(
434                    path.ancestors()
435                        .nth(
436                            local_tasks_file_relative_path()
437                                .components()
438                                .count()
439                                .saturating_sub(1),
440                        )
441                        .unwrap(),
442                );
443                (settings_dir, LocalSettingsKind::Tasks)
444            } else if path.ends_with(local_vscode_tasks_file_relative_path()) {
445                let settings_dir = Arc::<Path>::from(
446                    path.ancestors()
447                        .nth(
448                            local_vscode_tasks_file_relative_path()
449                                .components()
450                                .count()
451                                .saturating_sub(1),
452                        )
453                        .unwrap(),
454                );
455                (settings_dir, LocalSettingsKind::Tasks)
456            } else {
457                continue;
458            };
459
460            let removed = change == &PathChange::Removed;
461            let fs = fs.clone();
462            let abs_path = match worktree.read(cx).absolutize(path) {
463                Ok(abs_path) => abs_path,
464                Err(e) => {
465                    log::warn!("Cannot absolutize {path:?} received as {change:?} FS change: {e}");
466                    continue;
467                }
468            };
469            settings_contents.push(async move {
470                (
471                    settings_dir,
472                    kind,
473                    if removed {
474                        None
475                    } else {
476                        Some(
477                            async move {
478                                let content = fs.load(&abs_path).await?;
479                                if abs_path.ends_with(local_vscode_tasks_file_relative_path()) {
480                                    let vscode_tasks =
481                                        parse_json_with_comments::<VsCodeTaskFile>(&content)
482                                            .with_context(|| {
483                                                format!("parsing VSCode tasks, file {abs_path:?}")
484                                            })?;
485                                    let zed_tasks = TaskTemplates::try_from(vscode_tasks)
486                                        .with_context(|| {
487                                            format!(
488                                        "converting VSCode tasks into Zed ones, file {abs_path:?}"
489                                    )
490                                        })?;
491                                    serde_json::to_string(&zed_tasks).with_context(|| {
492                                        format!(
493                                            "serializing Zed tasks into JSON, file {abs_path:?}"
494                                        )
495                                    })
496                                } else {
497                                    Ok(content)
498                                }
499                            }
500                            .await,
501                        )
502                    },
503                )
504            });
505        }
506
507        if settings_contents.is_empty() {
508            return;
509        }
510
511        let worktree = worktree.clone();
512        cx.spawn(move |this, cx| async move {
513            let settings_contents: Vec<(Arc<Path>, _, _)> =
514                futures::future::join_all(settings_contents).await;
515            cx.update(|cx| {
516                this.update(cx, |this, cx| {
517                    this.update_settings(
518                        worktree,
519                        settings_contents.into_iter().map(|(path, kind, content)| {
520                            (path, kind, content.and_then(|c| c.log_err()))
521                        }),
522                        cx,
523                    )
524                })
525            })
526        })
527        .detach();
528    }
529
530    fn update_settings(
531        &mut self,
532        worktree: Model<Worktree>,
533        settings_contents: impl IntoIterator<Item = (Arc<Path>, LocalSettingsKind, Option<String>)>,
534        cx: &mut ModelContext<Self>,
535    ) {
536        let worktree_id = worktree.read(cx).id();
537        let remote_worktree_id = worktree.read(cx).id();
538        let task_store = self.task_store.clone();
539
540        for (directory, kind, file_content) in settings_contents {
541            let result = match kind {
542                LocalSettingsKind::Settings | LocalSettingsKind::Editorconfig => cx
543                    .update_global::<SettingsStore, anyhow::Result<()>>(|store, cx| {
544                        store.set_local_settings(
545                            worktree_id,
546                            directory.clone(),
547                            kind,
548                            file_content.as_deref(),
549                            cx,
550                        )
551                    }),
552                LocalSettingsKind::Tasks => task_store.update(cx, |task_store, cx| {
553                    task_store.update_user_tasks(
554                        Some(SettingsLocation {
555                            worktree_id,
556                            path: directory.as_ref(),
557                        }),
558                        file_content.as_deref(),
559                        cx,
560                    )
561                }),
562            };
563
564            if let Some(downstream_client) = &self.downstream_client {
565                downstream_client
566                    .send(proto::UpdateWorktreeSettings {
567                        project_id: self.project_id,
568                        worktree_id: remote_worktree_id.to_proto(),
569                        path: directory.to_string_lossy().into_owned(),
570                        content: file_content,
571                        kind: Some(local_settings_kind_to_proto(kind).into()),
572                    })
573                    .log_err();
574            }
575
576            match result {
577                Err(error) => {
578                    if let Ok(error) = error.downcast::<InvalidSettingsError>() {
579                        if let InvalidSettingsError::LocalSettings {
580                            ref path,
581                            ref message,
582                        } = error
583                        {
584                            log::error!(
585                                "Failed to set local settings in {:?}: {:?}",
586                                path,
587                                message
588                            );
589                            cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Err(error)));
590                        }
591                    }
592                }
593                Ok(()) => {
594                    cx.emit(SettingsObserverEvent::LocalSettingsUpdated(Ok(())));
595                }
596            }
597        }
598    }
599}
600
601pub fn local_settings_kind_from_proto(kind: proto::LocalSettingsKind) -> LocalSettingsKind {
602    match kind {
603        proto::LocalSettingsKind::Settings => LocalSettingsKind::Settings,
604        proto::LocalSettingsKind::Tasks => LocalSettingsKind::Tasks,
605        proto::LocalSettingsKind::Editorconfig => LocalSettingsKind::Editorconfig,
606    }
607}
608
609pub fn local_settings_kind_to_proto(kind: LocalSettingsKind) -> proto::LocalSettingsKind {
610    match kind {
611        LocalSettingsKind::Settings => proto::LocalSettingsKind::Settings,
612        LocalSettingsKind::Tasks => proto::LocalSettingsKind::Tasks,
613        LocalSettingsKind::Editorconfig => proto::LocalSettingsKind::Editorconfig,
614    }
615}