project_settings.rs

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